carlos / platform Public

echoapp + e2e: prove the LISTEN_FDS listener handoff, systemd-free

The reference tenant now serves on an inherited activation socket
(LISTEN_FDS=1, fd 3 — sd_listen_fds shape) before falling back to
-socket/-addr, and the e2e plays root systemd: binds the unix socket,
passes the fd, and shows the tenant serving without touching the path —
the contract that keeps tenant sockets carlos-owned 0600.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 4a21c80740ef9d830620a011ba967389ac80b878 parent 8cc77fa
2 files changed, +107 −8
  • e2e_test.go +80 −0
  • testdata/echoapp/main.go +27 −8
diff --git a/e2e_test.go b/e2e_test.go
index ffad5a956..1efa33faf 100644
--- a/e2e_test.go
+++ b/e2e_test.go
@@ -14,6 +14,7 @@ package main_test
import (
"context"
"crypto/ed25519"
+ "fmt"
"io"
"net"
"net/http"
@@ -88,6 +89,85 @@ func e2eShipBinary(t *testing.T, store release.Store, version string, bin []byte
}
}
+// The carlos-app@.socket listener handoff, systemd-free: the test plays
+// root systemd — binds the tenant's unix socket itself, passes the fd as fd
+// 3 with LISTEN_FDS=1 — and proves the tenant serves on the inherited
+// listener without ever touching the socket path. This is the contract that
+// lets the socket file stay carlos-owned 0600 (only the edge connects,
+// tenants can't reach each other) while a DynamicUser tenant owns the
+// process.
+func TestTenantInheritsActivationSocket(t *testing.T) {
+ goBin, err := exec.LookPath("go")
+ if err != nil {
+ t.Skip("go toolchain not found")
+ }
+ tmp := t.TempDir()
+ appBin := filepath.Join(tmp, "echoapp")
+ build := exec.Command(goBin, "build", "-o", appBin, "./testdata/echoapp")
+ build.Env = os.Environ()
+ if out, err := build.CombinedOutput(); err != nil {
+ t.Fatalf("build echoapp: %v\n%s", err, out)
+ }
+
+ // Short path — sun_path is ~104 bytes and t.TempDir() can blow it.
+ sock := filepath.Join(os.TempDir(), fmt.Sprintf("carlos-e2e-%d.sock", os.Getpid()))
+ os.Remove(sock)
+ t.Cleanup(func() { os.Remove(sock) })
+ ln, err := net.Listen("unix", sock)
+ if err != nil {
+ if strings.Contains(err.Error(), "operation not permitted") {
+ t.Skipf("unix-socket listeners not permitted in this environment: %v", err)
+ }
+ t.Fatalf("listen unix: %v", err)
+ }
+ defer ln.Close()
+ f, err := ln.(*net.UnixListener).File()
+ if err != nil {
+ t.Fatalf("listener fd: %v", err)
+ }
+ defer f.Close()
+
+ app := exec.Command(appBin) // no -socket, no -addr: the fd is the listener
+ app.ExtraFiles = []*os.File{f} // child fd 3, where sd_listen_fds starts
+ app.Env = append(os.Environ(), "LISTEN_FDS=1", "BUILD=sockv1")
+ if err := app.Start(); err != nil {
+ t.Fatalf("start echoapp: %v", err)
+ }
+ t.Cleanup(func() {
+ _ = app.Process.Kill()
+ _, _ = app.Process.Wait()
+ })
+
+ client := &http.Client{Transport: &http.Transport{
+ DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
+ return (&net.Dialer{}).DialContext(ctx, "unix", sock)
+ },
+ }}
+ get := func(path string) (int, string, error) {
+ resp, err := client.Get("http://tenant.local" + path)
+ if err != nil {
+ return 0, "", err
+ }
+ defer resp.Body.Close()
+ b, _ := io.ReadAll(resp.Body)
+ return resp.StatusCode, string(b), nil
+ }
+ deadline := time.Now().Add(10 * time.Second)
+ for {
+ code, body, err := get("/healthz")
+ if err == nil && code == 200 && body == "ok" {
+ break
+ }
+ if time.Now().After(deadline) {
+ t.Fatalf("tenant never healthy on the inherited listener: %d %q %v", code, body, err)
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+ if code, body, err := get("/"); err != nil || code != 200 || !strings.Contains(body, "BUILD=sockv1") {
+ t.Fatalf("GET / over inherited socket = %d %q %v, want 200 containing BUILD=sockv1", code, body, err)
+ }
+}
+
func TestEndToEndSmoke(t *testing.T) {
goBin, err := exec.LookPath("go")
if err != nil {
diff --git a/testdata/echoapp/main.go b/testdata/echoapp/main.go
index d96d78e20..ef5b2264a 100644
--- a/testdata/echoapp/main.go
+++ b/testdata/echoapp/main.go
@@ -1,6 +1,7 @@
// echoapp is the e2e/vet test tenant: a minimal app honoring the platform
-// contract — it accepts -socket (unix) or -addr (TCP), answers GET /healthz
-// with "ok", and echoes its BUILD env everywhere else.
+// contract — it accepts -socket (unix) or -addr (TCP), or inherits a
+// systemd-activated listener (LISTEN_FDS, the carlos-app@.socket contract),
+// answers GET /healthz with "ok", and echoes its BUILD env everywhere else.
package main
import (
@@ -9,6 +10,7 @@ import (
"net"
"net/http"
"os"
+ "strconv"
)
func main() {
@@ -24,12 +26,7 @@ func main() {
fmt.Fprintf(w, "echoapp BUILD=%s", os.Getenv("BUILD"))
})
- network, address := "tcp", *addr
- if *socket != "" {
- network, address = "unix", *socket
- os.Remove(*socket)
- }
- ln, err := net.Listen(network, address)
+ ln, err := listen(*socket, *addr)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
@@ -39,3 +36,25 @@ func main() {
os.Exit(1)
}
}
+
+// listen picks the app's listener, activation first: when systemd (or a test
+// playing systemd) passed a pre-bound socket — LISTEN_FDS=1, the fd at 3 —
+// serve on it and never touch any path. That is the carlos-app@.socket
+// contract: the file is root-bound and carlos-owned so only the edge can
+// connect; the tenant just inherits the fd. Otherwise bind -socket (unix)
+// or -addr (TCP) ourselves.
+func listen(socket, addr string) (net.Listener, error) {
+ if n, _ := strconv.Atoi(os.Getenv("LISTEN_FDS")); n >= 1 {
+ // systemd sets LISTEN_PID to our pid post-fork; accept it unset for
+ // harnesses that exec us directly and can't know the pid in advance.
+ if pid := os.Getenv("LISTEN_PID"); pid == "" || pid == strconv.Itoa(os.Getpid()) {
+ return net.FileListener(os.NewFile(3, "listen-fd"))
+ }
+ }
+ network, address := "tcp", addr
+ if socket != "" {
+ network, address = "unix", socket
+ os.Remove(socket)
+ }
+ return net.Listen(network, address)
+}