Skip to content

Commit 38c9f89

Browse files
kyleconroyclaude
andauthored
sqlc-test-setup: run Spanner Omni on kernels without IPv6 (#4641)
Spanner Omni's services open IPv6 sockets at startup and abort when the kernel has no IPv6, as in Claude Code's remote environments. When opening an AF_INET6 socket fails with EAFNOSUPPORT, start now builds v6to4, a small LD_PRELOAD library that runs those sockets over IPv4, and preloads it into Omni. Kernels with IPv6 start Omni as before. Claude-Session: https://claude.ai/code/session_01L8QoGi6VRtADkb4jL3BFHP Co-authored-by: Claude <noreply@anthropic.com>
1 parent 366857d commit 38c9f89

5 files changed

Lines changed: 285 additions & 0 deletions

File tree

‎cmd/sqlc-test-setup/ipv6_linux.go‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build linux
2+
3+
package main
4+
5+
import (
6+
"errors"
7+
"syscall"
8+
)
9+
10+
// kernelLacksIPv6 reports whether the kernel was built without IPv6, so
11+
// that opening an AF_INET6 socket fails with EAFNOSUPPORT. Some sandboxes
12+
// run such kernels, Claude Code's remote environments among them. Any other
13+
// outcome, including another error, leaves Spanner Omni to run as it is.
14+
func kernelLacksIPv6() bool {
15+
fd, err := syscall.Socket(syscall.AF_INET6, syscall.SOCK_STREAM, 0)
16+
if err == nil {
17+
syscall.Close(fd)
18+
return false
19+
}
20+
return errors.Is(err, syscall.EAFNOSUPPORT)
21+
}

‎cmd/sqlc-test-setup/ipv6_other.go‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//go:build !linux
2+
3+
package main
4+
5+
// kernelLacksIPv6 is false off Linux, where Spanner Omni is not published.
6+
func kernelLacksIPv6() bool {
7+
return false
8+
}

‎cmd/sqlc-test-setup/main.go‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,9 +740,15 @@ func startSpannerOmni() error {
740740
}
741741
defer logFile.Close()
742742

743+
env, err := omniEnv(dir)
744+
if err != nil {
745+
return err
746+
}
747+
743748
// The launcher supervises the server processes for as long as it runs,
744749
// so it is detached from this process and left running.
745750
cmd := exec.Command(launcher, "start-single-server", "--base-dir", data)
751+
cmd.Env = env
746752
cmd.Stdout = logFile
747753
cmd.Stderr = logFile
748754
cmd.SysProcAttr = detachedProcess()

‎cmd/sqlc-test-setup/omni_ipv6.go‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package main
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"log"
7+
"os"
8+
"path/filepath"
9+
)
10+
11+
// v6to4Source is an LD_PRELOAD library that runs a program's IPv6 sockets
12+
// over IPv4; see the comment at the top of the file.
13+
//
14+
//go:embed v6to4/v6to4.c
15+
var v6to4Source []byte
16+
17+
// omniEnv is the environment Spanner Omni is started with. On a kernel with
18+
// IPv6 it is this process's own. On one without, Omni's services abort at
19+
// startup, as they open IPv6 sockets and have no flag to stop it ("Failed
20+
// to create socket with ::socket ... Address family not supported by
21+
// protocol" in their logs), so the v6to4 library is built into dir and
22+
// preloaded into every Omni process, which inherit the environment from the
23+
// launcher.
24+
func omniEnv(dir string) ([]string, error) {
25+
env := os.Environ()
26+
if !kernelLacksIPv6() {
27+
return env, nil
28+
}
29+
log.Println("the kernel has no IPv6, which spanner omni needs: preloading v6to4 to run its IPv6 sockets over IPv4")
30+
lib, err := buildV6to4(dir)
31+
if err != nil {
32+
return nil, fmt.Errorf("building v6to4 for a kernel without IPv6: %w", err)
33+
}
34+
return append(env, "LD_PRELOAD="+lib), nil
35+
}
36+
37+
// buildV6to4 compiles the v6to4 library into dir with the system's C
38+
// compiler and returns its path.
39+
func buildV6to4(dir string) (string, error) {
40+
if !commandExists("cc") {
41+
return "", fmt.Errorf("no C compiler (cc) is installed")
42+
}
43+
src := filepath.Join(dir, "v6to4.c")
44+
if err := os.WriteFile(src, v6to4Source, 0o644); err != nil {
45+
return "", err
46+
}
47+
lib := filepath.Join(dir, "libv6to4.so")
48+
// No fortified functions or stack protector, so the library needs no
49+
// glibc symbol newer than the 2.27 Omni ships with.
50+
err := run("cc", "-O2", "-shared", "-fPIC", "-fno-stack-protector",
51+
"-U_FORTIFY_SOURCE", "-D_FORTIFY_SOURCE=0", "-o", lib, src)
52+
if err != nil {
53+
return "", err
54+
}
55+
return lib, nil
56+
}

‎cmd/sqlc-test-setup/v6to4/v6to4.c‎

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// v6to4 is an LD_PRELOAD library that lets a program which insists on IPv6
2+
// sockets run on a kernel built without IPv6, where socket(AF_INET6, ...)
3+
// fails with EAFNOSUPPORT. sqlc-test-setup preloads it into Spanner Omni on
4+
// such kernels only, since Omni's services open IPv6 sockets and abort when
5+
// they cannot; see omni_ipv6.go.
6+
//
7+
// An AF_INET6 socket is created as an AF_INET one and remembered. Addresses
8+
// passed in are rewritten from sockaddr_in6 to sockaddr_in (:: to 0.0.0.0,
9+
// ::1 to 127.0.0.1, ::ffff:a.b.c.d to a.b.c.d), and addresses handed back
10+
// are rewritten the other way, so the program goes on seeing IPv6. An
11+
// address with no IPv4 equivalent fails with EADDRNOTAVAIL.
12+
//
13+
// Omni runs on its own glibc 2.27, so the library calls the kernel through
14+
// syscall() rather than looking up the real functions, and must not need a
15+
// glibc symbol newer than that: build it without _FORTIFY_SOURCE or the
16+
// stack protector.
17+
#define _GNU_SOURCE
18+
#include <errno.h>
19+
#include <netinet/in.h>
20+
#include <string.h>
21+
#include <sys/socket.h>
22+
#include <sys/syscall.h>
23+
#include <unistd.h>
24+
25+
#define MAXFD 65536
26+
static unsigned char fake[MAXFD];
27+
28+
static int isfake(int fd) { return fd >= 0 && fd < MAXFD && fake[fd]; }
29+
static void mark(int fd, int v) { if (fd >= 0 && fd < MAXFD) fake[fd] = v; }
30+
31+
// to4 rewrites a sockaddr_in6 as a sockaddr_in in out; it returns the length to
32+
// pass on, or -1 when the address has no IPv4 equivalent.
33+
static socklen_t to4(const struct sockaddr *a, socklen_t len, struct sockaddr_in *out) {
34+
const struct sockaddr_in6 *s6 = (const struct sockaddr_in6 *)a;
35+
static const unsigned char any[16] = {0};
36+
static const unsigned char lo[16] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
37+
memset(out, 0, sizeof *out);
38+
out->sin_family = AF_INET;
39+
out->sin_port = s6->sin6_port;
40+
const unsigned char *b = s6->sin6_addr.s6_addr;
41+
if (memcmp(b, any, 16) == 0) out->sin_addr.s_addr = htonl(INADDR_ANY);
42+
else if (memcmp(b, lo, 16) == 0) out->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
43+
else if (memcmp(b, any, 10) == 0 && b[10] == 0xff && b[11] == 0xff) memcpy(&out->sin_addr, b + 12, 4);
44+
else return (socklen_t)-1;
45+
(void)len;
46+
return sizeof *out;
47+
}
48+
49+
// to6 rewrites the sockaddr_in the kernel wrote into a v4-mapped sockaddr_in6.
50+
static void to6(struct sockaddr *a, socklen_t *len, socklen_t cap) {
51+
if (!a || !len || *len < sizeof(struct sockaddr_in) || a->sa_family != AF_INET) return;
52+
struct sockaddr_in s4;
53+
memcpy(&s4, a, sizeof s4);
54+
struct sockaddr_in6 s6;
55+
memset(&s6, 0, sizeof s6);
56+
s6.sin6_family = AF_INET6;
57+
s6.sin6_port = s4.sin_port;
58+
if (s4.sin_addr.s_addr == htonl(INADDR_LOOPBACK)) s6.sin6_addr.s6_addr[15] = 1;
59+
else if (s4.sin_addr.s_addr != htonl(INADDR_ANY)) {
60+
s6.sin6_addr.s6_addr[10] = 0xff;
61+
s6.sin6_addr.s6_addr[11] = 0xff;
62+
memcpy(s6.sin6_addr.s6_addr + 12, &s4.sin_addr, 4);
63+
}
64+
memcpy(a, &s6, cap < sizeof s6 ? cap : sizeof s6);
65+
*len = sizeof s6;
66+
}
67+
68+
int socket(int domain, int type, int protocol) {
69+
int fake6 = domain == AF_INET6;
70+
int fd = syscall(SYS_socket, fake6 ? AF_INET : domain, type, protocol);
71+
if (fd >= 0) mark(fd, fake6);
72+
return fd;
73+
}
74+
75+
int close(int fd) {
76+
mark(fd, 0);
77+
return syscall(SYS_close, fd);
78+
}
79+
80+
#define WITH4(fd, addr, len, call4, call) \
81+
if ((addr) && (addr)->sa_family == AF_INET6 && isfake(fd)) { \
82+
struct sockaddr_in s4; \
83+
socklen_t l4 = to4((addr), (len), &s4); \
84+
if (l4 == (socklen_t)-1) { errno = EADDRNOTAVAIL; return -1; } \
85+
return call4; \
86+
} \
87+
return call;
88+
89+
int bind(int fd, const struct sockaddr *addr, socklen_t len) {
90+
WITH4(fd, addr, len, syscall(SYS_bind, fd, &s4, l4), syscall(SYS_bind, fd, addr, len))
91+
}
92+
93+
int connect(int fd, const struct sockaddr *addr, socklen_t len) {
94+
WITH4(fd, addr, len, syscall(SYS_connect, fd, &s4, l4), syscall(SYS_connect, fd, addr, len))
95+
}
96+
97+
ssize_t sendto(int fd, const void *buf, size_t n, int flags, const struct sockaddr *addr, socklen_t len) {
98+
WITH4(fd, addr, len, syscall(SYS_sendto, fd, buf, n, flags, &s4, l4),
99+
syscall(SYS_sendto, fd, buf, n, flags, addr, len))
100+
}
101+
102+
ssize_t sendmsg(int fd, const struct msghdr *msg, int flags) {
103+
if (msg && msg->msg_name && isfake(fd) && ((struct sockaddr *)msg->msg_name)->sa_family == AF_INET6) {
104+
struct sockaddr_in s4;
105+
socklen_t l4 = to4(msg->msg_name, msg->msg_namelen, &s4);
106+
if (l4 == (socklen_t)-1) { errno = EADDRNOTAVAIL; return -1; }
107+
struct msghdr m = *msg;
108+
m.msg_name = &s4;
109+
m.msg_namelen = l4;
110+
return syscall(SYS_sendmsg, fd, &m, flags);
111+
}
112+
return syscall(SYS_sendmsg, fd, msg, flags);
113+
}
114+
115+
#define BACK6(fd, ret, addr, lenp) \
116+
do { \
117+
if ((ret) >= 0 && isfake(fd) && (addr) && (lenp)) to6((addr), (lenp), cap); \
118+
} while (0)
119+
120+
int getsockname(int fd, struct sockaddr *addr, socklen_t *len) {
121+
socklen_t cap = len ? *len : 0;
122+
struct sockaddr_in6 tmp; socklen_t tl = sizeof tmp;
123+
if (!isfake(fd)) return syscall(SYS_getsockname, fd, addr, len);
124+
int r = syscall(SYS_getsockname, fd, &tmp, &tl);
125+
if (r < 0) return r;
126+
to6((struct sockaddr *)&tmp, &tl, sizeof tmp);
127+
memcpy(addr, &tmp, cap < tl ? cap : tl);
128+
*len = tl;
129+
return r;
130+
}
131+
132+
int getpeername(int fd, struct sockaddr *addr, socklen_t *len) {
133+
socklen_t cap = len ? *len : 0;
134+
struct sockaddr_in6 tmp; socklen_t tl = sizeof tmp;
135+
if (!isfake(fd)) return syscall(SYS_getpeername, fd, addr, len);
136+
int r = syscall(SYS_getpeername, fd, &tmp, &tl);
137+
if (r < 0) return r;
138+
to6((struct sockaddr *)&tmp, &tl, sizeof tmp);
139+
memcpy(addr, &tmp, cap < tl ? cap : tl);
140+
*len = tl;
141+
return r;
142+
}
143+
144+
static int acc(int fd, struct sockaddr *addr, socklen_t *len, int flags) {
145+
if (!isfake(fd)) return syscall(SYS_accept4, fd, addr, len, flags);
146+
struct sockaddr_in6 tmp; socklen_t tl = sizeof tmp;
147+
int r = syscall(SYS_accept4, fd, &tmp, &tl, flags);
148+
if (r < 0) return r;
149+
mark(r, 1);
150+
if (addr && len) {
151+
socklen_t cap = *len;
152+
to6((struct sockaddr *)&tmp, &tl, sizeof tmp);
153+
memcpy(addr, &tmp, cap < tl ? cap : tl);
154+
*len = tl;
155+
}
156+
return r;
157+
}
158+
int accept(int fd, struct sockaddr *addr, socklen_t *len) { return acc(fd, addr, len, 0); }
159+
int accept4(int fd, struct sockaddr *addr, socklen_t *len, int flags) { return acc(fd, addr, len, flags); }
160+
161+
ssize_t recvfrom(int fd, void *buf, size_t n, int flags, struct sockaddr *addr, socklen_t *len) {
162+
socklen_t cap = len ? *len : 0;
163+
ssize_t r = syscall(SYS_recvfrom, fd, buf, n, flags, addr, len);
164+
BACK6(fd, r, addr, len);
165+
return r;
166+
}
167+
168+
ssize_t recvmsg(int fd, struct msghdr *msg, int flags) {
169+
socklen_t cap = msg ? msg->msg_namelen : 0;
170+
ssize_t r = syscall(SYS_recvmsg, fd, msg, flags);
171+
if (r >= 0 && msg && msg->msg_name) BACK6(fd, r, (struct sockaddr *)msg->msg_name, &msg->msg_namelen);
172+
return r;
173+
}
174+
175+
int setsockopt(int fd, int level, int name, const void *val, socklen_t len) {
176+
if (isfake(fd) && level == IPPROTO_IPV6) {
177+
if (name == IPV6_TCLASS) return syscall(SYS_setsockopt, fd, IPPROTO_IP, IP_TOS, val, len);
178+
return 0; // IPV6_V6ONLY and the rest have no IPv4 meaning
179+
}
180+
return syscall(SYS_setsockopt, fd, level, name, val, len);
181+
}
182+
183+
int getsockopt(int fd, int level, int name, void *val, socklen_t *len) {
184+
if (isfake(fd) && level == SOL_SOCKET && name == SO_DOMAIN && val && len && *len >= sizeof(int)) {
185+
*(int *)val = AF_INET6;
186+
*len = sizeof(int);
187+
return 0;
188+
}
189+
if (isfake(fd) && level == IPPROTO_IPV6) {
190+
if (val && len && *len >= sizeof(int)) { *(int *)val = 0; *len = sizeof(int); }
191+
return 0;
192+
}
193+
return syscall(SYS_getsockopt, fd, level, name, val, len);
194+
}

0 commit comments

Comments
 (0)