| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "io" |
| 6 | "net" |
| 7 | "net/http" |
| 8 | "net/http/httptest" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "golang.org/x/crypto/ssh" |
| 13 | |
| 14 | "reasonix/internal/remote/forward" |
| 15 | "reasonix/internal/remote/sshtest" |
| 16 | ) |
| 17 | |
| 18 | // TestCredentialProxyReverseForwardEndToEnd drives the real SSH forwarding |
| 19 | // protocol against an sshtest server: the "remote" loopback listener forwards |
| 20 | // connections back through the SSH channel to the desktop-side target, which |
| 21 | // is exactly the path local-proxy model calls take. The helper is idempotent. |
| 22 | func TestCredentialProxyReverseForwardEndToEnd(t *testing.T) { |
| 23 | target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 24 | _, _ = w.Write([]byte("proxy-ok")) |
| 25 | })) |
| 26 | defer target.Close() |
| 27 | targetPort := target.Listener.Addr().(*net.TCPAddr).Port |
| 28 | |
| 29 | srv := sshtest.Start(t, sshtest.Options{}) |
| 30 | cfg := &ssh.ClientConfig{User: "t", HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: 5 * time.Second} |
| 31 | cl, err := ssh.Dial("tcp", srv.Addr, cfg) |
| 32 | if err != nil { |
| 33 | t.Fatal(err) |
| 34 | } |
| 35 | t.Cleanup(func() { _ = cl.Close() }) |
| 36 | set := forward.NewSet(nil) |
| 37 | set.Attach(cl) |
| 38 | lc := newLifecycleSSHClient(nil) |
| 39 | lc.forwards = set |
| 40 | |
| 41 | remotePort, err := ensureCredentialProxyForward(lc, "box", targetPort) |
| 42 | if err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | if remotePort <= 0 || remotePort > 65535 { |
| 46 | t.Fatalf("remote port = %d, want an ephemeral bound port", remotePort) |
| 47 | } |
| 48 | // Idempotent: a second ensure neither errors nor adds a second forward, |
| 49 | // and reports the SAME bound port. |
| 50 | again, err := ensureCredentialProxyForward(lc, "box", targetPort) |
| 51 | if err != nil { |
| 52 | t.Fatalf("second ensure: %v", err) |
| 53 | } |
| 54 | if again != remotePort { |
| 55 | t.Fatalf("second ensure port = %d, want %d", again, remotePort) |
| 56 | } |
| 57 | if n := len(set.List()); n != 1 { |
| 58 | t.Fatalf("forward count = %d, want 1", n) |
| 59 | } |
| 60 | |
| 61 | // Dial the REMOTE-side bind address. sshtest runs on localhost, so the |
| 62 | // remote loopback port is reachable here; the bytes travel |
| 63 | // dial → ssh server → forwarded-tcpip → ssh client → target. |
| 64 | resp, err := (&http.Client{Timeout: 5 * time.Second}).Get(fmt.Sprintf("http://127.0.0.1:%d/hello", remotePort)) |
| 65 | if err != nil { |
| 66 | t.Fatalf("reverse forward dial: %v", err) |
| 67 | } |
| 68 | defer resp.Body.Close() |
| 69 | body, _ := io.ReadAll(resp.Body) |
| 70 | if string(body) != "proxy-ok" { |
| 71 | t.Fatalf("body = %q, want proxy-ok", body) |
| 72 | } |
| 73 | } |
| 74 |