From bd1d1b76124b0c43c897627f5ef72e46bb1144f1 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Thu, 1 Oct 2026 00:41:41 +0000 Subject: [PATCH] Derive the serve peer's identity from its credential ServeHello no longer carries a PeerID: the serve side has no source for it and the credential already identifies the peer. The relay checks only the Hello's Resource against the Authority's ServePeer, which still returns the peer's identity. Updates the codec, validation, serve config, relay, tests, golden frames and the Link protocol document. --- docs/sandbox-link-protocol.md | 5 ++--- internal/sandboxlink/protocol.go | 11 +++++------ internal/sandboxlink/protocol_test.go | 2 +- internal/sandboxlink/relay/relay.go | 2 +- internal/sandboxlink/relay/relay_test.go | 5 ++--- internal/sandboxlink/serve.go | 3 +-- internal/sandboxlink/testdata/link_v1.hex | 5 ++--- 7 files changed, 14 insertions(+), 19 deletions(-) diff --git a/docs/sandbox-link-protocol.md b/docs/sandbox-link-protocol.md index 5a2a3c7b..71d59505 100644 --- a/docs/sandbox-link-protocol.md +++ b/docs/sandbox-link-protocol.md @@ -18,7 +18,7 @@ A stream ends in one of two ways, and the relay keeps them apart end to end. An The Sandbox I/O service runs `sandboxlink.Serve` with a `ServeConfig`: -- `URL`, `Credential` and `Resource` come from the [bootstrap input](sandbox-bootstrap.md). `PeerID` identifies the service. `ServerInstanceID` is a new ID whenever the service starts without its operation and handle registries. +- `URL`, `Credential` and `Resource` come from the [bootstrap input](sandbox-bootstrap.md). The credential identifies the service, so the Hello carries no peer ID. `ServerInstanceID` is a new ID whenever the service starts without its operation and handle registries. - `Services` holds one handler per offered service and version. A handler receives the `Bind`, which carries the authorized binding including a File stream's exports, and the stream. It owns the stream and returns when it is done with it. Its context ends when the attachment closes or `Serve` returns. - `Serve` reconnects with jittered exponential backoff, sending the same `ServerInstanceID`, whenever the link drops. It returns when its context ends or when the relay refuses the Hello with any failure other than `ServiceUnavailable` or `LimitExceeded`, for example `AuthenticationFailed` after the credential is withdrawn or `StaleGeneration` after a newer sandbox took over the resource. Before returning it cancels every handler's context and waits for the handlers. - `OnAttachmentLost` fires when an attachment's last open stream ends while the attachment is still open, such as when the link drops. `OnAttachmentRestored` fires when a stream binds a lost attachment again. `OnAttachmentClosed` fires with the reason when the relay reports `AttachmentClosed`. Losing a socket is not closing an attachment: the service keeps an attachment's state until it is closed. @@ -114,7 +114,6 @@ Hello Version u16 // 1 Role enum // RoleServe = 1, RoleAttach = 2 if RoleServe: - PeerID ID Credential bytes // 1..4096 bytes Resource ResourceRef ServerInstanceID ID @@ -204,7 +203,7 @@ Later control requests continue the Hello's request IDs. The relay ends an attac `HelloAccepted.MaxStreams` bounds the link's concurrent service streams. `MaxFrameBytes` bounds the payload of every frame on the link's service streams. -For a serve peer, the Authority returns the peer ID and the resource, including generation, that the credential serves. Both must equal the Hello's, otherwise the answer is `PermissionDenied`. The relay then applies the [generation rule](#authority-and-staleness) and makes the link the resource's current serve peer. +For a serve peer, the Authority returns the peer's identity and the resource, including generation, that the credential serves. The resource must equal the Hello's, otherwise the answer is `PermissionDenied`. The relay then applies the [generation rule](#authority-and-staleness) and makes the link the resource's current serve peer. The relay and the serve peer bound each handshake step, the WebSocket upgrade, the Hello, reading an `Open`, a `Bind` and its answer and each Authority call, by `sandboxlink.HandshakeTimeout` (10 seconds). The attach peer bounds an Open with its context. diff --git a/internal/sandboxlink/protocol.go b/internal/sandboxlink/protocol.go index d3f14fa5..abcae331 100644 --- a/internal/sandboxlink/protocol.go +++ b/internal/sandboxlink/protocol.go @@ -292,11 +292,11 @@ type ServiceVersion struct { Version uint16 } -// ServeHello introduces the Sandbox I/O service. ServerInstanceID changes -// whenever the service loses its operation or handle registry. +// ServeHello introduces the Sandbox I/O service. The credential identifies +// the peer, so the Hello carries no peer ID. ServerInstanceID changes whenever +// the service loses its operation or handle registry. type ServeHello struct { Version uint16 - PeerID sandboxwire.ID Credential []byte Resource ResourceRef ServerInstanceID sandboxwire.ID @@ -611,7 +611,7 @@ func decodeRequest(r *reader, op Op) Message { return nil } if Role(r.enum(func(v uint16) bool { return Role(v) == RoleServe || Role(v) == RoleAttach })) == RoleServe { - h := ServeHello{Version: Version, PeerID: r.id(), Credential: r.bytes(), Resource: r.resource(), ServerInstanceID: r.id()} + h := ServeHello{Version: Version, Credential: r.bytes(), Resource: r.resource(), ServerInstanceID: r.id()} n := r.count(uint32(ServiceNetwork)) for range n { h.Services = append(h.Services, ServiceVersion{Service: r.service(), Version: r.u16()}) @@ -672,7 +672,6 @@ func decodeSuccess(r *reader, op Op) Message { func (h ServeHello) encode(e *sandboxwire.Encoder) { e.U16(h.Version) e.Enum(uint16(RoleServe)) - e.ID(h.PeerID) e.Bytes(h.Credential) encodeResource(e, h.Resource) e.ID(h.ServerInstanceID) @@ -911,7 +910,7 @@ func (h ServeHello) validate() error { if err := h.Resource.validate(); err != nil { return err } - return checkIDs(h.PeerID, h.ServerInstanceID) + return checkIDs(h.ServerInstanceID) } func (h AttachHello) validate() error { diff --git a/internal/sandboxlink/protocol_test.go b/internal/sandboxlink/protocol_test.go index 271e56eb..f80b415d 100644 --- a/internal/sandboxlink/protocol_test.go +++ b/internal/sandboxlink/protocol_test.go @@ -40,7 +40,7 @@ type golden struct { // goldenFrames are the frames in testdata/link_v1.hex, in order. var goldenFrames = []golden{ - {1, ServeHello{Version: 1, PeerID: testID(0x04), Credential: []byte("serve"), Resource: testResource, ServerInstanceID: testID(0x05), + {1, ServeHello{Version: 1, Credential: []byte("serve"), Resource: testResource, ServerInstanceID: testID(0x05), Services: []ServiceVersion{{ServiceFile, 1}, {ServiceNetwork, 1}}}}, {1, AttachHello{Version: 1, RuntimeID: testID(0x06), Credential: []byte("runtime")}}, {1, HelloAccepted{LinkID: testID(0x0a), MaxStreams: 256, MaxFrameBytes: 1 << 20}}, diff --git a/internal/sandboxlink/relay/relay.go b/internal/sandboxlink/relay/relay.go index 2c678c20..ac8ff4aa 100644 --- a/internal/sandboxlink/relay/relay.go +++ b/internal/sandboxlink/relay/relay.go @@ -396,7 +396,7 @@ func (rl *Relay) admitServe(l *link, id uint64, hello sandboxlink.ServeHello) (* ctx, cancel := rl.authorityContext() peer, err := rl.cfg.Authority.AuthenticateServe(ctx, hello) cancel() - if err == nil && peer != (sandboxlink.ServePeer{PeerID: hello.PeerID, Resource: hello.Resource}) { + if err == nil && peer.Resource != hello.Resource { err = sandboxlink.Fail(sandboxlink.PermissionDenied) } if err != nil { diff --git a/internal/sandboxlink/relay/relay_test.go b/internal/sandboxlink/relay/relay_test.go index 0f24f00e..2972697d 100644 --- a/internal/sandboxlink/relay/relay_test.go +++ b/internal/sandboxlink/relay/relay_test.go @@ -143,8 +143,7 @@ func (f *fixture) serve(generation uint64) *servePeer { func (f *fixture) startServe(generation uint64) *servePeer { credential := []byte(fmt.Sprintf("serve credential %d", generation)) - peerID := sandboxwire.NewID() - f.auth.AddServe(credential, sandboxlink.ServePeer{PeerID: peerID, Resource: resource(generation)}) + f.auth.AddServe(credential, sandboxlink.ServePeer{PeerID: sandboxwire.NewID(), Resource: resource(generation)}) p := &servePeer{instance: sandboxwire.NewID(), connected: make(chan sandboxlink.HelloAccepted, 16), conns: make(chan net.Conn, 16), lost: make(chan sandboxwire.ID, 16), restored: make(chan sandboxwire.ID, 16), closed: make(chan sandboxlink.CloseReason, 128), binds: make(chan sandboxlink.Bind, 16), echoed: make(chan error, 16), done: make(chan struct{})} @@ -168,7 +167,7 @@ func (f *fixture) startServe(generation uint64) *servePeer { } return c, err }, - PeerID: peerID, Credential: credential, Resource: resource(generation), ServerInstanceID: p.instance, + Credential: credential, Resource: resource(generation), ServerInstanceID: p.instance, Services: []sandboxlink.ServiceHandler{ {Service: sandboxlink.ServiceFile, Version: 1, Serve: echo}, {Service: sandboxlink.ServiceProcess, Version: 1, Serve: resetAfterOne}, diff --git a/internal/sandboxlink/serve.go b/internal/sandboxlink/serve.go index 33f3634d..65df23f2 100644 --- a/internal/sandboxlink/serve.go +++ b/internal/sandboxlink/serve.go @@ -28,7 +28,6 @@ type ServeConfig struct { URL string TLS *tls.Config Dial Dialer - PeerID sandboxwire.ID Credential []byte Resource ResourceRef ServerInstanceID sandboxwire.ID @@ -59,7 +58,7 @@ type ServeConfig struct { // LimitExceeded; it then returns that *Error. Before returning it cancels // every handler's context and waits for the handlers. func Serve(parent context.Context, cfg ServeConfig) error { - hello := ServeHello{Version: Version, PeerID: cfg.PeerID, Credential: cfg.Credential, Resource: cfg.Resource, ServerInstanceID: cfg.ServerInstanceID} + hello := ServeHello{Version: Version, Credential: cfg.Credential, Resource: cfg.Resource, ServerInstanceID: cfg.ServerInstanceID} for _, h := range cfg.Services { if h.Serve == nil { return errors.New("sandbox link: service handler without Serve") diff --git a/internal/sandboxlink/testdata/link_v1.hex b/internal/sandboxlink/testdata/link_v1.hex index 75aabf27..537cbc79 100644 --- a/internal/sandboxlink/testdata/link_v1.hex +++ b/internal/sandboxlink/testdata/link_v1.hex @@ -1,11 +1,10 @@ # Link version 1 frames from goldenFrames in protocol_test.go, in order. Hex bytes; text after # is a comment. -# IDs repeat one byte: 01 tenant, 02 environment, 03 resource, 04 peer, 05 server instance, 06 Runtime, 07 attachment, 08 Session, 09 assignment, 0a link. +# IDs repeat one byte: 01 tenant, 02 environment, 03 resource, 05 server instance, 06 Runtime, 07 attachment, 08 Session, 09 assignment, 0a link. # ServeHello -00000073 0001 0000 0000000000000001 # header: length 115, OpHello, flags, request 1 +00000063 0001 0000 0000000000000001 # header: length 99, OpHello, flags, request 1 0001 # Version 1 0001 # Role RoleServe -04040404040404040404040404040404 # PeerID 00000005 7365727665 # Credential "serve" 01010101010101010101010101010101 # Resource.TenantID 02020202020202020202020202020202 # Resource.EnvironmentID