From 4b7cd61efd82bbe41cedbcce6de9c3c7a01717c3 Mon Sep 17 00:00:00 2001 From: Chris Bailey Date: Tue, 3 Mar 2026 11:57:43 -0800 Subject: [PATCH 1/4] Fix multi-protocol source IP selection --- conn.go | 21 +++++++++++++++++++-- processing.go | 6 +++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/conn.go b/conn.go index ef4a78bd..80bc0721 100644 --- a/conn.go +++ b/conn.go @@ -453,10 +453,27 @@ func NewDialer(value *Dialer) *Dialer { // SetRandomLocalAddr sets a random local address and port for the dialer. If either localIPs or localPorts are empty, // the IP or port, respectively, will be un-set and the system will choose. -func (d *Dialer) SetRandomLocalAddr(network string, localIPs []net.IP, localPorts []uint16) error { +// If targetIP is non-nil, localIPs are filtered to match the target's address family (IPv4 or IPv6) +// to prevent protocol mismatch errors when both IPv4 and IPv6 local addresses are configured. +func (d *Dialer) SetRandomLocalAddr(network string, localIPs []net.IP, localPorts []uint16, targetIP net.IP) error { var localIP net.IP if len(localIPs) != 0 { - localIP = localIPs[rand.Intn(len(localIPs))] + candidates := localIPs + if targetIP != nil { + targetIsIPv4 := targetIP.To4() != nil + filtered := make([]net.IP, 0, len(localIPs)) + for _, ip := range localIPs { + ipIsIPv4 := ip.To4() != nil + if ipIsIPv4 == targetIsIPv4 { + filtered = append(filtered, ip) + } + } + if len(filtered) > 0 { + candidates = filtered + } + // If no matching-family addresses exist, fall back to the full list + } + localIP = candidates[rand.Intn(len(candidates))] } var localPort int if len(localPorts) != 0 { diff --git a/processing.go b/processing.go index ca62f0b2..7fa5445f 100644 --- a/processing.go +++ b/processing.go @@ -90,7 +90,7 @@ func GetDefaultTCPDialer(flags *BaseFlags) func(ctx context.Context, t *ScanTarg } } } - err := dialer.SetRandomLocalAddr("tcp", config.localAddrs, config.localPorts) + err := dialer.SetRandomLocalAddr("tcp", config.localAddrs, config.localPorts, t.IP) if err != nil { return nil, fmt.Errorf("could not set random local address: %w", err) } @@ -155,7 +155,7 @@ func GetDefaultUDPDialer(flags *BaseFlags) func(ctx context.Context, t *ScanTarg // create dialer once and reuse it return func(ctx context.Context, t *ScanTarget, addr string) (net.Conn, error) { dialer := GetTimeoutConnectionDialer(flags.ConnectTimeout, flags.TargetTimeout) - err := dialer.SetRandomLocalAddr("udp", config.localAddrs, config.localPorts) + err := dialer.SetRandomLocalAddr("udp", config.localAddrs, config.localPorts, t.IP) if err != nil { return nil, fmt.Errorf("could not set random local address: %w", err) } @@ -216,7 +216,7 @@ func grabTarget(ctx context.Context, input ScanTarget, m *Monitor) *Grab { } // resolve the target's IP here once, so it doesn't need to be resolved in each module dialer := NewDialer(nil) - err := dialer.SetRandomLocalAddr("udp", config.localAddrs, config.localPorts) + err := dialer.SetRandomLocalAddr("udp", config.localAddrs, config.localPorts, nil) if err != nil { return onResolutionFailure(input, m, fmt.Errorf("could not set random local address: %w", err)) } From ef33eedbcae009e79a8d7564ebf0e381c0b2cd82 Mon Sep 17 00:00:00 2001 From: Chris Bailey Date: Tue, 3 Mar 2026 13:33:23 -0800 Subject: [PATCH 2/4] Tests --- conn.go | 37 +++++++++++++--------- conn_localaddr_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 15 deletions(-) create mode 100644 conn_localaddr_test.go diff --git a/conn.go b/conn.go index 80bc0721..256cd9b9 100644 --- a/conn.go +++ b/conn.go @@ -451,6 +451,27 @@ func NewDialer(value *Dialer) *Dialer { return value.SetDefaults() } +// filterLocalAddrsByFamily filters localIPs to only include addresses matching the address family of targetIP. +// If targetIP is nil or no local IPs are provided, we return the original list. +// If no local IPs match the target's address family, we return the original list to allow dialing to proceed and fail +// with the more informative error from the dial attempt, rather than failing with an empty address list. +func filterLocalAddrsByFamily(localIPs []net.IP, targetIP net.IP) []net.IP { + if targetIP == nil || len(localIPs) == 0 { + return localIPs + } + targetIsIPv4 := targetIP.To4() != nil + filtered := make([]net.IP, 0, len(localIPs)) + for _, ip := range localIPs { + if (ip.To4() != nil) == targetIsIPv4 { + filtered = append(filtered, ip) + } + } + if len(filtered) == 0 { + return localIPs + } + return filtered +} + // SetRandomLocalAddr sets a random local address and port for the dialer. If either localIPs or localPorts are empty, // the IP or port, respectively, will be un-set and the system will choose. // If targetIP is non-nil, localIPs are filtered to match the target's address family (IPv4 or IPv6) @@ -458,21 +479,7 @@ func NewDialer(value *Dialer) *Dialer { func (d *Dialer) SetRandomLocalAddr(network string, localIPs []net.IP, localPorts []uint16, targetIP net.IP) error { var localIP net.IP if len(localIPs) != 0 { - candidates := localIPs - if targetIP != nil { - targetIsIPv4 := targetIP.To4() != nil - filtered := make([]net.IP, 0, len(localIPs)) - for _, ip := range localIPs { - ipIsIPv4 := ip.To4() != nil - if ipIsIPv4 == targetIsIPv4 { - filtered = append(filtered, ip) - } - } - if len(filtered) > 0 { - candidates = filtered - } - // If no matching-family addresses exist, fall back to the full list - } + candidates := filterLocalAddrsByFamily(localIPs, targetIP) localIP = candidates[rand.Intn(len(candidates))] } var localPort int diff --git a/conn_localaddr_test.go b/conn_localaddr_test.go new file mode 100644 index 00000000..78e5fabc --- /dev/null +++ b/conn_localaddr_test.go @@ -0,0 +1,72 @@ +package zgrab2 + +import ( + "net" + "testing" +) + +var ( + testIPv4a = net.ParseIP("192.168.1.1") + testIPv4b = net.ParseIP("192.168.1.2") + testIPv6a = net.ParseIP("2001:db8::1") + testIPv6b = net.ParseIP("2001:db8::2") + testTargetIPv4 = net.ParseIP("192.168.1.100") + testTargetIPv6 = net.ParseIP("2001:db8::100") +) + +func TestFilterLocalAddrsByFamily_IPv4Target(t *testing.T) { + result := filterLocalAddrsByFamily([]net.IP{testIPv4a, testIPv6a, testIPv4b, testIPv6b}, testTargetIPv4) + if len(result) != 2 { + t.Fatalf("expected 2 IPv4 addresses, got %d: %v", len(result), result) + } + for _, ip := range result { + if ip.To4() == nil { + t.Errorf("expected only IPv4 addresses, got %s", ip) + } + } +} + +func TestFilterLocalAddrsByFamily_IPv6Target(t *testing.T) { + result := filterLocalAddrsByFamily([]net.IP{testIPv4a, testIPv6a, testIPv6b}, testTargetIPv6) + if len(result) != 2 { + t.Fatalf("expected 2 IPv6 addresses, got %d: %v", len(result), result) + } + for _, ip := range result { + if ip.To4() != nil { + t.Errorf("expected only IPv6 addresses, got %s", ip) + } + } +} + +func TestFilterLocalAddrsByFamily_NilTarget(t *testing.T) { + result := filterLocalAddrsByFamily([]net.IP{testIPv4a, testIPv6a}, nil) + if len(result) != 2 { + t.Fatalf("expected all addresses returned when target is nil, got %d", len(result)) + } +} + +func TestFilterLocalAddrsByFamily_NoMatchFallsBack(t *testing.T) { + result := filterLocalAddrsByFamily([]net.IP{testIPv6a, testIPv6b}, testTargetIPv4) + if len(result) != 2 { + t.Fatalf("expected fallback to full list (2 addrs), got %d: %v", len(result), result) + } +} + +func TestFilterLocalAddrsByFamily_EmptyInput(t *testing.T) { + result := filterLocalAddrsByFamily(nil, testTargetIPv4) + if len(result) != 0 { + t.Fatalf("expected empty result for nil input, got %d", len(result)) + } + + result = filterLocalAddrsByFamily([]net.IP{}, testTargetIPv4) + if len(result) != 0 { + t.Fatalf("expected empty result for empty input, got %d", len(result)) + } +} + +func TestFilterLocalAddrsByFamily_AllSameFamily(t *testing.T) { + result := filterLocalAddrsByFamily([]net.IP{testIPv4a, testIPv4b}, testTargetIPv4) + if len(result) != 2 { + t.Fatalf("expected 2 addresses when all match, got %d", len(result)) + } +} From 25b29c04ffe84db49a28fd9a24a4877daecf7ea6 Mon Sep 17 00:00:00 2001 From: Chris Bailey Date: Tue, 3 Mar 2026 13:35:43 -0800 Subject: [PATCH 3/4] comment wrapping --- conn.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conn.go b/conn.go index 256cd9b9..678ade03 100644 --- a/conn.go +++ b/conn.go @@ -474,8 +474,8 @@ func filterLocalAddrsByFamily(localIPs []net.IP, targetIP net.IP) []net.IP { // SetRandomLocalAddr sets a random local address and port for the dialer. If either localIPs or localPorts are empty, // the IP or port, respectively, will be un-set and the system will choose. -// If targetIP is non-nil, localIPs are filtered to match the target's address family (IPv4 or IPv6) -// to prevent protocol mismatch errors when both IPv4 and IPv6 local addresses are configured. +// If targetIP is non-nil, localIPs are filtered to match the target's address family (IPv4 or IPv6) to prevent +// protocol mismatch errors when both IPv4 and IPv6 local addresses are configured. func (d *Dialer) SetRandomLocalAddr(network string, localIPs []net.IP, localPorts []uint16, targetIP net.IP) error { var localIP net.IP if len(localIPs) != 0 { From 6eb7e0f5afc9815b84d24c11aea135ccc2317461 Mon Sep 17 00:00:00 2001 From: phillip-stephens Date: Tue, 31 Mar 2026 14:51:06 +1200 Subject: [PATCH 4/4] Fixed PR comments, make filtering function more predictable and guarded against a panic in rand.Intn --- conn.go | 12 ++++++------ conn_localaddr_test.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conn.go b/conn.go index 678ade03..9990050f 100644 --- a/conn.go +++ b/conn.go @@ -453,8 +453,7 @@ func NewDialer(value *Dialer) *Dialer { // filterLocalAddrsByFamily filters localIPs to only include addresses matching the address family of targetIP. // If targetIP is nil or no local IPs are provided, we return the original list. -// If no local IPs match the target's address family, we return the original list to allow dialing to proceed and fail -// with the more informative error from the dial attempt, rather than failing with an empty address list. +// If no local IPs match the target's address family, we return an empty list. func filterLocalAddrsByFamily(localIPs []net.IP, targetIP net.IP) []net.IP { if targetIP == nil || len(localIPs) == 0 { return localIPs @@ -462,13 +461,11 @@ func filterLocalAddrsByFamily(localIPs []net.IP, targetIP net.IP) []net.IP { targetIsIPv4 := targetIP.To4() != nil filtered := make([]net.IP, 0, len(localIPs)) for _, ip := range localIPs { - if (ip.To4() != nil) == targetIsIPv4 { + ipIsIPv4 := ip.To4() != nil + if ipIsIPv4 == targetIsIPv4 { filtered = append(filtered, ip) } } - if len(filtered) == 0 { - return localIPs - } return filtered } @@ -480,6 +477,9 @@ func (d *Dialer) SetRandomLocalAddr(network string, localIPs []net.IP, localPort var localIP net.IP if len(localIPs) != 0 { candidates := filterLocalAddrsByFamily(localIPs, targetIP) + if len(candidates) == 0 { + return fmt.Errorf("no selected local IPs %v match the address family of the target IP %s, so target would not be reachable", localIPs, targetIP.String()) + } localIP = candidates[rand.Intn(len(candidates))] } var localPort int diff --git a/conn_localaddr_test.go b/conn_localaddr_test.go index 78e5fabc..4440f2c1 100644 --- a/conn_localaddr_test.go +++ b/conn_localaddr_test.go @@ -47,8 +47,8 @@ func TestFilterLocalAddrsByFamily_NilTarget(t *testing.T) { func TestFilterLocalAddrsByFamily_NoMatchFallsBack(t *testing.T) { result := filterLocalAddrsByFamily([]net.IP{testIPv6a, testIPv6b}, testTargetIPv4) - if len(result) != 2 { - t.Fatalf("expected fallback to full list (2 addrs), got %d: %v", len(result), result) + if len(result) != 0 { + t.Fatalf("expected empty list, got %d: %v", len(result), result) } }