From 3c3e530a79c2725987fa2ca056861d0279640567 Mon Sep 17 00:00:00 2001 From: Andrea Zucchelli Date: Sat, 29 Aug 2026 13:53:50 +0200 Subject: [PATCH] fix(ssh): forward custom DERP map to ProxyCommand --- cmd/tailcat/ssh.go | 14 +++++++++++++- cmd/tailcat/ssh_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/cmd/tailcat/ssh.go b/cmd/tailcat/ssh.go index 59e175dd0..7afa3e87d 100644 --- a/cmd/tailcat/ssh.go +++ b/cmd/tailcat/ssh.go @@ -17,6 +17,7 @@ import ( "strings" "syscall" + "github.com/tailscale/tailcat" "tailscale.com/types/logger" ) @@ -63,7 +64,7 @@ func clientSSHMode(logf logger.Logf) { "-o", "StrictHostKeyChecking no", "-o", "UserKnownHostsFile /dev/null", "-o", "LogLevel ERROR", - "-o", fmt.Sprintf("ProxyCommand=%s --key=%q %s %s", exe, *flagKey, connBlobStr, portOrIPPort), + "-o", "ProxyCommand=" + sshProxyCommand(exe, *flagKey, *flagDERPMapURL, connBlobStr, portOrIPPort), sshDst, } argv = append(argv, cmdArgs...) @@ -71,6 +72,17 @@ func clientSSHMode(logf logger.Logf) { log.Fatalf("failed to exec: %v", err) } +// sshProxyCommand returns the command passed to OpenSSH to connect the SSH +// client to a tailcat server. The command is run by OpenSSH, so values that +// can contain shell-special characters must be quoted. +func sshProxyCommand(exe, keyName, derpMapURL, connBlob, portOrIPPort string) string { + cmd := fmt.Sprintf("%s --key=%q", exe, keyName) + if derpMapURL != tailcat.DefaultDERPMapURL { + cmd += fmt.Sprintf(" --derpmap-url=%q", derpMapURL) + } + return fmt.Sprintf("%s %s %s", cmd, connBlob, portOrIPPort) +} + // sshDestHost returns the hostname to give the system ssh client as the // connection destination for a tailcat ConnBlob. It is a short, deterministic // function of blob rather than blob itself. diff --git a/cmd/tailcat/ssh_test.go b/cmd/tailcat/ssh_test.go index bbcf44127..8281d1383 100644 --- a/cmd/tailcat/ssh_test.go +++ b/cmd/tailcat/ssh_test.go @@ -8,8 +8,32 @@ package main import ( "strings" "testing" + + "github.com/tailscale/tailcat" ) +func TestSSHProxyCommandDERPMap(t *testing.T) { + const ( + exe = "/path/to/tailcat" + key = "client-default" + blob = "tc-short-blob" + port = "22" + url = "https://derp.example.com/derpmap.json" + ) + + got := sshProxyCommand(exe, key, url, blob, port) + want := exe + ` --key="client-default" --derpmap-url="https://derp.example.com/derpmap.json" tc-short-blob 22` + if got != want { + t.Errorf("sshProxyCommand with custom DERP map = %q; want %q", got, want) + } + + got = sshProxyCommand(exe, key, tailcat.DefaultDERPMapURL, blob, port) + want = exe + ` --key="client-default" tc-short-blob 22` + if got != want { + t.Errorf("sshProxyCommand with default DERP map = %q; want %q", got, want) + } +} + func TestSSHDestHost(t *testing.T) { // A realistic ConnBlob, taken from an existing test fixture elsewhere // in this package.