test(app): cover remote environment proxy paths
- commit
- 811c2e4d92a3ac583e5a4326c7fde3caa9bb9164
- author
- brice · · 2026-07-15 06:05 UTC
- parent
- 0812bb2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +122 -3
M internal/app/run.go +20 -3
| @@ -794,7 +794,7 @@ func runStdio(ctx context.Context) (retErr error) { | |||
| 794 | 794 | } | |
| 795 | 795 | defer func() { _ = transport.Close() }() | |
| 796 | 796 | stdio := sshstdio.NewTransport(os.Stdin, os.Stdout, nil, sshstdio.WithRuntimeObserver(observer)) | |
| 797 | - | return proxyTransports(ctx, newFirstHelloTransport(stdio, "", os.Environ()), transport, log) | |
| 797 | + | return runStdioProxy(ctx, stdio, transport, os.Environ(), log) | |
| 798 | 798 | } | |
| 799 | 799 | ||
| 800 | 800 | var ( | |
| @@ -935,8 +935,7 @@ func runUDPProxy(ctx context.Context, session string, ready io.Writer) (retErr e | |||
| 935 | 935 | return err | |
| 936 | 936 | } | |
| 937 | 937 | ||
| 938 | - | dgramTr := newFirstHelloTransport(dg, session, os.Environ()) | |
| 939 | - | return dgram.ProxyRuntime{Client: dgramTr, Daemon: daemonTr, Log: log, IdleTTL: udpProxyIdleTTL}.Run(ctx) | |
| 938 | + | return runUDPProxyRuntime(ctx, session, dg, daemonTr, os.Environ(), log) | |
| 940 | 939 | } | |
| 941 | 940 | ||
| 942 | 941 | const udpProxyIdleTTL = 15 * time.Minute | |
| @@ -1071,6 +1070,24 @@ func (t *firstHelloTransport) Recv() (ports.Frame, error) { | |||
| 1071 | 1070 | return f, nil | |
| 1072 | 1071 | } | |
| 1073 | 1072 | ||
| 1073 | + | // runStdioProxy applies the remote host environment before the stdio stream | |
| 1074 | + | // reaches the daemon. Its explicit environment input keeps this host boundary | |
| 1075 | + | // independently testable. | |
| 1076 | + | func runStdioProxy(ctx context.Context, client, daemon ports.Transport, env []string, log *slog.Logger) error { | |
| 1077 | + | return proxyTransports(ctx, newFirstHelloTransport(client, "", env), daemon, log) | |
| 1078 | + | } | |
| 1079 | + | ||
| 1080 | + | // runUDPProxyRuntime applies the remote host environment and selected session | |
| 1081 | + | // before the datagram stream reaches the daemon. | |
| 1082 | + | func runUDPProxyRuntime(ctx context.Context, session string, client, daemon ports.Transport, env []string, log *slog.Logger) error { | |
| 1083 | + | return dgram.ProxyRuntime{ | |
| 1084 | + | Client: newFirstHelloTransport(client, session, env), | |
| 1085 | + | Daemon: daemon, | |
| 1086 | + | Log: log, | |
| 1087 | + | IdleTTL: udpProxyIdleTTL, | |
| 1088 | + | }.Run(ctx) | |
| 1089 | + | } | |
| 1090 | + | ||
| 1074 | 1091 | func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger) error { | |
| 1075 | 1092 | return dgram.ProxyRuntime{Client: a, Daemon: b, Log: log}.Run(ctx) | |
| 1076 | 1093 | } | |
| @@ -794,7 +794,7 @@ func runStdio(ctx context.Context) (retErr error) { | |||||
| 794 | } | 794 | } | ||
| 795 | defer func() { _ = transport.Close() }() | 795 | defer func() { _ = transport.Close() }() | ||
| 796 | stdio := sshstdio.NewTransport(os.Stdin, os.Stdout, nil, sshstdio.WithRuntimeObserver(observer)) | 796 | stdio := sshstdio.NewTransport(os.Stdin, os.Stdout, nil, sshstdio.WithRuntimeObserver(observer)) | ||
| 797 | - | return proxyTransports(ctx, newFirstHelloTransport(stdio, "", os.Environ()), transport, log) | 797 | + | return runStdioProxy(ctx, stdio, transport, os.Environ(), log) |
| 798 | } | 798 | } | ||
| 799 | 799 | ||||
| 800 | var ( | 800 | var ( | ||
| @@ -935,8 +935,7 @@ func runUDPProxy(ctx context.Context, session string, ready io.Writer) (retErr e | |||||
| 935 | return err | 935 | return err | ||
| 936 | } | 936 | } | ||
| 937 | 937 | ||||
| 938 | - | dgramTr := newFirstHelloTransport(dg, session, os.Environ()) | 938 | + | return runUDPProxyRuntime(ctx, session, dg, daemonTr, os.Environ(), log) |
| 939 | - | return dgram.ProxyRuntime{Client: dgramTr, Daemon: daemonTr, Log: log, IdleTTL: udpProxyIdleTTL}.Run(ctx) | |||
| 940 | } | 939 | } | ||
| 941 | 940 | ||||
| 942 | const udpProxyIdleTTL = 15 * time.Minute | 941 | const udpProxyIdleTTL = 15 * time.Minute | ||
| @@ -1071,6 +1070,24 @@ func (t *firstHelloTransport) Recv() (ports.Frame, error) { | |||||
| 1071 | return f, nil | 1070 | return f, nil | ||
| 1072 | } | 1071 | } | ||
| 1073 | 1072 | ||||
| 1073 | + | // runStdioProxy applies the remote host environment before the stdio stream | |||
| 1074 | + | // reaches the daemon. Its explicit environment input keeps this host boundary | |||
| 1075 | + | // independently testable. | |||
| 1076 | + | func runStdioProxy(ctx context.Context, client, daemon ports.Transport, env []string, log *slog.Logger) error { | |||
| 1077 | + | return proxyTransports(ctx, newFirstHelloTransport(client, "", env), daemon, log) | |||
| 1078 | + | } | |||
| 1079 | + | ||||
| 1080 | + | // runUDPProxyRuntime applies the remote host environment and selected session | |||
| 1081 | + | // before the datagram stream reaches the daemon. | |||
| 1082 | + | func runUDPProxyRuntime(ctx context.Context, session string, client, daemon ports.Transport, env []string, log *slog.Logger) error { | |||
| 1083 | + | return dgram.ProxyRuntime{ | |||
| 1084 | + | Client: newFirstHelloTransport(client, session, env), | |||
| 1085 | + | Daemon: daemon, | |||
| 1086 | + | Log: log, | |||
| 1087 | + | IdleTTL: udpProxyIdleTTL, | |||
| 1088 | + | }.Run(ctx) | |||
| 1089 | + | } | |||
| 1090 | + | ||||
| 1074 | func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger) error { | 1091 | func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger) error { | ||
| 1075 | return dgram.ProxyRuntime{Client: a, Daemon: b, Log: log}.Run(ctx) | 1092 | return dgram.ProxyRuntime{Client: a, Daemon: b, Log: log}.Run(ctx) | ||
| 1076 | } | 1093 | } | ||
M internal/app/run_test.go +102 -0
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "os/exec" | |
| 11 | 11 | "path/filepath" | |
| 12 | 12 | "strings" | |
| 13 | + | "sync" | |
| 13 | 14 | "testing" | |
| 14 | 15 | "time" | |
| 15 | 16 | ||
| @@ -83,6 +84,107 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) { | |||
| 83 | 84 | } | |
| 84 | 85 | } | |
| 85 | 86 | ||
| 87 | + | func TestRunStdioProxyReplacesHelloEnvironmentAtDaemonBoundary(t *testing.T) { | |
| 88 | + | original := remoteProxyHello() | |
| 89 | + | client := newRemoteProxyClient(original) | |
| 90 | + | daemon := newRemoteProxyDaemon() | |
| 91 | + | ||
| 92 | + | err := runStdioProxy(context.Background(), client, daemon, []string{"REMOTE=one", "TOKEN=a=b=c"}, nil) | |
| 93 | + | require.NoError(t, err) | |
| 94 | + | ||
| 95 | + | got := daemon.firstHello(t) | |
| 96 | + | original.Env = []string{"REMOTE=one", "TOKEN=a=b=c"} | |
| 97 | + | require.Equal(t, original, got) | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | func TestRunUDPProxyRuntimeReplacesHelloEnvironmentAndSessionAtDaemonBoundary(t *testing.T) { | |
| 101 | + | original := remoteProxyHello() | |
| 102 | + | client := newRemoteProxyClient(original) | |
| 103 | + | daemon := newRemoteProxyDaemon() | |
| 104 | + | ||
| 105 | + | err := runUDPProxyRuntime(context.Background(), "remote-work", client, daemon, []string{"REMOTE=udp", "TOKEN=a=b=c"}, nil) | |
| 106 | + | require.NoError(t, err) | |
| 107 | + | ||
| 108 | + | got := daemon.firstHello(t) | |
| 109 | + | original.Name = "remote-work" | |
| 110 | + | original.Env = []string{"REMOTE=udp", "TOKEN=a=b=c"} | |
| 111 | + | require.Equal(t, original, got) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | func remoteProxyHello() ports.Hello { | |
| 115 | + | return ports.Hello{ | |
| 116 | + | Version: ports.ProtocolVersion, | |
| 117 | + | Intent: ports.IntentAttach, | |
| 118 | + | ClientID: [16]byte{1}, | |
| 119 | + | ResumeToken: 2, | |
| 120 | + | Size: domain.Size{Cols: 80, Rows: 24}, | |
| 121 | + | TermEnv: "client-term", | |
| 122 | + | Cwd: "/client", | |
| 123 | + | TrueColor: true, | |
| 124 | + | MaxOutputInFlight: 1, | |
| 125 | + | Env: []string{"CLIENT=value"}, | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | type remoteProxyClient struct { | |
| 130 | + | first ports.Frame | |
| 131 | + | done chan struct{} | |
| 132 | + | once sync.Once | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func newRemoteProxyClient(hello ports.Hello) *remoteProxyClient { | |
| 136 | + | return &remoteProxyClient{ | |
| 137 | + | first: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}, | |
| 138 | + | done: make(chan struct{}), | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | func (t *remoteProxyClient) Send(ports.Frame) error { return nil } | |
| 143 | + | func (t *remoteProxyClient) Recv() (ports.Frame, error) { | |
| 144 | + | if t.first.Type != 0 { | |
| 145 | + | frame := t.first | |
| 146 | + | t.first = ports.Frame{} | |
| 147 | + | return frame, nil | |
| 148 | + | } | |
| 149 | + | <-t.done | |
| 150 | + | return ports.Frame{}, io.EOF | |
| 151 | + | } | |
| 152 | + | func (t *remoteProxyClient) Close() error { | |
| 153 | + | t.once.Do(func() { close(t.done) }) | |
| 154 | + | return nil | |
| 155 | + | } | |
| 156 | + | ||
| 157 | + | type remoteProxyDaemon struct { | |
| 158 | + | sent chan ports.Frame | |
| 159 | + | done chan struct{} | |
| 160 | + | once sync.Once | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func newRemoteProxyDaemon() *remoteProxyDaemon { | |
| 164 | + | return &remoteProxyDaemon{sent: make(chan ports.Frame, 1), done: make(chan struct{})} | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | func (t *remoteProxyDaemon) Send(frame ports.Frame) error { | |
| 168 | + | t.sent <- frame | |
| 169 | + | t.once.Do(func() { close(t.done) }) | |
| 170 | + | return nil | |
| 171 | + | } | |
| 172 | + | func (t *remoteProxyDaemon) Recv() (ports.Frame, error) { | |
| 173 | + | <-t.done | |
| 174 | + | return ports.Frame{}, io.EOF | |
| 175 | + | } | |
| 176 | + | func (t *remoteProxyDaemon) Close() error { | |
| 177 | + | t.once.Do(func() { close(t.done) }) | |
| 178 | + | return nil | |
| 179 | + | } | |
| 180 | + | func (t *remoteProxyDaemon) firstHello(testingT *testing.T) ports.Hello { | |
| 181 | + | testingT.Helper() | |
| 182 | + | frame := <-t.sent | |
| 183 | + | hello, err := ports.UnmarshalHello(frame.Payload) | |
| 184 | + | require.NoError(testingT, err) | |
| 185 | + | return hello | |
| 186 | + | } | |
| 187 | + | ||
| 86 | 188 | func TestRunUDPProxyUsesBoundedClientMaxPending(t *testing.T) { | |
| 87 | 189 | require.Equal(t, 32, udpProxyClientTransportOptions.MaxPending) | |
| 88 | 190 | } | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "os/exec" | 10 | "os/exec" | ||
| 11 | "path/filepath" | 11 | "path/filepath" | ||
| 12 | "strings" | 12 | "strings" | ||
| 13 | + | "sync" | |||
| 13 | "testing" | 14 | "testing" | ||
| 14 | "time" | 15 | "time" | ||
| 15 | 16 | ||||
| @@ -83,6 +84,107 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) { | |||||
| 83 | } | 84 | } | ||
| 84 | } | 85 | } | ||
| 85 | 86 | ||||
| 87 | + | func TestRunStdioProxyReplacesHelloEnvironmentAtDaemonBoundary(t *testing.T) { | |||
| 88 | + | original := remoteProxyHello() | |||
| 89 | + | client := newRemoteProxyClient(original) | |||
| 90 | + | daemon := newRemoteProxyDaemon() | |||
| 91 | + | ||||
| 92 | + | err := runStdioProxy(context.Background(), client, daemon, []string{"REMOTE=one", "TOKEN=a=b=c"}, nil) | |||
| 93 | + | require.NoError(t, err) | |||
| 94 | + | ||||
| 95 | + | got := daemon.firstHello(t) | |||
| 96 | + | original.Env = []string{"REMOTE=one", "TOKEN=a=b=c"} | |||
| 97 | + | require.Equal(t, original, got) | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | func TestRunUDPProxyRuntimeReplacesHelloEnvironmentAndSessionAtDaemonBoundary(t *testing.T) { | |||
| 101 | + | original := remoteProxyHello() | |||
| 102 | + | client := newRemoteProxyClient(original) | |||
| 103 | + | daemon := newRemoteProxyDaemon() | |||
| 104 | + | ||||
| 105 | + | err := runUDPProxyRuntime(context.Background(), "remote-work", client, daemon, []string{"REMOTE=udp", "TOKEN=a=b=c"}, nil) | |||
| 106 | + | require.NoError(t, err) | |||
| 107 | + | ||||
| 108 | + | got := daemon.firstHello(t) | |||
| 109 | + | original.Name = "remote-work" | |||
| 110 | + | original.Env = []string{"REMOTE=udp", "TOKEN=a=b=c"} | |||
| 111 | + | require.Equal(t, original, got) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | func remoteProxyHello() ports.Hello { | |||
| 115 | + | return ports.Hello{ | |||
| 116 | + | Version: ports.ProtocolVersion, | |||
| 117 | + | Intent: ports.IntentAttach, | |||
| 118 | + | ClientID: [16]byte{1}, | |||
| 119 | + | ResumeToken: 2, | |||
| 120 | + | Size: domain.Size{Cols: 80, Rows: 24}, | |||
| 121 | + | TermEnv: "client-term", | |||
| 122 | + | Cwd: "/client", | |||
| 123 | + | TrueColor: true, | |||
| 124 | + | MaxOutputInFlight: 1, | |||
| 125 | + | Env: []string{"CLIENT=value"}, | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | type remoteProxyClient struct { | |||
| 130 | + | first ports.Frame | |||
| 131 | + | done chan struct{} | |||
| 132 | + | once sync.Once | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func newRemoteProxyClient(hello ports.Hello) *remoteProxyClient { | |||
| 136 | + | return &remoteProxyClient{ | |||
| 137 | + | first: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}, | |||
| 138 | + | done: make(chan struct{}), | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func (t *remoteProxyClient) Send(ports.Frame) error { return nil } | |||
| 143 | + | func (t *remoteProxyClient) Recv() (ports.Frame, error) { | |||
| 144 | + | if t.first.Type != 0 { | |||
| 145 | + | frame := t.first | |||
| 146 | + | t.first = ports.Frame{} | |||
| 147 | + | return frame, nil | |||
| 148 | + | } | |||
| 149 | + | <-t.done | |||
| 150 | + | return ports.Frame{}, io.EOF | |||
| 151 | + | } | |||
| 152 | + | func (t *remoteProxyClient) Close() error { | |||
| 153 | + | t.once.Do(func() { close(t.done) }) | |||
| 154 | + | return nil | |||
| 155 | + | } | |||
| 156 | + | ||||
| 157 | + | type remoteProxyDaemon struct { | |||
| 158 | + | sent chan ports.Frame | |||
| 159 | + | done chan struct{} | |||
| 160 | + | once sync.Once | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | func newRemoteProxyDaemon() *remoteProxyDaemon { | |||
| 164 | + | return &remoteProxyDaemon{sent: make(chan ports.Frame, 1), done: make(chan struct{})} | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | func (t *remoteProxyDaemon) Send(frame ports.Frame) error { | |||
| 168 | + | t.sent <- frame | |||
| 169 | + | t.once.Do(func() { close(t.done) }) | |||
| 170 | + | return nil | |||
| 171 | + | } | |||
| 172 | + | func (t *remoteProxyDaemon) Recv() (ports.Frame, error) { | |||
| 173 | + | <-t.done | |||
| 174 | + | return ports.Frame{}, io.EOF | |||
| 175 | + | } | |||
| 176 | + | func (t *remoteProxyDaemon) Close() error { | |||
| 177 | + | t.once.Do(func() { close(t.done) }) | |||
| 178 | + | return nil | |||
| 179 | + | } | |||
| 180 | + | func (t *remoteProxyDaemon) firstHello(testingT *testing.T) ports.Hello { | |||
| 181 | + | testingT.Helper() | |||
| 182 | + | frame := <-t.sent | |||
| 183 | + | hello, err := ports.UnmarshalHello(frame.Payload) | |||
| 184 | + | require.NoError(testingT, err) | |||
| 185 | + | return hello | |||
| 186 | + | } | |||
| 187 | + | ||||
| 86 | func TestRunUDPProxyUsesBoundedClientMaxPending(t *testing.T) { | 188 | func TestRunUDPProxyUsesBoundedClientMaxPending(t *testing.T) { | ||
| 87 | require.Equal(t, 32, udpProxyClientTransportOptions.MaxPending) | 189 | require.Equal(t, 32, udpProxyClientTransportOptions.MaxPending) | ||
| 88 | } | 190 | } | ||