Skip to content

Commit f7a2c19

Browse files
committed
sqlc-test-setup: run Spanner Omni on kernels without IPv6
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. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L8QoGi6VRtADkb4jL3BFHP
1 parent 0942ad2 commit f7a2c19

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)