From a0516c2bd1c17d45158efe08b47776e06b4e71a2 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Thu, 12 Feb 2026 10:58:28 -0600 Subject: [PATCH] security: use net.JoinHostPort for SSH addresses to support IPv6 fmt.Sprintf("%s:%d") produces invalid addresses for IPv6. Also adds mockable sshDial variable for testability. --- ssh.go | 7 +++++-- ssh_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/ssh.go b/ssh.go index a79cefe..2759a38 100644 --- a/ssh.go +++ b/ssh.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "log/slog" + "net" + "strconv" "time" "github.com/towerops-app/towerops-agent/pb" @@ -11,6 +13,7 @@ import ( ) var sshBackup = executeMikrotikBackup +var sshDial = ssh.Dial var doPing = pingDevice // executeMikrotikBackup connects via SSH and runs /export compact. @@ -26,8 +29,8 @@ func executeMikrotikBackup(ip string, port uint16, username, password string) (s Timeout: 30 * time.Second, } - addr := fmt.Sprintf("%s:%d", ip, port) - conn, err := ssh.Dial("tcp", addr, config) + addr := net.JoinHostPort(ip, strconv.Itoa(int(port))) + conn, err := sshDial("tcp", addr, config) if err != nil { return "", fmt.Errorf("ssh dial %s: %w", addr, err) } diff --git a/ssh_test.go b/ssh_test.go index 55bc357..81d44e5 100644 --- a/ssh_test.go +++ b/ssh_test.go @@ -270,6 +270,40 @@ func TestExecuteMikrotikBackupViaSSH(t *testing.T) { }) } +func TestSSHBackupIPv6Address(t *testing.T) { + origDial := sshDial + defer func() { sshDial = origDial }() + + var capturedAddr string + sshDial = func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { + capturedAddr = addr + return nil, fmt.Errorf("mock dial") + } + + _, _ = executeMikrotikBackup("::1", 22, "admin", "pass") + + if capturedAddr != "[::1]:22" { + t.Errorf("expected [::1]:22, got %q", capturedAddr) + } +} + +func TestSSHBackupIPv4Address(t *testing.T) { + origDial := sshDial + defer func() { sshDial = origDial }() + + var capturedAddr string + sshDial = func(network, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { + capturedAddr = addr + return nil, fmt.Errorf("mock dial") + } + + _, _ = executeMikrotikBackup("10.0.0.1", 22, "admin", "pass") + + if capturedAddr != "10.0.0.1:22" { + t.Errorf("expected 10.0.0.1:22, got %q", capturedAddr) + } +} + func TestExecuteMikrotikBackupDialError(t *testing.T) { _, err := executeMikrotikBackup("127.0.0.1", 1, "admin", "pass") if err == nil {