@@ -87,6 +87,57 @@ func TestDemoDockerComposeE2E(t *testing.T) {
8787 assertClientCommandContains (t , demoDir , "lssh --host OverSocksProxy hostname" , "over-proxy-ssh" )
8888 })
8989
90+ t .Run ("control master local forward works" , func (t * testing.T ) {
91+ pidFile := "/tmp/lssh-demo-local-forward.pid"
92+ t .Cleanup (func () {
93+ stopClientForward (t , demoDir , pidFile )
94+ })
95+
96+ startClientForward (t , demoDir , pidFile ,
97+ "lssh --host OverSshProxyCM -N -L 10081:localhost:22" ,
98+ "127.0.0.1:10081" ,
99+ )
100+
101+ assertClientCommandContains (t , demoDir ,
102+ `banner=$(nc -w 5 127.0.0.1 10081 | head -c 8); printf '%s' "$banner"` ,
103+ "SSH-2.0-" ,
104+ )
105+ })
106+
107+ t .Run ("control master dynamic forward works" , func (t * testing.T ) {
108+ pidFile := "/tmp/lssh-demo-dynamic-forward.pid"
109+ t .Cleanup (func () {
110+ stopClientForward (t , demoDir , pidFile )
111+ })
112+
113+ startClientForward (t , demoDir , pidFile ,
114+ "lssh --host OverSshProxyCM -N -D 10080" ,
115+ "127.0.0.1:10080" ,
116+ )
117+
118+ assertClientCommandContains (t , demoDir ,
119+ `banner=$(nc -X 5 -x 127.0.0.1:10080 -w 5 172.31.1.41 22 | head -c 8); printf '%s' "$banner"` ,
120+ "SSH-2.0-" ,
121+ )
122+ })
123+
124+ t .Run ("control master remote forward works" , func (t * testing.T ) {
125+ pidFile := "/tmp/lssh-demo-remote-forward.pid"
126+ t .Cleanup (func () {
127+ stopClientForward (t , demoDir , pidFile )
128+ })
129+
130+ startClientForward (t , demoDir , pidFile ,
131+ "lssh --host OverSshProxyCM -N -R 172.31.0.10:2222:10082" ,
132+ "" ,
133+ )
134+
135+ waitForComposeExecContains (t , demoDir , "over_proxy_ssh" ,
136+ `banner=$(bash -lc 'exec 3<>/dev/tcp/127.0.0.1/10082; head -c 8 <&3' 2>/dev/null || true); printf '%s' "$banner"` ,
137+ "SSH-2.0-" ,
138+ )
139+ })
140+
90141 t .Run ("local rc is available on remote shell" , func (t * testing.T ) {
91142 assertClientCommandContains (t , demoDir ,
92143 `lssh --host LocalRcKeyAuth 'type lvim >/dev/null && type ltmux >/dev/null && echo local_rc_ok'` ,
@@ -227,3 +278,72 @@ func mustRunComposeCommand(t *testing.T, demoDir string, args ...string) string
227278
228279 return string (output )
229280}
281+
282+ func startClientForward (t * testing.T , demoDir , pidFile , forwardCommand , waitAddr string ) {
283+ t .Helper ()
284+
285+ stopClientForward (t , demoDir , pidFile )
286+
287+ startCmd := fmt .Sprintf (
288+ `rm -f %[1]s; nohup %[2]s >/tmp/$(basename %[1]s).log 2>&1 & echo $! > %[1]s` ,
289+ pidFile ,
290+ forwardCommand ,
291+ )
292+
293+ if output , err := runClientCommand (demoDir , startCmd ); err != nil {
294+ t .Fatalf ("failed to start forward: %s\n error: %v\n output:\n %s" , forwardCommand , err , output )
295+ }
296+
297+ if waitAddr == "" {
298+ time .Sleep (2 * time .Second )
299+ return
300+ }
301+
302+ deadline := time .Now ().Add (10 * time .Second )
303+ checkCmd := fmt .Sprintf ("nc -z -w 2 %s %s" , strings .Split (waitAddr , ":" )[0 ], strings .Split (waitAddr , ":" )[1 ])
304+ for time .Now ().Before (deadline ) {
305+ if _ , err := runClientCommand (demoDir , checkCmd ); err == nil {
306+ return
307+ }
308+ time .Sleep (200 * time .Millisecond )
309+ }
310+
311+ logOutput , _ := runClientCommand (demoDir , fmt .Sprintf ("cat /tmp/$(basename %s).log || true" , pidFile ))
312+ t .Fatalf ("forward did not become ready: %s\n log:\n %s" , forwardCommand , logOutput )
313+ }
314+
315+ func stopClientForward (t * testing.T , demoDir , pidFile string ) {
316+ t .Helper ()
317+
318+ _ , _ = runClientCommand (demoDir ,
319+ fmt .Sprintf (`if [ -f %[1]s ]; then kill $(cat %[1]s) >/dev/null 2>&1 || true; rm -f %[1]s; fi` , pidFile ),
320+ )
321+ }
322+
323+ func waitForComposeExecContains (t * testing.T , demoDir , service , command , want string ) {
324+ t .Helper ()
325+
326+ deadline := time .Now ().Add (10 * time .Second )
327+ var lastOutput string
328+
329+ for time .Now ().Before (deadline ) {
330+ output , err := runComposeServiceCommand (demoDir , service , command )
331+ lastOutput = output
332+ if err == nil && strings .Contains (output , want ) {
333+ return
334+ }
335+ time .Sleep (200 * time .Millisecond )
336+ }
337+
338+ t .Fatalf ("output missing %q for service %s command %s\n last output:\n %s" , want , service , command , lastOutput )
339+ }
340+
341+ func runComposeServiceCommand (demoDir , service , command string ) (string , error ) {
342+ cmd := exec .Command ("docker" , "compose" , "exec" , "-T" , service , "bash" , "-lc" , command )
343+ cmd .Dir = demoDir
344+ output , err := cmd .CombinedOutput ()
345+ if err != nil {
346+ return string (output ), fmt .Errorf ("%w" , err )
347+ }
348+ return string (output ), nil
349+ }
0 commit comments