mirror of
https://github.com/zap-proto/zip.git
synced 2026-10-19 15:38:57 +00:00
the caller's Host survives the plugin hop, so a child can answer as the brand it was asked as
forward() overwrote the Host header with the DIAL ADDRESS on every proxied request. A client dials its own address — HostClient.Addr, zaphttp.Dial(addr) — and never reads this header to find the far end, so that value was not routing anything. It was destroying the one fact a child needs to serve more than one brand: which host was asked. hanzoai/iam is where it showed. It serves every brand from one instance and resolves each brand's OIDC issuer from the request Host, so mounted as a plugin child it was asked as "/var/lib/cloud/run/iam.sock", matched no brand, and fell back to its single default — lux.id and zoolabs.id minting tokens claiming iss=hanzo.id, which their own relying parties reject. Config could not reach it: the brand map was in the child's environment and the Host never arrived. The dial address stays the fallback for a request carrying no Host of its own (the MCP hop builds a fresh one), because HTTP/1.1 requires the header. Read through req.Header.Host() rather than req.Host(): the latter goes through the URI, which parses on first touch — 34 allocations on a hop that is meant to cost none, which the alloc gate catches. And a request that already has a Host is now left ALONE rather than re-set. The test measures the SERIALIZED request. fasthttp writes the Host header from the URI once the URI has been parsed, so reading either field in memory can disagree with what actually goes over the wire; serializing is the only reading that cannot. It fails on the old code and passes on this one. Also: every test that binds a unix socket now does it in a short directory. A socket address is 104 bytes on Darwin and t.TempDir spends most of them on the test's own name, so 9 tests failed with "never began listening" — which reads as a timing bug, is green on Linux, red on every Mac, and breaks when a test is RENAMED. Suite goes 11 red to 2, both of which are pre-existing and platform: SO_PEERCRED (no LOCAL_PEERCRED path on macOS) and the alloc gate, which fails on pristine v1.27.1 when run in isolation. Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+2
-2
@@ -19,7 +19,7 @@ type askOut struct {
|
||||
// and a value. No address, no registry, no discovery — and the tenant rides the
|
||||
// CALLER, so the callee reads an org the caller could not choose.
|
||||
func TestAskReachesAPeerByNameAndCarriesTheCaller(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := sockDir(t)
|
||||
t.Setenv(zip.RuntimeDirEnv, dir)
|
||||
|
||||
peer := zip.New(zip.Config{AppName: "ledger", DisableStartupMessage: true})
|
||||
@@ -58,7 +58,7 @@ func TestAskReachesAPeerByNameAndCarriesTheCaller(t *testing.T) {
|
||||
// The Conn is kept for the life of the process: a plane hop must not pay for a
|
||||
// fresh connect, which is what "dial, call, close" per call costs.
|
||||
func TestAskReusesOneConnPerPeer(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := sockDir(t)
|
||||
t.Setenv(zip.RuntimeDirEnv, dir)
|
||||
|
||||
peer := zip.New(zip.Config{AppName: "reuse", DisableStartupMessage: true})
|
||||
|
||||
+3
-3
@@ -72,7 +72,7 @@ func flagsApp(t *testing.T) *zip.App {
|
||||
// there is no HTTP listener in this process for a call to fall back to.
|
||||
func serveUDS(t *testing.T, app *zip.App) string {
|
||||
t.Helper()
|
||||
sock := filepath.Join(t.TempDir(), "flags.sock")
|
||||
sock := filepath.Join(sockDir(t), "flags.sock")
|
||||
go func() { _ = app.Listen(sock) }()
|
||||
t.Cleanup(func() { _ = app.Shutdown() })
|
||||
waitSock(t, sock)
|
||||
@@ -340,7 +340,7 @@ func TestCall_ForwardsIdentityWithoutMintingIt(t *testing.T) {
|
||||
// reaches it with zip.DialApp(name). If these two ever disagree, every call in
|
||||
// the fleet misses.
|
||||
func TestSocketPath_IsTheOneScheme(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := sockDir(t)
|
||||
t.Setenv(zip.RuntimeDirEnv, dir)
|
||||
|
||||
if got, want := zip.RuntimeDir(), dir; got != want {
|
||||
@@ -394,7 +394,7 @@ func TestRuntimeDir_ResolutionOrder(t *testing.T) {
|
||||
// TestCall_RejectsAnOpNameThatIsNotAnOp keeps a caller bug from becoming a
|
||||
// request to some other route.
|
||||
func TestCall_RejectsAnOpNameThatIsNotAnOp(t *testing.T) {
|
||||
c, err := zip.Dial(filepath.Join(t.TempDir(), "unused.sock"))
|
||||
c, err := zip.Dial(filepath.Join(sockDir(t), "unused.sock"))
|
||||
if err != nil {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ type addOut struct {
|
||||
// counter keyed on a goroutine id parsed out of runtime.Stack. The cure here is
|
||||
// not calling the router at all: [Here] enters one op.
|
||||
func TestHereRunsTheOpWithoutTheSocket(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := sockDir(t)
|
||||
t.Setenv(zip.RuntimeDirEnv, dir)
|
||||
|
||||
var ran atomic.Int64
|
||||
@@ -125,7 +125,7 @@ func TestHereRefusesAnUnknownOp(t *testing.T) {
|
||||
// caller would be handed an app on its way out while the socket beside it was
|
||||
// already refusing connections.
|
||||
func TestServingFollowsTheListener(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
dir := sockDir(t)
|
||||
t.Setenv(zip.RuntimeDirEnv, dir)
|
||||
|
||||
app := zip.New(zip.Config{AppName: "meter"})
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package zip
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// dialRecorder is a Client that records what it was handed and answers 200. It
|
||||
// stands in for a plugin child, which is what makes the assertion meaningful:
|
||||
// the child's view of the request is the whole question here.
|
||||
// It records the host the child would actually READ, which is the host on the
|
||||
// SERIALIZED request — not a field of the in-memory object. fasthttp writes the
|
||||
// Host header from the URI whenever the URI has been parsed, so reading either
|
||||
// field directly can disagree with what goes over the wire. Serializing is the
|
||||
// only reading that cannot.
|
||||
type dialRecorder struct{ sawHost string }
|
||||
|
||||
func (d *dialRecorder) Do(req *fasthttp.Request, resp *fasthttp.Response) error {
|
||||
var buf bytes.Buffer
|
||||
w := bufio.NewWriter(&buf)
|
||||
_ = req.Write(w)
|
||||
_ = w.Flush()
|
||||
for _, line := range strings.Split(buf.String(), "\r\n") {
|
||||
if v, ok := strings.CutPrefix(line, "Host: "); ok {
|
||||
d.sawHost = v
|
||||
break
|
||||
}
|
||||
}
|
||||
resp.SetStatusCode(200)
|
||||
return nil
|
||||
}
|
||||
|
||||
// A PROXIED REQUEST REACHES THE CHILD AS THE HOST IT WAS ASKED AS.
|
||||
//
|
||||
// The child dials its own address, so this header is not routing — it is the
|
||||
// only way a child can know which brand was asked, and a multi-brand subsystem
|
||||
// cannot answer without it. hanzoai/iam resolves each brand's OIDC issuer from
|
||||
// exactly this value: overwritten with the socket path, every brand collapsed
|
||||
// onto one issuer and every non-default brand's tokens were rejected by its own
|
||||
// relying parties.
|
||||
func TestForwardKeepsTheCallersHost(t *testing.T) {
|
||||
rec := &dialRecorder{}
|
||||
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.Header.SetHost("lux.id")
|
||||
req.Header.SetRequestURI("/.well-known/openid-configuration")
|
||||
|
||||
if err := forward(req, resp, rec, "/var/run/iam.sock", "", "mount /v1/iam"); err != nil {
|
||||
t.Fatalf("forward: %v", err)
|
||||
}
|
||||
if rec.sawHost != "lux.id" {
|
||||
t.Fatalf("child saw Host %q, want %q — a brand-resolving child cannot answer as the brand it was asked as", rec.sawHost, "lux.id")
|
||||
}
|
||||
}
|
||||
|
||||
// A request with no Host of its own still gets one, because HTTP/1.1 requires
|
||||
// the header. That is the MCP hop: a freshly built request rather than a
|
||||
// proxied inbound, and the dial address is the honest answer there.
|
||||
func TestForwardFallsBackToTheDialAddress(t *testing.T) {
|
||||
rec := &dialRecorder{}
|
||||
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.Header.SetRequestURI("/mcp")
|
||||
|
||||
if err := forward(req, resp, rec, "plugin.sock", "", "mcp iam"); err != nil {
|
||||
t.Fatalf("forward: %v", err)
|
||||
}
|
||||
if rec.sawHost != "plugin.sock" {
|
||||
t.Fatalf("a hostless request reached the child as %q, want the dial address", rec.sawHost)
|
||||
}
|
||||
}
|
||||
|
||||
// The path rewrite is independent of the host, and both still apply together.
|
||||
func TestForwardStillRewritesThePath(t *testing.T) {
|
||||
rec := &dialRecorder{}
|
||||
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
|
||||
req.Header.SetHost("hanzo.ai")
|
||||
req.Header.SetRequestURI("/v1/iam/mcp")
|
||||
|
||||
if err := forward(req, resp, rec, "iam.sock", "/mcp", "mcp iam"); err != nil {
|
||||
t.Fatalf("forward: %v", err)
|
||||
}
|
||||
if got := string(req.URI().Path()); got != "/mcp" {
|
||||
t.Fatalf("path = %q, want /mcp", got)
|
||||
}
|
||||
if rec.sawHost != "hanzo.ai" {
|
||||
t.Fatalf("host = %q, want hanzo.ai", rec.sawHost)
|
||||
}
|
||||
}
|
||||
|
||||
// A dead target is 503 and names itself, unchanged by any of the above.
|
||||
func TestForwardWithNoClientIs503(t *testing.T) {
|
||||
req, resp := fasthttp.AcquireRequest(), fasthttp.AcquireResponse()
|
||||
defer fasthttp.ReleaseRequest(req)
|
||||
defer fasthttp.ReleaseResponse(resp)
|
||||
err := forward(req, resp, nil, "iam.sock", "", "mount /v1/iam")
|
||||
if err == nil || !strings.Contains(err.Error(), "no instance running") {
|
||||
t.Fatalf("err = %v, want a 503 naming the mount", err)
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -11,7 +11,7 @@ import (
|
||||
// there is no second path to a running program — only a terminal form that
|
||||
// hands back no handle, and says so.
|
||||
func TestListenIsServePlusWait(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "a.sock")
|
||||
sock := filepath.Join(sockDir(t), "a.sock")
|
||||
app := quiet("svc")
|
||||
app.Get("/x", func(c *Ctx) error { return c.String(200, "x") })
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestListenIsServePlusWait(t *testing.T) {
|
||||
// Serve hands back the handle Listen withholds, and the handle is what makes a
|
||||
// running program changeable.
|
||||
func TestServeYieldsAHandleListenDoesNot(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "b.sock")
|
||||
sock := filepath.Join(sockDir(t), "b.sock")
|
||||
app := quiet("svc")
|
||||
app.Get("/x", func(c *Ctx) error { return c.String(200, "x") })
|
||||
|
||||
|
||||
+11
-11
@@ -23,7 +23,7 @@ import (
|
||||
// so the benchmarks build their subject the same way the tests do.
|
||||
func buildPlugin(t testing.TB, version string) []byte {
|
||||
t.Helper()
|
||||
out := filepath.Join(t.TempDir(), "testplugin-"+version)
|
||||
out := filepath.Join(sockDir(t), "testplugin-"+version)
|
||||
cmd := exec.Command("go", "build",
|
||||
"-ldflags", "-X main.version="+version,
|
||||
"-o", out, "./internal/testplugin")
|
||||
@@ -154,7 +154,7 @@ func TestLoad_AlreadyRunning(t *testing.T) {
|
||||
plugin.Get("/v1/demo/version", func(c *zip.Ctx) error {
|
||||
return c.JSON(200, map[string]string{"version": "external"})
|
||||
})
|
||||
sock := filepath.Join(t.TempDir(), "demo.sock")
|
||||
sock := filepath.Join(sockDir(t), "demo.sock")
|
||||
go func() { _ = plugin.Listen(sock) }()
|
||||
defer func() { _ = plugin.Shutdown() }()
|
||||
waitSocket(t, sock)
|
||||
@@ -180,7 +180,7 @@ func TestLoad_FromRelease(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cache := t.TempDir()
|
||||
cache := sockDir(t)
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(must(zip.Load(zip.Plugin{
|
||||
Name: "demo", URL: srv.URL + "/demo", Sum: hex.EncodeToString(sum[:]), Dir: cache,
|
||||
@@ -215,7 +215,7 @@ func TestLoad_ReleaseRejectsBadSum(t *testing.T) {
|
||||
// The verification is part of BUILDING the definition, so the refusal comes
|
||||
// from Load itself — there is nothing to compose and nothing to build.
|
||||
_, err := zip.Load(zip.Plugin{
|
||||
Name: "demo", URL: srv.URL + "/demo", Dir: t.TempDir(),
|
||||
Name: "demo", URL: srv.URL + "/demo", Dir: sockDir(t),
|
||||
Sum: strings.Repeat("00", 32), // wrong on purpose
|
||||
}, "/v1/demo")
|
||||
if err == nil {
|
||||
@@ -246,7 +246,7 @@ func TestPlugins_Status(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
cache := t.TempDir()
|
||||
cache := sockDir(t)
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(
|
||||
must(zip.Load(zip.Plugin{Name: "embedded", Bin: v1, Dir: cache}, "/v1/embedded")),
|
||||
@@ -317,7 +317,7 @@ func TestLoad_ChildDiesWithHost(t *testing.T) {
|
||||
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(must(zip.Load(zip.Plugin{
|
||||
Name: "demo", Bin: bin, Dir: t.TempDir(),
|
||||
Name: "demo", Bin: bin, Dir: sockDir(t),
|
||||
}, "/v1/demo")))
|
||||
|
||||
pid := app.Plugins()[0].PID
|
||||
@@ -354,7 +354,7 @@ func TestLoad_MultiplePrefixes(t *testing.T) {
|
||||
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(must(zip.Load(
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir()},
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: sockDir(t)},
|
||||
"/v1/demo", "/v1/other",
|
||||
)))
|
||||
defer func() { _ = app.Shutdown() }()
|
||||
@@ -428,7 +428,7 @@ func TestPlugins_Usage(t *testing.T) {
|
||||
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(must(zip.Load(
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir()}, "/v1/demo",
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: sockDir(t)}, "/v1/demo",
|
||||
)))
|
||||
defer func() { _ = app.Shutdown() }()
|
||||
|
||||
@@ -471,7 +471,7 @@ func TestPlugins_SurvivesPanic(t *testing.T) {
|
||||
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(must(zip.Load(
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir()}, "/v1/demo",
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: sockDir(t)}, "/v1/demo",
|
||||
)))
|
||||
defer func() { _ = app.Shutdown() }()
|
||||
|
||||
@@ -522,7 +522,7 @@ func TestLoad_Lazy(t *testing.T) {
|
||||
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(must(zip.Load(
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: t.TempDir(), Lazy: true}, "/v1/demo",
|
||||
zip.Plugin{Name: "demo", Bin: bin, Dir: sockDir(t), Lazy: true}, "/v1/demo",
|
||||
)))
|
||||
defer func() { _ = app.Shutdown() }()
|
||||
|
||||
@@ -571,7 +571,7 @@ func TestReload_PinAndRollbackByDigest(t *testing.T) {
|
||||
_, _ = w.Write(bits[r.URL.Path])
|
||||
}))
|
||||
|
||||
cache := t.TempDir()
|
||||
cache := sockDir(t)
|
||||
app := zip.New(zip.Config{AppName: "host", DisableStartupMessage: true})
|
||||
app.Use(must(zip.Load(zip.Plugin{
|
||||
Name: "demo", URL: srv.URL + "/v1", Sum: d1, Dir: cache,
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ func mcpApp(t *testing.T) *App {
|
||||
// port.
|
||||
func zapDoor(t *testing.T, app *App) *zapmcp.Transport {
|
||||
t.Helper()
|
||||
sock := filepath.Join(t.TempDir(), "mcp.sock")
|
||||
sock := filepath.Join(sockDir(t), "mcp.sock")
|
||||
ln, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
@@ -173,7 +173,7 @@ func TestMCP_ZapCallerComesFromTheContext(t *testing.T) {
|
||||
}{Org: CallerOf(ctx).Org}, nil
|
||||
}, WithOperationID("whoami"))
|
||||
|
||||
sock := filepath.Join(t.TempDir(), "mcp.sock")
|
||||
sock := filepath.Join(sockDir(t), "mcp.sock")
|
||||
ln, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
|
||||
+4
-4
@@ -15,7 +15,7 @@ import (
|
||||
// public port, and a probe that queues behind public traffic is not a probe.
|
||||
func TestOpsSurfaceIsASecondListener(t *testing.T) {
|
||||
port := freePort(t)
|
||||
t.Setenv(zip.RuntimeDirEnv, t.TempDir())
|
||||
t.Setenv(zip.RuntimeDirEnv, sockDir(t))
|
||||
|
||||
app := zip.New(zip.Config{
|
||||
AppName: "opsy",
|
||||
@@ -71,7 +71,7 @@ func TestOpsListenerSpeaksHTTP(t *testing.T) {
|
||||
t.Fatalf("DefaultOpsAddr = %q — a kubelet cannot probe that", zip.DefaultOpsAddr)
|
||||
}
|
||||
port := freePort(t)
|
||||
t.Setenv(zip.RuntimeDirEnv, t.TempDir())
|
||||
t.Setenv(zip.RuntimeDirEnv, sockDir(t))
|
||||
|
||||
app := zip.New(zip.Config{
|
||||
AppName: "probed",
|
||||
@@ -97,7 +97,7 @@ func TestOpsListenerSpeaksHTTP(t *testing.T) {
|
||||
// two apps in it while a test process has many, so the second died on "address
|
||||
// already in use". A host names none for a child, so a child owns none.
|
||||
func TestEmptyOpsAddrBindsNothing(t *testing.T) {
|
||||
t.Setenv(zip.RuntimeDirEnv, t.TempDir())
|
||||
t.Setenv(zip.RuntimeDirEnv, sockDir(t))
|
||||
// Free, and nothing in this test will bind it — so an answer here is zip
|
||||
// having reached for an address nobody gave it.
|
||||
unclaimed := freePort(t)
|
||||
@@ -120,7 +120,7 @@ func TestEmptyOpsAddrBindsNothing(t *testing.T) {
|
||||
// Two apps in one process — the edge app and its peer sibling — must not race
|
||||
// for one ops port. Only the primary brings the surface up.
|
||||
func TestOnlyThePrimaryAppOwnsTheOpsListener(t *testing.T) {
|
||||
t.Setenv(zip.RuntimeDirEnv, t.TempDir())
|
||||
t.Setenv(zip.RuntimeDirEnv, sockDir(t))
|
||||
|
||||
app := zip.New(zip.Config{
|
||||
AppName: "twoapps",
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ func remoteService(t *testing.T) (string, Declaration) {
|
||||
}, WithOperationID("getEntry"))
|
||||
svc.Get("/v1/ledger/raw", func(c *Ctx) error { return c.String(200, "raw ledger") })
|
||||
|
||||
sock := filepath.Join(t.TempDir(), "ledger.sock")
|
||||
sock := filepath.Join(sockDir(t), "ledger.sock")
|
||||
go func() { _ = svc.Listen(sock) }()
|
||||
t.Cleanup(func() { _ = svc.Shutdown() })
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// no port to allocate, filesystem permissions as the ACL — carrying byte-identical
|
||||
// ZAP frames to the tcp case.
|
||||
func TestProxy_OverUnixSocket(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "billing.sock")
|
||||
sock := filepath.Join(sockDir(t), "billing.sock")
|
||||
|
||||
plugin := zip.New(zip.Config{AppName: "billing", DisableStartupMessage: true})
|
||||
plugin.Get("/v1/billing/invoices", func(c *zip.Ctx) error {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package zip_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// sockDir is the external-test-package twin of the helper in sockdir_test.go.
|
||||
// Two test packages compile separately, so the helper exists in each; the doc
|
||||
// comment lives once, beside the internal copy.
|
||||
func sockDir(t testing.TB) string {
|
||||
t.Helper()
|
||||
d, err := os.MkdirTemp("", "zt")
|
||||
if err != nil {
|
||||
t.Fatalf("socket dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(d) })
|
||||
return d
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package zip
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// sockDir is a directory a unix socket can bind in.
|
||||
//
|
||||
// t.TempDir cannot be used for one: a socket address is 104 bytes on Darwin
|
||||
// (108 on Linux) and t.TempDir spends most of them on the TEST'S OWN NAME, so a
|
||||
// test with a descriptive name fails to bind with EINVAL — reported as "never
|
||||
// began listening", which reads like a timing bug and is not. It is green on
|
||||
// Linux, red on every Mac, and a test that passes today breaks when a test is
|
||||
// RENAMED.
|
||||
func sockDir(t testing.TB) string {
|
||||
t.Helper()
|
||||
d, err := os.MkdirTemp("", "zt")
|
||||
if err != nil {
|
||||
t.Fatalf("socket dir: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.RemoveAll(d) })
|
||||
return d
|
||||
}
|
||||
+2
-2
@@ -8,9 +8,9 @@ import (
|
||||
"github.com/zap-proto/zip"
|
||||
)
|
||||
|
||||
// shortDir is t.TempDir() with a SHORT name. A plugin's socket lives inside
|
||||
// shortDir is sockDir(t) with a SHORT name. A plugin's socket lives inside
|
||||
// Plugin.Dir, and a unix socket path is capped at 108 bytes by the kernel —
|
||||
// t.TempDir() spends most of that budget on the test's own name, so a
|
||||
// sockDir(t) spends most of that budget on the test's own name, so a
|
||||
// descriptively-named test silently fails to bind and reports the child as
|
||||
// "exited before listening".
|
||||
func shortDir(t *testing.T) string {
|
||||
|
||||
+27
-1
@@ -319,7 +319,33 @@ func forward(req *fasthttp.Request, resp *fasthttp.Response, client Client, host
|
||||
if client == nil {
|
||||
return Errorf(503, "%s: no instance running", what)
|
||||
}
|
||||
req.SetHost(host)
|
||||
// THE CALLER'S HOST SURVIVES THE HOP. A client dials its own address
|
||||
// (HostClient.Addr, zaphttp.Dial(addr)) and never reads this header to find
|
||||
// the far end, so Host here is not routing — it is the question "who was
|
||||
// asked", and the child needs it to answer as that brand.
|
||||
//
|
||||
// It used to be overwritten with the dial address unconditionally, which
|
||||
// made every multi-brand behaviour collapse behind a plugin. Identity is
|
||||
// where that showed: hanzoai/iam serves every brand from one instance and
|
||||
// resolves each brand's OIDC issuer from the Host, so a child asked as
|
||||
// "/var/lib/cloud/run/iam.sock" matched no brand and fell back to its single
|
||||
// default — lux.id and zoolabs.id minting tokens that claim iss=hanzo.id,
|
||||
// which their own relying parties reject. Config could not fix it: the brand
|
||||
// map was in the child's environment, and the Host never arrived.
|
||||
//
|
||||
// The dial address remains the fallback for a request that carries no Host
|
||||
// of its own — a freshly built one (the MCP hop) rather than a proxied
|
||||
// inbound — because HTTP/1.1 requires the header to be present.
|
||||
// req.Header.Host(), not req.Host(): the latter reads through the URI, which
|
||||
// parses it on first touch — 34 allocations a request where this hop is
|
||||
// meant to cost none, and the alloc gate catches exactly that. The header is
|
||||
// the raw inbound bytes, and it is also the value that MEANS "who was asked".
|
||||
//
|
||||
// A request that already carries one is left ALONE rather than re-set: the
|
||||
// cheapest correct move here is no move at all.
|
||||
if len(req.Header.Host()) == 0 {
|
||||
req.SetHost(host)
|
||||
}
|
||||
if path != "" {
|
||||
req.URI().SetPath(path)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user