diff --git a/cmd/sqlc-test-setup/ipv6_linux.go b/cmd/sqlc-test-setup/ipv6_linux.go new file mode 100644 index 0000000000..1877a123e5 --- /dev/null +++ b/cmd/sqlc-test-setup/ipv6_linux.go @@ -0,0 +1,21 @@ +//go:build linux + +package main + +import ( + "errors" + "syscall" +) + +// kernelLacksIPv6 reports whether the kernel was built without IPv6, so +// that opening an AF_INET6 socket fails with EAFNOSUPPORT. Some sandboxes +// run such kernels, Claude Code's remote environments among them. Any other +// outcome, including another error, leaves Spanner Omni to run as it is. +func kernelLacksIPv6() bool { + fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, 0) + if err == nil { + syscall.Close(fd) + return false + } + return errors.Is(err, syscall.EAFNOSUPPORT) +} diff --git a/cmd/sqlc-test-setup/ipv6_other.go b/cmd/sqlc-test-setup/ipv6_other.go new file mode 100644 index 0000000000..4c6cf5f570 --- /dev/null +++ b/cmd/sqlc-test-setup/ipv6_other.go @@ -0,0 +1,8 @@ +//go:build !linux + +package main + +// kernelLacksIPv6 is false off Linux, where Spanner Omni is not published. +func kernelLacksIPv6() bool { + return false +} diff --git a/cmd/sqlc-test-setup/main.go b/cmd/sqlc-test-setup/main.go index befe938930..30c0280ac2 100644 --- a/cmd/sqlc-test-setup/main.go +++ b/cmd/sqlc-test-setup/main.go @@ -740,9 +740,15 @@ func startSpannerOmni() error { } defer logFile.Close() + env, err := omniEnv(dir) + if err != nil { + return err + } + // The launcher supervises the server processes for as long as it runs, // so it is detached from this process and left running. cmd := exec.Command(launcher, "start-single-server", "--base-dir", data) + cmd.Env = env cmd.Stdout = logFile cmd.Stderr = logFile cmd.SysProcAttr = detachedProcess() diff --git a/cmd/sqlc-test-setup/omni_ipv6.go b/cmd/sqlc-test-setup/omni_ipv6.go new file mode 100644 index 0000000000..44c053654a --- /dev/null +++ b/cmd/sqlc-test-setup/omni_ipv6.go @@ -0,0 +1,56 @@ +package main + +import ( + _ "embed" + "fmt" + "log" + "os" + "path/filepath" +) + +// v6to4Source is an LD_PRELOAD library that runs a program's IPv6 sockets +// over IPv4; see the comment at the top of the file. +// +//go:embed v6to4/v6to4.c +var v6to4Source []byte + +// omniEnv is the environment Spanner Omni is started with. On a kernel with +// IPv6 it is this process's own. On one without, Omni's services abort at +// startup, as they open IPv6 sockets and have no flag to stop it ("Failed +// to create socket with ::socket ... Address family not supported by +// protocol" in their logs), so the v6to4 library is built into dir and +// preloaded into every Omni process, which inherit the environment from the +// launcher. +func omniEnv(dir string) ([]string, error) { + env := os.Environ() + if !kernelLacksIPv6() { + return env, nil + } + log.Println("the kernel has no IPv6, which spanner omni needs: preloading v6to4 to run its IPv6 sockets over IPv4") + lib, err := buildV6to4(dir) + if err != nil { + return nil, fmt.Errorf("building v6to4 for a kernel without IPv6: %w", err) + } + return append(env, "LD_PRELOAD="+lib), nil +} + +// buildV6to4 compiles the v6to4 library into dir with the system's C +// compiler and returns its path. +func buildV6to4(dir string) (string, error) { + if !commandExists("cc") { + return "", fmt.Errorf("no C compiler (cc) is installed") + } + src := filepath.Join(dir, "v6to4.c") + if err := os.WriteFile(src, v6to4Source, 0o644); err != nil { + return "", err + } + lib := filepath.Join(dir, "libv6to4.so") + // No fortified functions or stack protector, so the library needs no + // glibc symbol newer than the 2.27 Omni ships with. + err := run("cc", "-O2", "-shared", "-fPIC", "-fno-stack-protector", + "-U_FORTIFY_SOURCE", "-D_FORTIFY_SOURCE=0", "-o", lib, src) + if err != nil { + return "", err + } + return lib, nil +} diff --git a/cmd/sqlc-test-setup/v6to4/v6to4.c b/cmd/sqlc-test-setup/v6to4/v6to4.c new file mode 100644 index 0000000000..87b2163669 --- /dev/null +++ b/cmd/sqlc-test-setup/v6to4/v6to4.c @@ -0,0 +1,194 @@ +// v6to4 is an LD_PRELOAD library that lets a program which insists on IPv6 +// sockets run on a kernel built without IPv6, where socket(AF_INET6, ...) +// fails with EAFNOSUPPORT. sqlc-test-setup preloads it into Spanner Omni on +// such kernels only, since Omni's services open IPv6 sockets and abort when +// they cannot; see omni_ipv6.go. +// +// An AF_INET6 socket is created as an AF_INET one and remembered. Addresses +// passed in are rewritten from sockaddr_in6 to sockaddr_in (:: to 0.0.0.0, +// ::1 to 127.0.0.1, ::ffff:a.b.c.d to a.b.c.d), and addresses handed back +// are rewritten the other way, so the program goes on seeing IPv6. An +// address with no IPv4 equivalent fails with EADDRNOTAVAIL. +// +// Omni runs on its own glibc 2.27, so the library calls the kernel through +// syscall() rather than looking up the real functions, and must not need a +// glibc symbol newer than that: build it without _FORTIFY_SOURCE or the +// stack protector. +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#define MAXFD 65536 +static unsigned char fake[MAXFD]; + +static int isfake(int fd) { return fd >= 0 && fd < MAXFD && fake[fd]; } +static void mark(int fd, int v) { if (fd >= 0 && fd < MAXFD) fake[fd] = v; } + +// to4 rewrites a sockaddr_in6 as a sockaddr_in in out; it returns the length to +// pass on, or -1 when the address has no IPv4 equivalent. +static socklen_t to4(const struct sockaddr *a, socklen_t len, struct sockaddr_in *out) { + const struct sockaddr_in6 *s6 = (const struct sockaddr_in6 *)a; + static const unsigned char any[16] = {0}; + static const unsigned char lo[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; + memset(out, 0, sizeof *out); + out->sin_family = AF_INET; + out->sin_port = s6->sin6_port; + const unsigned char *b = s6->sin6_addr.s6_addr; + if (memcmp(b, any, 16) == 0) out->sin_addr.s_addr = htonl(INADDR_ANY); + else if (memcmp(b, lo, 16) == 0) out->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + else if (memcmp(b, any, 10) == 0 && b[10] == 0xff && b[11] == 0xff) memcpy(&out->sin_addr, b + 12, 4); + else return (socklen_t)-1; + (void)len; + return sizeof *out; +} + +// to6 rewrites the sockaddr_in the kernel wrote into a v4-mapped sockaddr_in6. +static void to6(struct sockaddr *a, socklen_t *len, socklen_t cap) { + if (!a || !len || *len < sizeof(struct sockaddr_in) || a->sa_family != AF_INET) return; + struct sockaddr_in s4; + memcpy(&s4, a, sizeof s4); + struct sockaddr_in6 s6; + memset(&s6, 0, sizeof s6); + s6.sin6_family = AF_INET6; + s6.sin6_port = s4.sin_port; + if (s4.sin_addr.s_addr == htonl(INADDR_LOOPBACK)) s6.sin6_addr.s6_addr[15] = 1; + else if (s4.sin_addr.s_addr != htonl(INADDR_ANY)) { + s6.sin6_addr.s6_addr[10] = 0xff; + s6.sin6_addr.s6_addr[11] = 0xff; + memcpy(s6.sin6_addr.s6_addr + 12, &s4.sin_addr, 4); + } + memcpy(a, &s6, cap < sizeof s6 ? cap : sizeof s6); + *len = sizeof s6; +} + +int socket(int domain, int type, int protocol) { + int fake6 = domain == AF_INET6; + int fd = syscall(SYS_socket, fake6 ? AF_INET : domain, type, protocol); + if (fd >= 0) mark(fd, fake6); + return fd; +} + +int close(int fd) { + mark(fd, 0); + return syscall(SYS_close, fd); +} + +#define WITH4(fd, addr, len, call4, call) \ + if ((addr) && (addr)->sa_family == AF_INET6 && isfake(fd)) { \ + struct sockaddr_in s4; \ + socklen_t l4 = to4((addr), (len), &s4); \ + if (l4 == (socklen_t)-1) { errno = EADDRNOTAVAIL; return -1; } \ + return call4; \ + } \ + return call; + +int bind(int fd, const struct sockaddr *addr, socklen_t len) { + WITH4(fd, addr, len, syscall(SYS_bind, fd, &s4, l4), syscall(SYS_bind, fd, addr, len)) +} + +int connect(int fd, const struct sockaddr *addr, socklen_t len) { + WITH4(fd, addr, len, syscall(SYS_connect, fd, &s4, l4), syscall(SYS_connect, fd, addr, len)) +} + +ssize_t sendto(int fd, const void *buf, size_t n, int flags, const struct sockaddr *addr, socklen_t len) { + WITH4(fd, addr, len, syscall(SYS_sendto, fd, buf, n, flags, &s4, l4), + syscall(SYS_sendto, fd, buf, n, flags, addr, len)) +} + +ssize_t sendmsg(int fd, const struct msghdr *msg, int flags) { + if (msg && msg->msg_name && isfake(fd) && ((struct sockaddr *)msg->msg_name)->sa_family == AF_INET6) { + struct sockaddr_in s4; + socklen_t l4 = to4(msg->msg_name, msg->msg_namelen, &s4); + if (l4 == (socklen_t)-1) { errno = EADDRNOTAVAIL; return -1; } + struct msghdr m = *msg; + m.msg_name = &s4; + m.msg_namelen = l4; + return syscall(SYS_sendmsg, fd, &m, flags); + } + return syscall(SYS_sendmsg, fd, msg, flags); +} + +#define BACK6(fd, ret, addr, lenp) \ + do { \ + if ((ret) >= 0 && isfake(fd) && (addr) && (lenp)) to6((addr), (lenp), cap); \ + } while (0) + +int getsockname(int fd, struct sockaddr *addr, socklen_t *len) { + socklen_t cap = len ? *len : 0; + struct sockaddr_in6 tmp; socklen_t tl = sizeof tmp; + if (!isfake(fd)) return syscall(SYS_getsockname, fd, addr, len); + int r = syscall(SYS_getsockname, fd, &tmp, &tl); + if (r < 0) return r; + to6((struct sockaddr *)&tmp, &tl, sizeof tmp); + memcpy(addr, &tmp, cap < tl ? cap : tl); + *len = tl; + return r; +} + +int getpeername(int fd, struct sockaddr *addr, socklen_t *len) { + socklen_t cap = len ? *len : 0; + struct sockaddr_in6 tmp; socklen_t tl = sizeof tmp; + if (!isfake(fd)) return syscall(SYS_getpeername, fd, addr, len); + int r = syscall(SYS_getpeername, fd, &tmp, &tl); + if (r < 0) return r; + to6((struct sockaddr *)&tmp, &tl, sizeof tmp); + memcpy(addr, &tmp, cap < tl ? cap : tl); + *len = tl; + return r; +} + +static int acc(int fd, struct sockaddr *addr, socklen_t *len, int flags) { + if (!isfake(fd)) return syscall(SYS_accept4, fd, addr, len, flags); + struct sockaddr_in6 tmp; socklen_t tl = sizeof tmp; + int r = syscall(SYS_accept4, fd, &tmp, &tl, flags); + if (r < 0) return r; + mark(r, 1); + if (addr && len) { + socklen_t cap = *len; + to6((struct sockaddr *)&tmp, &tl, sizeof tmp); + memcpy(addr, &tmp, cap < tl ? cap : tl); + *len = tl; + } + return r; +} +int accept(int fd, struct sockaddr *addr, socklen_t *len) { return acc(fd, addr, len, 0); } +int accept4(int fd, struct sockaddr *addr, socklen_t *len, int flags) { return acc(fd, addr, len, flags); } + +ssize_t recvfrom(int fd, void *buf, size_t n, int flags, struct sockaddr *addr, socklen_t *len) { + socklen_t cap = len ? *len : 0; + ssize_t r = syscall(SYS_recvfrom, fd, buf, n, flags, addr, len); + BACK6(fd, r, addr, len); + return r; +} + +ssize_t recvmsg(int fd, struct msghdr *msg, int flags) { + socklen_t cap = msg ? msg->msg_namelen : 0; + ssize_t r = syscall(SYS_recvmsg, fd, msg, flags); + if (r >= 0 && msg && msg->msg_name) BACK6(fd, r, (struct sockaddr *)msg->msg_name, &msg->msg_namelen); + return r; +} + +int setsockopt(int fd, int level, int name, const void *val, socklen_t len) { + if (isfake(fd) && level == IPPROTO_IPV6) { + if (name == IPV6_TCLASS) return syscall(SYS_setsockopt, fd, IPPROTO_IP, IP_TOS, val, len); + return 0; // IPV6_V6ONLY and the rest have no IPv4 meaning + } + return syscall(SYS_setsockopt, fd, level, name, val, len); +} + +int getsockopt(int fd, int level, int name, void *val, socklen_t *len) { + if (isfake(fd) && level == SOL_SOCKET && name == SO_DOMAIN && val && len && *len >= sizeof(int)) { + *(int *)val = AF_INET6; + *len = sizeof(int); + return 0; + } + if (isfake(fd) && level == IPPROTO_IPV6) { + if (val && len && *len >= sizeof(int)) { *(int *)val = 0; *len = sizeof(int); } + return 0; + } + return syscall(SYS_getsockopt, fd, level, name, val, len); +}