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.
This commit is contained in:
Graham McIntire 2026-02-12 10:58:28 -06:00
parent 8b65dffc37
commit a0516c2bd1
No known key found for this signature in database
2 changed files with 39 additions and 2 deletions

7
ssh.go
View file

@ -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)
}

View file

@ -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 {