Skip to content

Commit ec21319

Browse files
committed
feat(agent): use multistream and yamux muxer instead of HTTP server
This change replaces the legacy per-connection HTTP/revdial reverse-tunnel flow with a multiplexed, per-stream protocol (v2) based on HashiCorp's yamux and multistream-select. Yamux sessions allow a single agent websocket to host many logical streams without repeating the HTTP reverse-listen handshake for each session. Multistream-select provides explicit per-stream protocol negotiation, making each logical stream's intent (SSH open/close, HTTP proxy) explicit and versionable. The new approach reduces handshake overhead and makes future protocol extensions easier and safer. Over time the legacy reverse-tunnel implementation (per-request HTTP/revdial) has become a maintenance burden and a performance limiter: each logical session required a full HTTP handshake and a separate reverse listener, which increased latency and duplicated logic across server components. This change replaces that model with a single multiplexed transport per agent using HashiCorp's yamux, and introduces explicit per-stream protocol negotiation via multistream-select. Practically, when an agent connects we now bind its websocket to a yamux session (Manager.Bind) and keep that session alive with a light ping loop. Individual logical operations—opening an SSH session, closing it, or proxying HTTP—are created as yamux streams. Each stream negotiates its intent using multistream identifiers (`/ssh/open/1.0.0`, `/ssh/close/1.0.0`, `/http/proxy/1.0.0`) and then exchanges a small JSON envelope for any parameters. The dialer package centralizes version handling and prepares connections for callers via DialTo(ctx, tenant, uid, target), while Target implementations encapsulate the version-specific bootstrap (legacy HTTP v1 vs yamux+multistream v2). We kept backward compatibility in mind: the Manager still recognizes existing revdial-based connections and will return a v1 connection when appropriate. That lets the system run both v1 and v2 agents concurrently and supports staged rollouts. I also removed the duplicated tunnel wiring and replaced it with a focused HTTP sidecar (http) that exposes both the legacy endpoints and the new v2 bind endpoint; server and session code now call into dialer.Dialer and Target helpers rather than performing raw HTTP handshakes.
1 parent 62ae183 commit ec21319

File tree

28 files changed

+1449
-817
lines changed

28 files changed

+1449
-817
lines changed

agent/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@ require (
4040
github.com/gogo/protobuf v1.3.2 // indirect
4141
github.com/google/uuid v1.6.0 // indirect
4242
github.com/gorilla/websocket v1.5.0 // indirect
43+
github.com/hashicorp/yamux v0.1.2 // indirect
4344
github.com/inconshreveable/mousetrap v1.1.0 // indirect
4445
github.com/kr/fs v0.1.0 // indirect
4546
github.com/labstack/echo/v4 v4.10.2 // indirect
4647
github.com/leodido/go-urn v1.2.2 // indirect
4748
github.com/mattn/go-shellwords v1.0.12 // indirect
4849
github.com/moby/docker-image-spec v1.3.1 // indirect
50+
github.com/multiformats/go-multistream v0.6.1 // indirect
51+
github.com/multiformats/go-varint v0.0.6 // indirect
4952
github.com/opencontainers/go-digest v1.0.0 // indirect
5053
github.com/opencontainers/image-spec v1.1.0 // indirect
5154
github.com/openwall/yescrypt-go v1.0.0 // indirect

agent/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
5353
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5454
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
5555
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
56+
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
57+
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
5658
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
5759
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
5860
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
@@ -82,6 +84,10 @@ github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
8284
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
8385
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
8486
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
87+
github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ=
88+
github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw=
89+
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
90+
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
8591
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
8692
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
8793
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=

gateway/nginx/conf.d/shellhub.conf

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,29 @@ server {
368368
proxy_redirect off;
369369
}
370370

371+
location /connection {
372+
set $upstream ssh:8080;
373+
374+
auth_request /auth;
375+
auth_request_set $tenant_id $upstream_http_x_tenant_id;
376+
auth_request_set $device_uid $upstream_http_x_device_uid;
377+
proxy_pass http://$upstream;
378+
proxy_set_header Connection $connection_upgrade;
379+
proxy_set_header Host $host;
380+
proxy_set_header Upgrade $http_upgrade;
381+
{{ if $cfg.EnableProxyProtocol -}}
382+
proxy_set_header X-Real-IP $proxy_protocol_addr;
383+
{{ else -}}
384+
proxy_set_header X-Real-IP $x_real_ip;
385+
{{ end -}}
386+
proxy_set_header X-Device-UID $device_uid;
387+
proxy_set_header X-Tenant-ID $tenant_id;
388+
proxy_set_header X-Request-ID $request_id;
389+
proxy_http_version 1.1;
390+
proxy_cache_bypass $http_upgrade;
391+
proxy_redirect off;
392+
}
393+
371394
location /ssh/auth {
372395
set $upstream api:8080;
373396

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ require (
1717
github.com/golang-jwt/jwt/v4 v4.5.2
1818
github.com/google/uuid v1.6.0
1919
github.com/gorilla/websocket v1.5.0
20+
github.com/hashicorp/yamux v0.1.2
2021
github.com/hibiken/asynq v0.24.1
2122
github.com/jarcoal/httpmock v1.3.1
2223
github.com/labstack/echo/v4 v4.10.2
2324
github.com/mattn/go-shellwords v1.0.12
2425
github.com/mholt/archiver/v4 v4.0.0-alpha.8
26+
github.com/multiformats/go-multistream v0.6.1
2527
github.com/openwall/yescrypt-go v1.0.0
2628
github.com/oschwald/geoip2-golang v1.8.0
2729
github.com/pkg/errors v0.9.1
@@ -85,6 +87,7 @@ require (
8587
github.com/moby/sys/userns v0.1.0 // indirect
8688
github.com/moby/term v0.5.0 // indirect
8789
github.com/morikuni/aec v1.0.0 // indirect
90+
github.com/multiformats/go-varint v0.0.6 // indirect
8891
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 // indirect
8992
github.com/opencontainers/go-digest v1.0.0 // indirect
9093
github.com/opencontainers/image-spec v1.1.0 // indirect

0 commit comments

Comments
 (0)