From f1c7ef7a81e4c0f61c2dfee22e49e3d74d424c57 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Thu, 15 May 2025 16:54:50 -0400 Subject: [PATCH 01/17] Clean up IsDualStack tests --- net/ipfamily_test.go | 164 +++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 83 deletions(-) diff --git a/net/ipfamily_test.go b/net/ipfamily_test.go index 78f2cdc1..13dd954d 100644 --- a/net/ipfamily_test.go +++ b/net/ipfamily_test.go @@ -24,167 +24,169 @@ import ( func TestDualStackIPs(t *testing.T) { testCases := []struct { + desc string ips []string - errMessage string expectedResult bool expectError bool }{ { + desc: "should fail because length is not at least 2", ips: []string{"1.1.1.1"}, - errMessage: "should fail because length is not at least 2", expectedResult: false, expectError: false, }, { + desc: "should fail because length is not at least 2", ips: []string{}, - errMessage: "should fail because length is not at least 2", expectedResult: false, expectError: false, }, { + desc: "should fail because all are v4", ips: []string{"1.1.1.1", "2.2.2.2", "3.3.3.3"}, - errMessage: "should fail because all are v4", expectedResult: false, expectError: false, }, { + desc: "should fail because all are v6", ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff0", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff1"}, - errMessage: "should fail because all are v6", expectedResult: false, expectError: false, }, { + desc: "should fail because 2nd ip is invalid", ips: []string{"1.1.1.1", "not-a-valid-ip"}, - errMessage: "should fail because 2nd ip is invalid", expectedResult: false, expectError: true, }, { + desc: "should fail because 1st ip is invalid", ips: []string{"not-a-valid-ip", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff"}, - errMessage: "should fail because 1st ip is invalid", expectedResult: false, expectError: true, }, { + desc: "should fail despite dual-stack because 3rd ip is invalid", + ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "not-a-valid-ip"}, + expectedResult: false, + expectError: true, + }, + { + desc: "dual-stack ipv4-primary", ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff"}, - errMessage: "expected success, but found failure", expectedResult: true, expectError: false, }, { + desc: "dual-stack, multiple ipv6", ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff0"}, - errMessage: "expected success, but found failure", expectedResult: true, expectError: false, }, { + desc: "dual-stack, multiple ipv4", ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "10.0.0.0"}, - errMessage: "expected success, but found failure", expectedResult: true, expectError: false, }, { + desc: "dual-stack, ipv6-primary", ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "1.1.1.1"}, - errMessage: "expected success, but found failure", expectedResult: true, expectError: false, }, } // for each test case, test the regular func and the string func for _, tc := range testCases { - dualStack, err := IsDualStackIPStrings(tc.ips) - if err == nil && tc.expectError { - t.Errorf("%s", tc.errMessage) - continue - } - if err != nil && !tc.expectError { - t.Errorf("failed to run test case for %v, error: %v", tc.ips, err) - continue - } - if dualStack != tc.expectedResult { - t.Errorf("%v for %v", tc.errMessage, tc.ips) - } - } + t.Run(tc.desc, func(t *testing.T) { + dualStack, err := IsDualStackIPStrings(tc.ips) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackIPStrings") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackIPStrings: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackIPStrings=%v, got %v", tc.expectedResult, dualStack) + } - for _, tc := range testCases { - ips := make([]net.IP, 0, len(tc.ips)) - for _, ip := range tc.ips { - parsedIP := ParseIPSloppy(ip) - ips = append(ips, parsedIP) - } - dualStack, err := IsDualStackIPs(ips) - if err == nil && tc.expectError { - t.Errorf("%s", tc.errMessage) - continue - } - if err != nil && !tc.expectError { - t.Errorf("failed to run test case for %v, error: %v", tc.ips, err) - continue - } - if dualStack != tc.expectedResult { - t.Errorf("%v for %v", tc.errMessage, tc.ips) - } + ips := make([]net.IP, 0, len(tc.ips)) + for _, ip := range tc.ips { + parsedIP := ParseIPSloppy(ip) + ips = append(ips, parsedIP) + } + dualStack, err = IsDualStackIPs(ips) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackIPs") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackIPs: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackIPs=%v, got %v", tc.expectedResult, dualStack) + } + }) } } func TestDualStackCIDRs(t *testing.T) { testCases := []struct { + desc string cidrs []string - errMessage string expectedResult bool expectError bool }{ { + desc: "should fail because length is not at least 2", cidrs: []string{"10.10.10.10/8"}, - errMessage: "should fail because length is not at least 2", expectedResult: false, expectError: false, }, { + desc: "should fail because length is not at least 2", cidrs: []string{}, - errMessage: "should fail because length is not at least 2", expectedResult: false, expectError: false, }, { + desc: "should fail because all cidrs are v4", cidrs: []string{"10.10.10.10/8", "20.20.20.20/8", "30.30.30.30/8"}, - errMessage: "should fail because all cidrs are v4", expectedResult: false, expectError: false, }, { + desc: "should fail because all cidrs are v6", cidrs: []string{"2000::/10", "3000::/10"}, - errMessage: "should fail because all cidrs are v6", expectedResult: false, expectError: false, }, { + desc: "should fail because 2nd cidr is invalid", cidrs: []string{"10.10.10.10/8", "not-a-valid-cidr"}, - errMessage: "should fail because 2nd cidr is invalid", expectedResult: false, expectError: true, }, { + desc: "should fail because 1st cidr is invalid", cidrs: []string{"not-a-valid-ip", "2000::/10"}, - errMessage: "should fail because 1st cidr is invalid", expectedResult: false, expectError: true, }, { + desc: "dual-stack, ipv4-primary", cidrs: []string{"10.10.10.10/8", "2000::/10"}, - errMessage: "expected success, but found failure", expectedResult: true, expectError: false, }, { + desc: "dual-stack, ipv6-primary", cidrs: []string{"2000::/10", "10.10.10.10/8"}, - errMessage: "expected success, but found failure", expectedResult: true, expectError: false, }, { + desc: "dual-stack, multiple IPv6", cidrs: []string{"2000::/10", "10.10.10.10/8", "3000::/10"}, - errMessage: "expected success, but found failure", expectedResult: true, expectError: false, }, @@ -192,39 +194,35 @@ func TestDualStackCIDRs(t *testing.T) { // for each test case, test the regular func and the string func for _, tc := range testCases { - dualStack, err := IsDualStackCIDRStrings(tc.cidrs) - if err == nil && tc.expectError { - t.Errorf("%s", tc.errMessage) - continue - } - if err != nil && !tc.expectError { - t.Errorf("failed to run test case for %v, error: %v", tc.cidrs, err) - continue - } - if dualStack != tc.expectedResult { - t.Errorf("%v for %v", tc.errMessage, tc.cidrs) - } - } + t.Run(tc.desc, func(t *testing.T) { + dualStack, err := IsDualStackCIDRStrings(tc.cidrs) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackCIDRStrings") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackCIDRStrings: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackCIDRStrings=%v, got %v", tc.expectedResult, dualStack) + } - for _, tc := range testCases { - cidrs := make([]*net.IPNet, 0, len(tc.cidrs)) - for _, cidr := range tc.cidrs { - _, parsedCIDR, _ := ParseCIDRSloppy(cidr) - cidrs = append(cidrs, parsedCIDR) - } + cidrs := make([]*net.IPNet, 0, len(tc.cidrs)) + for _, cidr := range tc.cidrs { + _, parsedCIDR, _ := ParseCIDRSloppy(cidr) + cidrs = append(cidrs, parsedCIDR) + } - dualStack, err := IsDualStackCIDRs(cidrs) - if err == nil && tc.expectError { - t.Errorf("%s", tc.errMessage) - continue - } - if err != nil && !tc.expectError { - t.Errorf("failed to run test case for %v, error: %v", tc.cidrs, err) - continue - } - if dualStack != tc.expectedResult { - t.Errorf("%v for %v", tc.errMessage, tc.cidrs) - } + dualStack, err = IsDualStackCIDRs(cidrs) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackCIDRs") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackCIDRs: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackCIDRs=%v, got %v", tc.expectedResult, dualStack) + } + }) } } From d737f729cb5dcaa5a7429e5266091bafe1e58ea5 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Thu, 15 May 2025 12:39:43 -0400 Subject: [PATCH 02/17] Add k8s.io/utils/net/v2 (currently just a copy of v1) --- net/v2/ipfamily.go | 181 +++++++++++ net/v2/ipfamily_test.go | 332 +++++++++++++++++++++ net/v2/ipnet.go | 221 ++++++++++++++ net/v2/ipnet_test.go | 314 +++++++++++++++++++ net/v2/ips_test.go | 579 ++++++++++++++++++++++++++++++++++++ net/v2/multi_listen.go | 195 ++++++++++++ net/v2/multi_listen_test.go | 545 +++++++++++++++++++++++++++++++++ net/v2/net.go | 91 ++++++ net/v2/net_test.go | 250 ++++++++++++++++ net/v2/parse.go | 33 ++ net/v2/parse_test.go | 107 +++++++ net/v2/port.go | 129 ++++++++ net/v2/port_test.go | 87 ++++++ 13 files changed, 3064 insertions(+) create mode 100644 net/v2/ipfamily.go create mode 100644 net/v2/ipfamily_test.go create mode 100644 net/v2/ipnet.go create mode 100644 net/v2/ipnet_test.go create mode 100644 net/v2/ips_test.go create mode 100644 net/v2/multi_listen.go create mode 100644 net/v2/multi_listen_test.go create mode 100644 net/v2/net.go create mode 100644 net/v2/net_test.go create mode 100644 net/v2/parse.go create mode 100644 net/v2/parse_test.go create mode 100644 net/v2/port.go create mode 100644 net/v2/port_test.go diff --git a/net/v2/ipfamily.go b/net/v2/ipfamily.go new file mode 100644 index 00000000..1a51fa39 --- /dev/null +++ b/net/v2/ipfamily.go @@ -0,0 +1,181 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "fmt" + "net" +) + +// IPFamily refers to a specific family if not empty, i.e. "4" or "6". +type IPFamily string + +// Constants for valid IPFamilys: +const ( + IPFamilyUnknown IPFamily = "" + + IPv4 IPFamily = "4" + IPv6 IPFamily = "6" +) + +// IsDualStackIPs returns true if: +// - all elements of ips are valid +// - at least one IP from each family (v4 and v6) is present +func IsDualStackIPs(ips []net.IP) (bool, error) { + v4Found := false + v6Found := false + for i, ip := range ips { + switch IPFamilyOf(ip) { + case IPv4: + v4Found = true + case IPv6: + v6Found = true + default: + return false, fmt.Errorf("invalid IP[%d]: %v", i, ip) + } + } + + return (v4Found && v6Found), nil +} + +// IsDualStackIPStrings returns true if: +// - all elements of ips can be parsed as IPs +// - at least one IP from each family (v4 and v6) is present +func IsDualStackIPStrings(ips []string) (bool, error) { + parsedIPs := make([]net.IP, 0, len(ips)) + for i, ip := range ips { + parsedIP := ParseIPSloppy(ip) + if parsedIP == nil { + return false, fmt.Errorf("invalid IP[%d]: %v", i, ip) + } + parsedIPs = append(parsedIPs, parsedIP) + } + return IsDualStackIPs(parsedIPs) +} + +// IsDualStackCIDRs returns true if: +// - all elements of cidrs are non-nil +// - at least one CIDR from each family (v4 and v6) is present +func IsDualStackCIDRs(cidrs []*net.IPNet) (bool, error) { + v4Found := false + v6Found := false + for i, cidr := range cidrs { + switch IPFamilyOfCIDR(cidr) { + case IPv4: + v4Found = true + case IPv6: + v6Found = true + default: + return false, fmt.Errorf("invalid CIDR[%d]: %v", i, cidr) + } + } + + return (v4Found && v6Found), nil +} + +// IsDualStackCIDRStrings returns if +// - all elements of cidrs can be parsed as CIDRs +// - at least one CIDR from each family (v4 and v6) is present +func IsDualStackCIDRStrings(cidrs []string) (bool, error) { + parsedCIDRs, err := ParseCIDRs(cidrs) + if err != nil { + return false, err + } + return IsDualStackCIDRs(parsedCIDRs) +} + +// IPFamilyOf returns the IP family of ip, or IPFamilyUnknown if it is invalid. +func IPFamilyOf(ip net.IP) IPFamily { + switch { + case ip.To4() != nil: + return IPv4 + case ip.To16() != nil: + return IPv6 + default: + return IPFamilyUnknown + } +} + +// IPFamilyOfString returns the IP family of ip, or IPFamilyUnknown if ip cannot +// be parsed as an IP. +func IPFamilyOfString(ip string) IPFamily { + return IPFamilyOf(ParseIPSloppy(ip)) +} + +// IPFamilyOfCIDR returns the IP family of cidr. +func IPFamilyOfCIDR(cidr *net.IPNet) IPFamily { + if cidr == nil { + return IPFamilyUnknown + } + return IPFamilyOf(cidr.IP) +} + +// IPFamilyOfCIDRString returns the IP family of cidr. +func IPFamilyOfCIDRString(cidr string) IPFamily { + ip, _, _ := ParseCIDRSloppy(cidr) + return IPFamilyOf(ip) +} + +// IsIPv6 returns true if netIP is IPv6 (and false if it is IPv4, nil, or invalid). +func IsIPv6(netIP net.IP) bool { + return IPFamilyOf(netIP) == IPv6 +} + +// IsIPv6String returns true if ip contains a single IPv6 address and nothing else. It +// returns false if ip is an empty string, an IPv4 address, or anything else that is not a +// single IPv6 address. +func IsIPv6String(ip string) bool { + return IPFamilyOfString(ip) == IPv6 +} + +// IsIPv6CIDR returns true if a cidr is a valid IPv6 CIDR. It returns false if cidr is +// nil or an IPv4 CIDR. Its behavior is not defined if cidr is invalid. +func IsIPv6CIDR(cidr *net.IPNet) bool { + return IPFamilyOfCIDR(cidr) == IPv6 +} + +// IsIPv6CIDRString returns true if cidr contains a single IPv6 CIDR and nothing else. It +// returns false if cidr is an empty string, an IPv4 CIDR, or anything else that is not a +// single valid IPv6 CIDR. +func IsIPv6CIDRString(cidr string) bool { + return IPFamilyOfCIDRString(cidr) == IPv6 +} + +// IsIPv4 returns true if netIP is IPv4 (and false if it is IPv6, nil, or invalid). +func IsIPv4(netIP net.IP) bool { + return IPFamilyOf(netIP) == IPv4 +} + +// IsIPv4String returns true if ip contains a single IPv4 address and nothing else. It +// returns false if ip is an empty string, an IPv6 address, or anything else that is not a +// single IPv4 address. +func IsIPv4String(ip string) bool { + return IPFamilyOfString(ip) == IPv4 +} + +// IsIPv4CIDR returns true if cidr is a valid IPv4 CIDR. It returns false if cidr is nil +// or an IPv6 CIDR. Its behavior is not defined if cidr is invalid. +func IsIPv4CIDR(cidr *net.IPNet) bool { + return IPFamilyOfCIDR(cidr) == IPv4 +} + +// IsIPv4CIDRString returns true if cidr contains a single IPv4 CIDR and nothing else. It +// returns false if cidr is an empty string, an IPv6 CIDR, or anything else that is not a +// single valid IPv4 CIDR. +func IsIPv4CIDRString(cidr string) bool { + return IPFamilyOfCIDRString(cidr) == IPv4 +} diff --git a/net/v2/ipfamily_test.go b/net/v2/ipfamily_test.go new file mode 100644 index 00000000..13dd954d --- /dev/null +++ b/net/v2/ipfamily_test.go @@ -0,0 +1,332 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "fmt" + "net" + "testing" +) + +func TestDualStackIPs(t *testing.T) { + testCases := []struct { + desc string + ips []string + expectedResult bool + expectError bool + }{ + { + desc: "should fail because length is not at least 2", + ips: []string{"1.1.1.1"}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because length is not at least 2", + ips: []string{}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because all are v4", + ips: []string{"1.1.1.1", "2.2.2.2", "3.3.3.3"}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because all are v6", + ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff0", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff1"}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because 2nd ip is invalid", + ips: []string{"1.1.1.1", "not-a-valid-ip"}, + expectedResult: false, + expectError: true, + }, + { + desc: "should fail because 1st ip is invalid", + ips: []string{"not-a-valid-ip", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff"}, + expectedResult: false, + expectError: true, + }, + { + desc: "should fail despite dual-stack because 3rd ip is invalid", + ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "not-a-valid-ip"}, + expectedResult: false, + expectError: true, + }, + { + desc: "dual-stack ipv4-primary", + ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff"}, + expectedResult: true, + expectError: false, + }, + { + desc: "dual-stack, multiple ipv6", + ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff0"}, + expectedResult: true, + expectError: false, + }, + { + desc: "dual-stack, multiple ipv4", + ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "10.0.0.0"}, + expectedResult: true, + expectError: false, + }, + { + desc: "dual-stack, ipv6-primary", + ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "1.1.1.1"}, + expectedResult: true, + expectError: false, + }, + } + // for each test case, test the regular func and the string func + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + dualStack, err := IsDualStackIPStrings(tc.ips) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackIPStrings") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackIPStrings: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackIPStrings=%v, got %v", tc.expectedResult, dualStack) + } + + ips := make([]net.IP, 0, len(tc.ips)) + for _, ip := range tc.ips { + parsedIP := ParseIPSloppy(ip) + ips = append(ips, parsedIP) + } + dualStack, err = IsDualStackIPs(ips) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackIPs") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackIPs: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackIPs=%v, got %v", tc.expectedResult, dualStack) + } + }) + } +} + +func TestDualStackCIDRs(t *testing.T) { + testCases := []struct { + desc string + cidrs []string + expectedResult bool + expectError bool + }{ + { + desc: "should fail because length is not at least 2", + cidrs: []string{"10.10.10.10/8"}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because length is not at least 2", + cidrs: []string{}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because all cidrs are v4", + cidrs: []string{"10.10.10.10/8", "20.20.20.20/8", "30.30.30.30/8"}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because all cidrs are v6", + cidrs: []string{"2000::/10", "3000::/10"}, + expectedResult: false, + expectError: false, + }, + { + desc: "should fail because 2nd cidr is invalid", + cidrs: []string{"10.10.10.10/8", "not-a-valid-cidr"}, + expectedResult: false, + expectError: true, + }, + { + desc: "should fail because 1st cidr is invalid", + cidrs: []string{"not-a-valid-ip", "2000::/10"}, + expectedResult: false, + expectError: true, + }, + { + desc: "dual-stack, ipv4-primary", + cidrs: []string{"10.10.10.10/8", "2000::/10"}, + expectedResult: true, + expectError: false, + }, + { + desc: "dual-stack, ipv6-primary", + cidrs: []string{"2000::/10", "10.10.10.10/8"}, + expectedResult: true, + expectError: false, + }, + { + desc: "dual-stack, multiple IPv6", + cidrs: []string{"2000::/10", "10.10.10.10/8", "3000::/10"}, + expectedResult: true, + expectError: false, + }, + } + + // for each test case, test the regular func and the string func + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + dualStack, err := IsDualStackCIDRStrings(tc.cidrs) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackCIDRStrings") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackCIDRStrings: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackCIDRStrings=%v, got %v", tc.expectedResult, dualStack) + } + + cidrs := make([]*net.IPNet, 0, len(tc.cidrs)) + for _, cidr := range tc.cidrs { + _, parsedCIDR, _ := ParseCIDRSloppy(cidr) + cidrs = append(cidrs, parsedCIDR) + } + + dualStack, err = IsDualStackCIDRs(cidrs) + if err == nil && tc.expectError { + t.Fatalf("expected an error from IsDualStackCIDRs") + } + if err != nil && !tc.expectError { + t.Fatalf("unexpected error from IsDualStackCIDRs: %v", err) + } + if dualStack != tc.expectedResult { + t.Errorf("expected IsDualStackCIDRs=%v, got %v", tc.expectedResult, dualStack) + } + }) + } +} + +func checkOneIPFamily(t *testing.T, ip string, expectedFamily, family IPFamily, isIPv4, isIPv6 bool) { + t.Helper() + if family != expectedFamily { + t.Errorf("Expect %q family %q, got %q", ip, expectedFamily, family) + } + if isIPv4 != (expectedFamily == IPv4) { + t.Errorf("Expect %q ipv4 %v, got %v", ip, expectedFamily == IPv4, isIPv6) + } + if isIPv6 != (expectedFamily == IPv6) { + t.Errorf("Expect %q ipv6 %v, got %v", ip, expectedFamily == IPv6, isIPv6) + } +} + +func TestIPFamilyOf(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestIPs { + if tc.skipFamily { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for _, str := range tc.strings { + family := IPFamilyOfString(str) + isIPv4 := IsIPv4String(str) + isIPv6 := IsIPv6String(str) + checkOneIPFamily(t, str, tc.family, family, isIPv4, isIPv6) + } + for _, ip := range tc.ips { + family := IPFamilyOf(ip) + isIPv4 := IsIPv4(ip) + isIPv6 := IsIPv6(ip) + checkOneIPFamily(t, ip.String(), tc.family, family, isIPv4, isIPv6) + } + }) + } + + // See test cases in ips_test.go + for _, tc := range badTestIPs { + if tc.skipFamily { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for _, ip := range tc.ips { + family := IPFamilyOf(ip) + isIPv4 := IsIPv4(ip) + isIPv6 := IsIPv6(ip) + checkOneIPFamily(t, fmt.Sprintf("%#v", ip), IPFamilyUnknown, family, isIPv4, isIPv6) + } + for _, str := range tc.strings { + family := IPFamilyOfString(str) + isIPv4 := IsIPv4String(str) + isIPv6 := IsIPv6String(str) + checkOneIPFamily(t, str, IPFamilyUnknown, family, isIPv4, isIPv6) + } + }) + } +} + +func TestIPFamilyOfCIDR(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestCIDRs { + if tc.skipFamily { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for _, str := range tc.strings { + family := IPFamilyOfCIDRString(str) + isIPv4 := IsIPv4CIDRString(str) + isIPv6 := IsIPv6CIDRString(str) + checkOneIPFamily(t, str, tc.family, family, isIPv4, isIPv6) + } + for _, ipnet := range tc.ipnets { + family := IPFamilyOfCIDR(ipnet) + isIPv4 := IsIPv4CIDR(ipnet) + isIPv6 := IsIPv6CIDR(ipnet) + checkOneIPFamily(t, ipnet.String(), tc.family, family, isIPv4, isIPv6) + } + }) + } + + // See test cases in ips_test.go + for _, tc := range badTestCIDRs { + if tc.skipFamily { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for _, ipnet := range tc.ipnets { + family := IPFamilyOfCIDR(ipnet) + isIPv4 := IsIPv4CIDR(ipnet) + isIPv6 := IsIPv6CIDR(ipnet) + str := "" + if ipnet != nil { + str = fmt.Sprintf("%#v", *ipnet) + } + checkOneIPFamily(t, str, IPFamilyUnknown, family, isIPv4, isIPv6) + } + for _, str := range tc.strings { + family := IPFamilyOfCIDRString(str) + isIPv4 := IsIPv4CIDRString(str) + isIPv6 := IsIPv6CIDRString(str) + checkOneIPFamily(t, str, IPFamilyUnknown, family, isIPv4, isIPv6) + } + }) + } +} diff --git a/net/v2/ipnet.go b/net/v2/ipnet.go new file mode 100644 index 00000000..2f3ee37f --- /dev/null +++ b/net/v2/ipnet.go @@ -0,0 +1,221 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "fmt" + "net" + "strings" +) + +// IPNetSet maps string to net.IPNet. +type IPNetSet map[string]*net.IPNet + +// ParseIPNets parses string slice to IPNetSet. +func ParseIPNets(specs ...string) (IPNetSet, error) { + ipnetset := make(IPNetSet) + for _, spec := range specs { + spec = strings.TrimSpace(spec) + _, ipnet, err := ParseCIDRSloppy(spec) + if err != nil { + return nil, err + } + k := ipnet.String() // In case of normalization + ipnetset[k] = ipnet + } + return ipnetset, nil +} + +// Insert adds items to the set. +func (s IPNetSet) Insert(items ...*net.IPNet) { + for _, item := range items { + s[item.String()] = item + } +} + +// Delete removes all items from the set. +func (s IPNetSet) Delete(items ...*net.IPNet) { + for _, item := range items { + delete(s, item.String()) + } +} + +// Has returns true if and only if item is contained in the set. +func (s IPNetSet) Has(item *net.IPNet) bool { + _, contained := s[item.String()] + return contained +} + +// HasAll returns true if and only if all items are contained in the set. +func (s IPNetSet) HasAll(items ...*net.IPNet) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// Difference returns a set of objects that are not in s2 +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.Difference(s2) = {a3} +// s2.Difference(s1) = {a4, a5} +func (s IPNetSet) Difference(s2 IPNetSet) IPNetSet { + result := make(IPNetSet) + for k, i := range s { + _, found := s2[k] + if found { + continue + } + result[k] = i + } + return result +} + +// StringSlice returns a []string with the String representation of each element in the set. +// Order is undefined. +func (s IPNetSet) StringSlice() []string { + a := make([]string, 0, len(s)) + for k := range s { + a = append(a, k) + } + return a +} + +// IsSuperset returns true if and only if s1 is a superset of s2. +func (s IPNetSet) IsSuperset(s2 IPNetSet) bool { + for k := range s2 { + _, found := s[k] + if !found { + return false + } + } + return true +} + +// Equal returns true if and only if s1 is equal (as a set) to s2. +// Two sets are equal if their membership is identical. +// (In practice, this means same elements, order doesn't matter) +func (s IPNetSet) Equal(s2 IPNetSet) bool { + return len(s) == len(s2) && s.IsSuperset(s2) +} + +// Len returns the size of the set. +func (s IPNetSet) Len() int { + return len(s) +} + +// IPSet maps string to net.IP +type IPSet map[string]net.IP + +// ParseIPSet parses string slice to IPSet +func ParseIPSet(items ...string) (IPSet, error) { + ipset := make(IPSet) + for _, item := range items { + ip := ParseIPSloppy(strings.TrimSpace(item)) + if ip == nil { + return nil, fmt.Errorf("error parsing IP %q", item) + } + + ipset[ip.String()] = ip + } + + return ipset, nil +} + +// Insert adds items to the set. +func (s IPSet) Insert(items ...net.IP) { + for _, item := range items { + s[item.String()] = item + } +} + +// Delete removes all items from the set. +func (s IPSet) Delete(items ...net.IP) { + for _, item := range items { + delete(s, item.String()) + } +} + +// Has returns true if and only if item is contained in the set. +func (s IPSet) Has(item net.IP) bool { + _, contained := s[item.String()] + return contained +} + +// HasAll returns true if and only if all items are contained in the set. +func (s IPSet) HasAll(items ...net.IP) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// Difference returns a set of objects that are not in s2 +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.Difference(s2) = {a3} +// s2.Difference(s1) = {a4, a5} +func (s IPSet) Difference(s2 IPSet) IPSet { + result := make(IPSet) + for k, i := range s { + _, found := s2[k] + if found { + continue + } + result[k] = i + } + return result +} + +// StringSlice returns a []string with the String representation of each element in the set. +// Order is undefined. +func (s IPSet) StringSlice() []string { + a := make([]string, 0, len(s)) + for k := range s { + a = append(a, k) + } + return a +} + +// IsSuperset returns true if and only if s1 is a superset of s2. +func (s IPSet) IsSuperset(s2 IPSet) bool { + for k := range s2 { + _, found := s[k] + if !found { + return false + } + } + return true +} + +// Equal returns true if and only if s1 is equal (as a set) to s2. +// Two sets are equal if their membership is identical. +// (In practice, this means same elements, order doesn't matter) +func (s IPSet) Equal(s2 IPSet) bool { + return len(s) == len(s2) && s.IsSuperset(s2) +} + +// Len returns the size of the set. +func (s IPSet) Len() int { + return len(s) +} diff --git a/net/v2/ipnet_test.go b/net/v2/ipnet_test.go new file mode 100644 index 00000000..6d6e1b94 --- /dev/null +++ b/net/v2/ipnet_test.go @@ -0,0 +1,314 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "net" + "reflect" + "sort" + "testing" +) + +func parseIPNet(s string) *net.IPNet { + _, net, err := ParseCIDRSloppy(s) + if err != nil { + panic(err) + } + return net +} + +func TestIPNets(t *testing.T) { + s := IPNetSet{} + s2 := IPNetSet{} + if len(s) != 0 { + t.Errorf("Expected len=0: %d", len(s)) + } + a := parseIPNet("1.0.0.0/8") + b := parseIPNet("2.0.0.0/8") + c := parseIPNet("3.0.0.0/8") + d := parseIPNet("4.0.0.0/8") + + s.Insert(a, b) + if len(s) != 2 { + t.Errorf("Expected len=2: %d", len(s)) + } + s.Insert(c) + if s.Has(d) { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.Has(a) { + t.Errorf("Missing contents: %#v", s) + } + s.Delete(a) + if s.Has(a) { + t.Errorf("Unexpected contents: %#v", s) + } + s.Insert(a) + if s.HasAll(a, b, d) { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.HasAll(a, b) { + t.Errorf("Missing contents: %#v", s) + } + s2.Insert(a, b, d) + if s.IsSuperset(s2) { + t.Errorf("Unexpected contents: %#v", s) + } + s2.Delete(d) + if !s.IsSuperset(s2) { + t.Errorf("Missing contents: %#v", s) + } +} + +func TestIPNetSetDeleteMultiples(t *testing.T) { + s := IPNetSet{} + a := parseIPNet("1.0.0.0/8") + b := parseIPNet("2.0.0.0/8") + c := parseIPNet("3.0.0.0/8") + + s.Insert(a, b, c) + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + + s.Delete(a, c) + if len(s) != 1 { + t.Errorf("Expected len=1: %d", len(s)) + } + if s.Has(a) { + t.Errorf("Unexpected contents: %#v", s) + } + if s.Has(c) { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.Has(b) { + t.Errorf("Missing contents: %#v", s) + } +} + +func TestNewIPNetSet(t *testing.T) { + s, err := ParseIPNets("1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8") + if err != nil { + t.Errorf("error parsing IPNets: %v", err) + } + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + a := parseIPNet("1.0.0.0/8") + b := parseIPNet("2.0.0.0/8") + c := parseIPNet("3.0.0.0/8") + + if !s.Has(a) || !s.Has(b) || !s.Has(c) { + t.Errorf("Unexpected contents: %#v", s) + } +} + +func TestIPNetSetDifference(t *testing.T) { + l, err := ParseIPNets("1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8") + if err != nil { + t.Errorf("error parsing IPNets: %v", err) + } + r, err := ParseIPNets("1.0.0.0/8", "2.0.0.0/8", "4.0.0.0/8", "5.0.0.0/8") + if err != nil { + t.Errorf("error parsing IPNets: %v", err) + } + c := l.Difference(r) + d := r.Difference(l) + if len(c) != 1 { + t.Errorf("Expected len=1: %d", len(c)) + } + if !c.Has(parseIPNet("3.0.0.0/8")) { + t.Errorf("Unexpected contents: %#v", c) + } + if len(d) != 2 { + t.Errorf("Expected len=2: %d", len(d)) + } + if !d.Has(parseIPNet("4.0.0.0/8")) || !d.Has(parseIPNet("5.0.0.0/8")) { + t.Errorf("Unexpected contents: %#v", d) + } +} + +func TestIPNetSetList(t *testing.T) { + s, err := ParseIPNets("3.0.0.0/8", "1.0.0.0/8", "2.0.0.0/8") + if err != nil { + t.Errorf("error parsing IPNets: %v", err) + } + l := s.StringSlice() + sort.Strings(l) + if !reflect.DeepEqual(l, []string{"1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8"}) { + t.Errorf("List gave unexpected result: %#v", l) + } +} + +func TestIPSet(t *testing.T) { + s := IPSet{} + s2 := IPSet{} + + a := ParseIPSloppy("1.0.0.0") + b := ParseIPSloppy("2.0.0.0") + c := ParseIPSloppy("3.0.0.0") + d := ParseIPSloppy("4.0.0.0") + + s.Insert(a, b) + if len(s) != 2 { + t.Errorf("Expected len=2: %d", len(s)) + } + if !s.Has(a) { + t.Errorf("Missing contents: %#v", s) + } + + s.Insert(c) + if s.Has(d) { + t.Errorf("Unexpected contents: %#v", s) + } + + s.Delete(a) + if s.Has(a) { + t.Errorf("Unexpected contents: %#v", s) + } + s.Insert(a) + if s.HasAll(a, b, d) { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.HasAll(a, b) { + t.Errorf("Missing contents: %#v", s) + } + s2.Insert(a, b, d) + if s.IsSuperset(s2) { + t.Errorf("Unexpected contents: %#v", s) + } + s2.Delete(d) + if !s.IsSuperset(s2) { + t.Errorf("Missing contents: %#v", s) + } +} + +func TestIPSetDeleteMultiples(t *testing.T) { + s := IPSet{} + a := ParseIPSloppy("1.0.0.0") + b := ParseIPSloppy("2.0.0.0") + c := ParseIPSloppy("3.0.0.0") + + s.Insert(a, b, c) + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + + s.Delete(a, c) + if len(s) != 1 { + t.Errorf("Expected len=1: %d", len(s)) + } + if s.Has(a) { + t.Errorf("Unexpected contents: %#v", s) + } + if s.Has(c) { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.Has(b) { + t.Errorf("Missing contents: %#v", s) + } +} + +func TestParseIPSet(t *testing.T) { + s, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0", "::ffff:4.0.0.0") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + if len(s) != 4 { + t.Errorf("Expected len=3: %d", len(s)) + } + a := ParseIPSloppy("1.0.0.0") + b := ParseIPSloppy("2.0.0.0") + c := ParseIPSloppy("3.0.0.0") + d := ParseIPSloppy("::ffff:4.0.0.0") + e := ParseIPSloppy("4.0.0.0") + + if !s.Has(a) || !s.Has(b) || !s.Has(c) || !s.Has(d) || !s.Has(e) { + t.Errorf("Unexpected contents: %#v", s) + } +} + +func TestIPSetDifference(t *testing.T) { + l, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + r, err := ParseIPSet("1.0.0.0", "2.0.0.0", "4.0.0.0", "5.0.0.0") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + c := l.Difference(r) + d := r.Difference(l) + if len(c) != 1 { + t.Errorf("Expected len=1: %d", len(c)) + } + if !c.Has(ParseIPSloppy("3.0.0.0")) { + t.Errorf("Unexpected contents: %#v", c) + } + if len(d) != 2 { + t.Errorf("Expected len=2: %d", len(d)) + } + if !d.Has(ParseIPSloppy("4.0.0.0")) || !d.Has(ParseIPSloppy("5.0.0.0")) { + t.Errorf("Unexpected contents: %#v", d) + } +} + +func TestIPSetList(t *testing.T) { + // NOTE: IPv4-in-IPv6 addresses are represented as IPv4 in its string value + s, err := ParseIPSet("3.0.0.0", "1.0.0.0", "2.0.0.0", "::ffff:1.2.3.4") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + + l := s.StringSlice() + sort.Strings(l) + if !reflect.DeepEqual(l, []string{"1.0.0.0", "1.2.3.4", "2.0.0.0", "3.0.0.0"}) { + t.Errorf("List gave unexpected result: %#v", l) + } +} + +func TestIPSetEqual(t *testing.T) { + // IPv4-in-IPv6 addresses are equal to their IPv4 equivalents + set1, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0", "::ffff:4.0.0.0") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + + set2, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0", "4.0.0.0") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + + if !set1.Equal(set2) { + t.Errorf("sets %v and %v are not equal", set1, set2) + } + + // order shouldn't matter + set1, err = ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + + set2, err = ParseIPSet("3.0.0.0", "1.0.0.0", "2.0.0.0") + if err != nil { + t.Errorf("error parsing IPSet: %v", err) + } + + if !set1.Equal(set2) { + t.Errorf("sets %v and %v are not equal", set1, set2) + } +} diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go new file mode 100644 index 00000000..1a267d99 --- /dev/null +++ b/net/v2/ips_test.go @@ -0,0 +1,579 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "net" + "testing" +) + +// testIP represents a set of equivalent IP address representations. +type testIP struct { + desc string + family IPFamily + strings []string + ips []net.IP + + skipFamily bool + skipParse bool +} + +// goodTestIPs are "good" test IP values. For each item: +// +// Preconditions (not involving functions in netutils): +// - Each element of .ips is the same (i.e., .Equal()). +// - Each element of .ips stringifies to .strings[0]. +// +// IPFamily tests (unless `skipFamily: true`): +// - Each element of .strings should be identified as .family. +// - Each element of .ips should be identified as .family. +// +// Parsing tests (unless `skipParse: true`): +// - Each element of .strings should parse to a value equal to .ips[0]. +var goodTestIPs = []testIP{ + { + desc: "IPv4", + family: IPv4, + strings: []string{ + "192.168.0.5", + "192.168.000.005", + }, + ips: []net.IP{ + net.IPv4(192, 168, 0, 5), + {192, 168, 0, 5}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 192, 168, 0, 5}, + net.ParseIP("192.168.0.5"), + func() net.IP { ip, _, _ := net.ParseCIDR("192.168.0.5/24"); return ip }(), + func() net.IP { _, ipnet, _ := net.ParseCIDR("192.168.0.5/32"); return ipnet.IP }(), + }, + }, + { + desc: "IPv4 all-zeros", + family: IPv4, + strings: []string{ + "0.0.0.0", + "000.000.000.000", + }, + ips: []net.IP{ + net.IPv4zero, + net.IPv4(0, 0, 0, 0), + {0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0, 0, 0, 0}, + net.ParseIP("0.0.0.0"), + }, + }, + { + desc: "IPv4 broadcast", + family: IPv4, + strings: []string{ + "255.255.255.255", + }, + ips: []net.IP{ + net.IPv4bcast, + net.IPv4(255, 255, 255, 255), + {255, 255, 255, 255}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 255, 255, 255, 255}, + net.ParseIP("255.255.255.255"), + // A /32 IPMask is equivalent to 255.255.255.255 + func() net.IP { _, ipnet, _ := net.ParseCIDR("1.2.3.4/32"); return net.IP(ipnet.Mask) }(), + }, + }, + { + desc: "IPv6", + family: IPv6, + strings: []string{ + "2001:db8::5", + "2001:0db8::0005", + "2001:DB8::5", + }, + ips: []net.IP{ + {0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x05}, + net.ParseIP("2001:db8::5"), + func() net.IP { ip, _, _ := net.ParseCIDR("2001:db8::5/64"); return ip }(), + func() net.IP { _, ipnet, _ := net.ParseCIDR("2001:db8::5/128"); return ipnet.IP }(), + }, + }, + { + desc: "IPv6 all-zeros", + family: IPv6, + strings: []string{ + "::", + "0:0:0:0:0:0:0:0", + "0000:0000:0000:0000:0000:0000:0000:0000", + "0::0", + }, + ips: []net.IP{ + net.IPv6zero, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + net.ParseIP("::"), + // ::/0 has an IP, network base IP, and Mask that are all + // equivalent to :: + func() net.IP { ip, _, _ := net.ParseCIDR("::/0"); return ip }(), + func() net.IP { _, ipnet, _ := net.ParseCIDR("::/0"); return ipnet.IP }(), + func() net.IP { _, ipnet, _ := net.ParseCIDR("::/0"); return net.IP(ipnet.Mask) }(), + }, + }, + { + desc: "IPv6 loopback", + family: IPv6, + strings: []string{ + "::1", + "0000:0000:0000:0000:0000:0000:0000:0001", + }, + ips: []net.IP{ + net.IPv6loopback, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + net.ParseIP("::1"), + }, + }, + { + desc: "IPv4-mapped IPv6", + // net.IP can represent an IPv4 address internally as either a 4-byte + // value or a 16-byte value, but it treats the two forms as equivalent. + // + // This test case confirms that: + // - The 4-byte and 16-byte forms of a given net.IP compare as .Equal(). + // - Our parsers parse the plain IPv4 and IPv4-mapped IPv6 forms of an + // IPv4 string to the same thing. + // - The 4-byte and 16-byte forms of a given net.IP all stringify + // to the plain IPv4 string form (i.e., .strings[0]). + family: IPv4, + strings: []string{ + "192.168.0.5", + "::ffff:192.168.0.5", + "::ffff:0192.0168.0000.0005", + }, + ips: []net.IP{ + {192, 168, 0, 5}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 192, 168, 0, 5}, + net.IPv4(192, 168, 0, 5).To4(), + net.IPv4(192, 168, 0, 5).To16(), + net.ParseIP("192.168.0.5").To4(), + net.ParseIP("192.168.0.5").To16(), + net.ParseIP("::ffff:192.168.0.5").To4(), + net.ParseIP("::ffff:192.168.0.5").To16(), + }, + }, +} + +// badTestIPs are bad test IP values. For each item: +// +// IPFamily tests (unless `skipFamily: true`): +// - Each element of .strings should be identified as IPFamilyUnknown. +// - Each element of .ips should be identified as IPFamilyUnknown. +// +// Parsing tests (unless `skipParse: true`): +// - Each element of .strings should fail to parse. +// - Each element of .ips should stringify to an error value that fails to parse. +var badTestIPs = []testIP{ + { + desc: "empty string is not an IP", + strings: []string{ + "", + }, + }, + { + desc: "random non-IP string is not an IP", + strings: []string{ + "bad ip", + }, + }, + { + desc: "domain name is not an IP", + strings: []string{ + "www.example.com", + }, + }, + { + desc: "mangled IPv4 addresses are invalid", + strings: []string{ + "1.2.3.400", + "1.2..4", + "1.2.3", + "1.2.3.4.5", + }, + }, + { + desc: "mangled IPv6 addresses are invalid", + strings: []string{ + "1:2::12345", + "1::2::3", + "1:2:::3", + "1:2:3", + "1:2:3:4:5:6:7:8:9", + "1:2:3:4::6:7:8:9", + }, + }, + { + desc: "IPs do not have ports or brackets", + strings: []string{ + "1.2.3.4:80", + "[2001:db8::5]", + "[2001:db8::5]:80", + "www.example.com:80", + }, + }, + { + desc: "IPs with zones are invalid", + strings: []string{ + "169.254.169.254%eth0", + "fe80::1234%eth0", + }, + }, + { + desc: "CIDR strings are not IPs", + strings: []string{ + "1.2.3.0/24", + "2001:db8::/64", + }, + }, + { + desc: "IPs with whitespace are invalid", + strings: []string{ + " 1.2.3.4", + "1.2.3.4 ", + " 2001:db8::5", + "2001:db8::5 ", + }, + }, + { + desc: "nil is an invalid net.IP", + ips: []net.IP{ + nil, + }, + }, + { + desc: "a byte slice of length other than 4 or 16 is an invalid net.IP", + ips: []net.IP{ + {}, + {1, 2, 3}, + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}, + }, + }, +} + +// testCIDR represents a set of equivalent CIDR representations. +type testCIDR struct { + desc string + family IPFamily + strings []string + ipnets []*net.IPNet + + skipFamily bool + skipParse bool +} + +// goodTestCIDRs are "good" test CIDR values. For each item: +// +// Preconditions: +// - Each element of .ipnets stringifies to .strings[0]. +// +// IPFamily tests (unless `skipFamily: true`): +// - Each element of .strings should be identified as .family. +// - Each element of .ipnets should be identified as .family. +// +// Parsing tests (unless `skipParse: true`): +// - Each element of .strings should parse to a value "equal" to .ipnets[0]. +// +// (Unlike net.IP, *net.IPNet has no `.Equal()` method, and testing equality "by hand" is +// complicated (there are 4 equivalent representations of every IPv4 CIDR value), so we +// just consider two *net.IPNet values to be equal if they stringify to the same value.) +var goodTestCIDRs = []testCIDR{ + { + desc: "IPv4", + family: IPv4, + strings: []string{ + "1.2.3.0/24", + }, + ipnets: []*net.IPNet{ + {IP: net.IPv4(1, 2, 3, 0), Mask: net.CIDRMask(24, 32)}, + {IP: net.ParseIP("1.2.3.0"), Mask: net.CIDRMask(24, 32)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.2.3.0/24"); return ipnet }(), + }, + }, + { + desc: "IPv4, single IP", + family: IPv4, + strings: []string{ + "1.1.1.1/32", + }, + ipnets: []*net.IPNet{ + {IP: net.IPv4(1, 1, 1, 1), Mask: net.CIDRMask(32, 32)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.1.1.1/32"); return ipnet }(), + }, + }, + { + desc: "IPv4, all IPs", + family: IPv4, + strings: []string{ + "0.0.0.0/0", + "000.000.000.000/000", + }, + ipnets: []*net.IPNet{ + {IP: net.IPv4zero.To4(), Mask: net.IPMask(net.IPv4zero.To4())}, + {IP: net.IPv4(0, 0, 0, 0), Mask: net.CIDRMask(0, 32)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("0.0.0.0/0"); return ipnet }(), + }, + }, + { + desc: "IPv4 ifaddr (masked)", + // This tests that if you try to parse an "ifaddr-style" CIDR string with + // ParseCIDR/ParseCIDRSloppy, the *net.IPNet return value has the bits + // beyond the prefix length masked out. + family: IPv4, + strings: []string{ + "1.2.3.0/24", + "1.2.3.4/24", + "1.2.3.255/24", + }, + ipnets: []*net.IPNet{ + {IP: net.IPv4(1, 2, 3, 0), Mask: net.CIDRMask(24, 32)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.2.3.0/24"); return ipnet }(), + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.2.3.4/24"); return ipnet }(), + }, + }, + { + desc: "IPv4 ifaddr", + family: IPv4, + strings: []string{ + "1.2.3.4/24", + }, + ipnets: []*net.IPNet{ + {IP: net.IPv4(1, 2, 3, 4), Mask: net.CIDRMask(24, 32)}, + }, + + // The *net.IPNet return value of ParseCIDRSloppy() masks out the lower + // bits, so the parsed version won't compare equal to .ipnets[0] + skipParse: true, + }, + { + desc: "IPv6", + family: IPv6, + strings: []string{ + "2001:db8::/64", + "2001:db8:0:0:0:0:0:0/64", + "2001:DB8::/64", + }, + ipnets: []*net.IPNet{ + {IP: net.IP{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.IPMask{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}, + {IP: net.ParseIP("2001:db8::"), Mask: net.CIDRMask(64, 128)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("2001:db8::/64"); return ipnet }(), + }, + }, + { + desc: "IPv6, all IPs", + family: IPv6, + strings: []string{ + "::/0", + }, + ipnets: []*net.IPNet{ + {IP: net.IPv6zero, Mask: net.IPMask(net.IPv6zero)}, + {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.CIDRMask(0, 128)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("::/0"); return ipnet }(), + }, + }, + { + desc: "IPv6, single IP", + family: IPv6, + strings: []string{ + "::1/128", + }, + ipnets: []*net.IPNet{ + {IP: net.IPv6loopback, Mask: net.CIDRMask(128, 128)}, + {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}, Mask: net.CIDRMask(128, 128)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("::1/128"); return ipnet }(), + }, + }, + { + desc: "IPv6 ifaddr (masked)", + // This tests that if you try to parse an "ifaddr-style" CIDR string with + // ParseCIDRSloppy, the *net.IPNet return value has the bits beyond the + // prefix length masked out. + family: IPv6, + strings: []string{ + "2001:db8::/64", + "2001:db8::1/64", + "2001:db8::f00f:f0f0:0f0f:000f/64", + }, + ipnets: []*net.IPNet{ + {IP: net.IP{0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.CIDRMask(64, 128)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("2001:db8::/64"); return ipnet }(), + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("2001:db8::1/64"); return ipnet }(), + }, + }, + { + desc: "IPv6 ifaddr", + family: IPv6, + strings: []string{ + "2001:db8::1/64", + }, + ipnets: []*net.IPNet{ + {IP: net.IP{0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Mask: net.CIDRMask(64, 128)}, + }, + + // The *net.IPNet return value of ParseCIDRSloppy() masks out the lower + // bits, so the parsed version won't compare equal to .ipnets[0] + skipParse: true, + }, + { + desc: "IPv4-mapped IPv6", + // As in the IP tests, confirm that plain IPv4 and IPv4-mapped IPv6 are + // treated as equivalent. + family: IPv4, + strings: []string{ + "1.1.1.0/24", + "::ffff:1.1.1.0/120", + "::ffff:01.01.01.00/0120", + }, + ipnets: []*net.IPNet{ + {IP: net.IP{1, 1, 1, 0}, Mask: net.CIDRMask(24, 32)}, + {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 1, 1, 0}, Mask: net.CIDRMask(120, 128)}, + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.1.1.0/24"); return ipnet }(), + func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("::ffff:1.1.1.0/120"); return ipnet }(), + + // Explicitly test each of the 4 different combinations of 4-byte + // or 16-byte IP and 4-byte or 16-byte Mask, all of which should + // compare as equal and re-stringify to "1.1.1.0/24". + {IP: net.IP{1, 1, 1, 0}, Mask: net.IPMask{255, 255, 255, 0}}, + {IP: net.IP{1, 1, 1, 0}, Mask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0}}, + {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 1, 1, 0}, Mask: net.IPMask{255, 255, 255, 0}}, + {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 1, 1, 0}, Mask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0}}, + }, + }, +} + +// badTestCIDRs are bad test CIDR values. For each item: +// +// IPFamily tests (unless `skipFamily: true`): +// - Each element of .strings should be identified as IPFamilyUnknown. +// - Each element of .ipnets should be identified as IPFamilyUnknown. +// +// Parsing tests (unless `skipParse: true`): +// - Each element of .strings should fail to parse. +// - Each element of .ipnets should stringify to some error value that fails to parse. +var badTestCIDRs = []testCIDR{ + { + desc: "empty string is not a CIDR", + strings: []string{ + "", + }, + }, + { + desc: "random unparseable string is not a CIDR", + strings: []string{ + "bad cidr", + }, + }, + { + desc: "CIDR with invalid IP is invalid", + strings: []string{ + "1.2.300.0/24", + "2001:db8000::/64", + }, + }, + { + desc: "CIDR with invalid prefix length is invalid", + strings: []string{ + "1.2.3.4/64", + "2001:db8::5/192", + "1.2.3.0/-8", + "1.2.3.0/+24", + }, + }, + { + desc: "URLs (that aren't also valid CIDRs) are invalid", + strings: []string{ + "www.example.com/24", + "192.168.0.1/0/99", + }, + }, + { + desc: "plain IP is not a CIDR", + strings: []string{ + "1.2.3.4", + "2001:db8::1", + }, + }, + { + desc: "CIDR with whitespace is invalid", + strings: []string{ + " 1.2.3.0/24", + "1.2.3.0/24 ", + }, + }, + { + desc: "nil is an invalid IPNet", + ipnets: []*net.IPNet{ + nil, + }, + }, + { + desc: "IPNet containing invalid IP is invalid", + ipnets: []*net.IPNet{ + {IP: net.IP{0x1}, Mask: net.CIDRMask(24, 32)}, + }, + }, + { + desc: "IPNet containing non-CIDR Mask is invalid", + ipnets: []*net.IPNet{ + {IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 0, 255, 0}}, + }, + + // IPFamilyOfCIDR only looks at IP and doesn't notice that Mask is invalid + skipFamily: true, + }, + { + desc: "IPNet containing IPv6 IP and IPv4 Mask is invalid", + ipnets: []*net.IPNet{ + {IP: net.IP{0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.CIDRMask(24, 32)}, + }, + + // IPFamilyOfCIDR only looks at IP and doesn't notice that Mask is invalid + skipFamily: true, + }, +} + +// TestGoodTestIPs confirms the Preconditions for goodTestIPs. +func TestGoodTestIPs(t *testing.T) { + for _, tc := range goodTestIPs { + t.Run(tc.desc, func(t *testing.T) { + for i, ip := range tc.ips { + if !ip.Equal(tc.ips[0]) { + t.Errorf("BAD TEST DATA: IP %d %#v %q does not equal %#v %q", i+1, ip, ip, tc.ips[0], tc.ips[0]) + } + str := ip.String() + if str != tc.strings[0] { + t.Errorf("BAD TEST DATA: IP %d %#v %q does not stringify to %q", i+1, ip, ip, tc.strings[0]) + } + } + }) + } +} + +// TestGoodTestCIDRs confirms the Preconditions for goodTestCIDRs. +func TestGoodTestCIDRs(t *testing.T) { + for _, tc := range goodTestCIDRs { + t.Run(tc.desc, func(t *testing.T) { + for i, ipnet := range tc.ipnets { + if ipnet.String() != tc.strings[0] { + t.Errorf("BAD TEST DATA: IPNet %d %#v %q does not stringify to %q", i+1, ipnet, ipnet, tc.strings[0]) + } + } + }) + } +} diff --git a/net/v2/multi_listen.go b/net/v2/multi_listen.go new file mode 100644 index 00000000..e5d50805 --- /dev/null +++ b/net/v2/multi_listen.go @@ -0,0 +1,195 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "context" + "fmt" + "net" + "sync" + "sync/atomic" +) + +// connErrPair pairs conn and error which is returned by accept on sub-listeners. +type connErrPair struct { + conn net.Conn + err error +} + +// multiListener implements net.Listener +type multiListener struct { + listeners []net.Listener + wg sync.WaitGroup + + // connCh passes accepted connections, from child listeners to parent. + connCh chan connErrPair + // stopCh communicates from parent to child listeners. + stopCh chan struct{} + closed atomic.Bool +} + +// compile time check to ensure *multiListener implements net.Listener +var _ net.Listener = &multiListener{} + +// MultiListen returns net.Listener which can listen on and accept connections for +// the given network on multiple addresses. Internally it uses stdlib to create +// sub-listener and multiplexes connection requests using go-routines. +// The network must be "tcp", "tcp4" or "tcp6". +// It follows the semantics of net.Listen that primarily means: +// 1. If the host is an unspecified/zero IP address with "tcp" network, MultiListen +// listens on all available unicast and anycast IP addresses of the local system. +// 2. Use "tcp4" or "tcp6" to exclusively listen on IPv4 or IPv6 family, respectively. +// 3. The host can accept names (e.g, localhost) and it will create a listener for at +// most one of the host's IP. +func MultiListen(ctx context.Context, network string, addrs ...string) (net.Listener, error) { + var lc net.ListenConfig + return multiListen( + ctx, + network, + addrs, + func(ctx context.Context, network, address string) (net.Listener, error) { + return lc.Listen(ctx, network, address) + }) +} + +// multiListen implements MultiListen by consuming stdlib functions as dependency allowing +// mocking for unit-testing. +func multiListen( + ctx context.Context, + network string, + addrs []string, + listenFunc func(ctx context.Context, network, address string) (net.Listener, error), +) (net.Listener, error) { + if !(network == "tcp" || network == "tcp4" || network == "tcp6") { + return nil, fmt.Errorf("network %q not supported", network) + } + if len(addrs) == 0 { + return nil, fmt.Errorf("no address provided to listen on") + } + + ml := &multiListener{ + connCh: make(chan connErrPair), + stopCh: make(chan struct{}), + } + for _, addr := range addrs { + l, err := listenFunc(ctx, network, addr) + if err != nil { + // close all the sub-listeners and exit + _ = ml.Close() + return nil, err + } + ml.listeners = append(ml.listeners, l) + } + + for _, l := range ml.listeners { + ml.wg.Add(1) + go func(l net.Listener) { + defer ml.wg.Done() + for { + // Accept() is blocking, unless ml.Close() is called, in which + // case it will return immediately with an error. + conn, err := l.Accept() + // This assumes that ANY error from Accept() will terminate the + // sub-listener. We could maybe be more precise, but it + // doesn't seem necessary. + terminate := err != nil + + select { + case ml.connCh <- connErrPair{conn: conn, err: err}: + case <-ml.stopCh: + // In case we accepted a connection AND were stopped, and + // this select-case was chosen, just throw away the + // connection. This avoids potentially blocking on connCh + // or leaking a connection. + if conn != nil { + _ = conn.Close() + } + terminate = true + } + // Make sure we don't loop on Accept() returning an error and + // the select choosing the channel case. + if terminate { + return + } + } + }(l) + } + return ml, nil +} + +// Accept implements net.Listener. It waits for and returns a connection from +// any of the sub-listener. +func (ml *multiListener) Accept() (net.Conn, error) { + // wait for any sub-listener to enqueue an accepted connection + connErr, ok := <-ml.connCh + if !ok { + // The channel will be closed only when Close() is called on the + // multiListener. Closing of this channel implies that all + // sub-listeners are also closed, which causes a "use of closed + // network connection" error on their Accept() calls. We return the + // same error for multiListener.Accept() if multiListener.Close() + // has already been called. + return nil, fmt.Errorf("use of closed network connection") + } + return connErr.conn, connErr.err +} + +// Close implements net.Listener. It will close all sub-listeners and wait for +// the go-routines to exit. +func (ml *multiListener) Close() error { + // Make sure this can be called repeatedly without explosions. + if !ml.closed.CompareAndSwap(false, true) { + return fmt.Errorf("use of closed network connection") + } + + // Tell all sub-listeners to stop. + close(ml.stopCh) + + // Closing the listeners causes Accept() to immediately return an error in + // the sub-listener go-routines. + for _, l := range ml.listeners { + _ = l.Close() + } + + // Wait for all the sub-listener go-routines to exit. + ml.wg.Wait() + close(ml.connCh) + + // Drain any already-queued connections. + for connErr := range ml.connCh { + if connErr.conn != nil { + _ = connErr.conn.Close() + } + } + return nil +} + +// Addr is an implementation of the net.Listener interface. It always returns +// the address of the first listener. Callers should use conn.LocalAddr() to +// obtain the actual local address of the sub-listener. +func (ml *multiListener) Addr() net.Addr { + return ml.listeners[0].Addr() +} + +// Addrs is like Addr, but returns the address for all registered listeners. +func (ml *multiListener) Addrs() []net.Addr { + var ret []net.Addr + for _, l := range ml.listeners { + ret = append(ret, l.Addr()) + } + return ret +} diff --git a/net/v2/multi_listen_test.go b/net/v2/multi_listen_test.go new file mode 100644 index 00000000..9a10feb7 --- /dev/null +++ b/net/v2/multi_listen_test.go @@ -0,0 +1,545 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "strconv" + "sync/atomic" + "testing" + "time" +) + +type fakeCon struct { + remoteAddr net.Addr +} + +func (f *fakeCon) Read(_ []byte) (n int, err error) { + return 0, nil +} + +func (f *fakeCon) Write(_ []byte) (n int, err error) { + return 0, nil +} + +func (f *fakeCon) Close() error { + return nil +} + +func (f *fakeCon) LocalAddr() net.Addr { + return nil +} + +func (f *fakeCon) RemoteAddr() net.Addr { + return f.remoteAddr +} + +func (f *fakeCon) SetDeadline(_ time.Time) error { + return nil +} + +func (f *fakeCon) SetReadDeadline(_ time.Time) error { + return nil +} + +func (f *fakeCon) SetWriteDeadline(_ time.Time) error { + return nil +} + +var _ net.Conn = &fakeCon{} + +type fakeListener struct { + addr net.Addr + index int + err error + closed atomic.Bool + connErrPairs []connErrPair +} + +func (f *fakeListener) Accept() (net.Conn, error) { + if f.index < len(f.connErrPairs) { + index := f.index + connErr := f.connErrPairs[index] + f.index++ + return connErr.conn, connErr.err + } + for { + if f.closed.Load() { + return nil, fmt.Errorf("use of closed network connection") + } + } +} + +func (f *fakeListener) Close() error { + f.closed.Store(true) + return nil +} + +func (f *fakeListener) Addr() net.Addr { + return f.addr +} + +var _ net.Listener = &fakeListener{} + +func listenFuncFactory(listeners []*fakeListener) func(_ context.Context, network string, address string) (net.Listener, error) { + index := 0 + return func(_ context.Context, network string, address string) (net.Listener, error) { + if index < len(listeners) { + host, portStr, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, err + } + listener := listeners[index] + addr := &net.TCPAddr{ + IP: ParseIPSloppy(host), + Port: port, + } + if err != nil { + return nil, err + } + listener.addr = addr + index++ + + if listener.err != nil { + return nil, listener.err + } + return listener, nil + } + return nil, nil + } +} + +func TestMultiListen(t *testing.T) { + testCases := []struct { + name string + network string + addrs []string + fakeListeners []*fakeListener + errString string + }{ + { + name: "unsupported network", + network: "udp", + errString: "network \"udp\" not supported", + }, + { + name: "no host", + network: "tcp", + errString: "no address provided to listen on", + }, + { + name: "valid", + network: "tcp", + addrs: []string{"127.0.0.1:12345"}, + fakeListeners: []*fakeListener{{connErrPairs: []connErrPair{}}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.TODO() + ml, err := multiListen(ctx, tc.network, tc.addrs, listenFuncFactory(tc.fakeListeners)) + + if tc.errString != "" { + assertError(t, tc.errString, err) + } else { + assertNoError(t, err) + } + if ml != nil { + err = ml.Close() + if err != nil { + t.Errorf("Did not expect error: %v", err) + } + } + }) + } +} + +func TestMultiListen_Addr(t *testing.T) { + ctx := context.TODO() + ml, err := multiListen(ctx, "tcp", []string{"10.10.10.10:5000", "192.168.1.10:5000", "127.0.0.1:5000"}, listenFuncFactory( + []*fakeListener{{}, {}, {}}, + )) + if err != nil { + t.Errorf("Did not expect error: %v", err) + } + + if ml.Addr().String() != "10.10.10.10:5000" { + t.Errorf("Expected '10.10.10.10:5000' but got '%s'", ml.Addr().String()) + } + + err = ml.Close() + if err != nil { + t.Errorf("Did not expect error: %v", err) + } +} + +func TestMultiListen_Addrs(t *testing.T) { + ctx := context.TODO() + addrs := []string{"10.10.10.10:5000", "192.168.1.10:5000", "127.0.0.1:5000"} + ml, err := multiListen(ctx, "tcp", addrs, listenFuncFactory( + []*fakeListener{{}, {}, {}}, + )) + if err != nil { + t.Errorf("Did not expect error: %v", err) + } + + gotAddrs := ml.(*multiListener).Addrs() + for i := range gotAddrs { + if gotAddrs[i].String() != addrs[i] { + t.Errorf("expected %q; got %q", addrs[i], gotAddrs[i].String()) + } + + } + + err = ml.Close() + if err != nil { + t.Errorf("Did not expect error: %v", err) + } +} + +func TestMultiListen_Close(t *testing.T) { + testCases := []struct { + name string + addrs []string + runner func(listener net.Listener, acceptCalls int) error + fakeListeners []*fakeListener + acceptCalls int + errString string + }{ + { + name: "close", + addrs: []string{"10.10.10.10:5000", "192.168.1.10:5000", "127.0.0.1:5000"}, + runner: func(ml net.Listener, acceptCalls int) error { + for i := 0; i < acceptCalls; i++ { + _, err := ml.Accept() + if err != nil { + return err + } + } + err := ml.Close() + if err != nil { + return err + } + return nil + }, + fakeListeners: []*fakeListener{{}, {}, {}}, + }, + { + name: "close with pending connections", + addrs: []string{"10.10.10.10:5001", "192.168.1.10:5002", "127.0.0.1:5003"}, + runner: func(ml net.Listener, acceptCalls int) error { + for i := 0; i < acceptCalls; i++ { + _, err := ml.Accept() + if err != nil { + return err + } + } + err := ml.Close() + if err != nil { + return err + } + return nil + }, + fakeListeners: []*fakeListener{{ + connErrPairs: []connErrPair{{ + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}, + }}}, { + connErrPairs: []connErrPair{{ + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}, + }, + }}, { + connErrPairs: []connErrPair{{ + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}, + }}, + }}, + }, + { + name: "close with no pending connections", + addrs: []string{"10.10.10.10:3001", "192.168.1.10:3002", "127.0.0.1:3003"}, + runner: func(ml net.Listener, acceptCalls int) error { + for i := 0; i < acceptCalls; i++ { + _, err := ml.Accept() + if err != nil { + return err + } + } + err := ml.Close() + if err != nil { + return err + } + return nil + }, + fakeListeners: []*fakeListener{{ + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}}, + }, + }}, + acceptCalls: 3, + }, + { + name: "close on close", + addrs: []string{"10.10.10.10:5000", "192.168.1.10:5000", "127.0.0.1:5000"}, + runner: func(ml net.Listener, acceptCalls int) error { + for i := 0; i < acceptCalls; i++ { + _, err := ml.Accept() + if err != nil { + return err + } + } + err := ml.Close() + if err != nil { + return err + } + + err = ml.Close() + if err != nil { + return err + } + return nil + }, + fakeListeners: []*fakeListener{{}, {}, {}}, + errString: "use of closed network connection", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.TODO() + ml, err := multiListen(ctx, "tcp", tc.addrs, listenFuncFactory(tc.fakeListeners)) + if err != nil { + t.Errorf("Did not expect error: %v", err) + } + err = tc.runner(ml, tc.acceptCalls) + if tc.errString != "" { + assertError(t, tc.errString, err) + } else { + assertNoError(t, err) + } + + for _, f := range tc.fakeListeners { + if !f.closed.Load() { + t.Errorf("Expeted sub-listener to be closed") + } + } + }) + } +} + +func TestMultiListen_Accept(t *testing.T) { + testCases := []struct { + name string + addrs []string + runner func(listener net.Listener, acceptCalls int) error + fakeListeners []*fakeListener + acceptCalls int + errString string + }{ + { + name: "accept all connections", + addrs: []string{"10.10.10.10:3000", "192.168.1.103:4000", "127.0.0.1:5000"}, + runner: func(ml net.Listener, acceptCalls int) error { + for i := 0; i < acceptCalls; i++ { + _, err := ml.Accept() + if err != nil { + return err + } + } + err := ml.Close() + if err != nil { + return err + } + return nil + }, + fakeListeners: []*fakeListener{{ + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}}, + }, + }}, + acceptCalls: 3, + }, + { + name: "accept some connections", + addrs: []string{"10.10.10.10:3000", "192.168.1.103:4000", "172.16.20.10:5000", "127.0.0.1:6000"}, + runner: func(ml net.Listener, acceptCalls int) error { + + for i := 0; i < acceptCalls; i++ { + _, err := ml.Accept() + if err != nil { + return err + } + + } + err := ml.Close() + if err != nil { + return err + } + return nil + }, + fakeListeners: []*fakeListener{{ + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 30001}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 40001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 40002}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50004}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60004}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60005}}}, + }, + }}, + acceptCalls: 3, + }, + { + name: "accept on closed listener", + addrs: []string{"10.10.10.10:3001", "192.168.1.10:3002", "127.0.0.1:3003"}, + runner: func(ml net.Listener, acceptCalls int) error { + err := ml.Close() + if err != nil { + return err + } + for i := 0; i < acceptCalls; i++ { + _, err := ml.Accept() + if err != nil { + return err + } + } + return nil + }, + fakeListeners: []*fakeListener{{ + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}}, + }}, { + connErrPairs: []connErrPair{ + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}}, + }, + }}, + acceptCalls: 1, + errString: "use of closed network connection", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := context.TODO() + ml, err := multiListen(ctx, "tcp", tc.addrs, listenFuncFactory(tc.fakeListeners)) + if err != nil { + t.Errorf("Did not expect error: %v", err) + } + + err = tc.runner(ml, tc.acceptCalls) + if tc.errString != "" { + assertError(t, tc.errString, err) + } else { + assertNoError(t, err) + } + }) + } +} + +func TestMultiListen_HTTP(t *testing.T) { + ctx := context.TODO() + ml, err := MultiListen(ctx, "tcp", ":0", ":0", ":0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + addrs := ml.(*multiListener).Addrs() + if len(addrs) != 3 { + t.Fatalf("expected 3 listeners, got %v", addrs) + } + + // serve http on multi-listener + handler := func(w http.ResponseWriter, _ *http.Request) { + io.WriteString(w, "hello") + } + server := http.Server{ + Handler: http.HandlerFunc(handler), + } + go func() { _ = server.Serve(ml) }() + defer server.Close() + + // Wait for server + awake := false + for i := 0; i < 5; i++ { + _, err = http.Get("http://" + addrs[0].String()) + if err == nil { + awake = true + break + } + time.Sleep(50 * time.Millisecond) + } + if !awake { + t.Fatalf("http server did not respond in time") + } + + // HTTP GET on each address. + for _, addr := range addrs { + _, err = http.Get("http://" + addr.String()) + if err != nil { + t.Errorf("error connecting to %q: %v", addr.String(), err) + } + } +} + +func assertError(t *testing.T, errString string, err error) { + if err == nil { + t.Errorf("Expected error '%s' but got none", errString) + } + if err.Error() != errString { + t.Errorf("Expected error '%s' but got '%s'", errString, err.Error()) + } +} + +func assertNoError(t *testing.T, err error) { + if err != nil { + t.Errorf("Did not expect error: %v", err) + } +} diff --git a/net/v2/net.go b/net/v2/net.go new file mode 100644 index 00000000..704c1f23 --- /dev/null +++ b/net/v2/net.go @@ -0,0 +1,91 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "errors" + "fmt" + "math" + "math/big" + "net" + "strconv" +) + +// ParseCIDRs parses a list of cidrs and return error if any is invalid. +// order is maintained +func ParseCIDRs(cidrsString []string) ([]*net.IPNet, error) { + cidrs := make([]*net.IPNet, 0, len(cidrsString)) + for i, cidrString := range cidrsString { + _, cidr, err := ParseCIDRSloppy(cidrString) + if err != nil { + return nil, fmt.Errorf("invalid CIDR[%d]: %v (%v)", i, cidr, err) + } + cidrs = append(cidrs, cidr) + } + return cidrs, nil +} + +// ParsePort parses a string representing an IP port. If the string is not a +// valid port number, this returns an error. +func ParsePort(port string, allowZero bool) (int, error) { + portInt, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return 0, err + } + if portInt == 0 && !allowZero { + return 0, errors.New("0 is not a valid port number") + } + return int(portInt), nil +} + +// BigForIP creates a big.Int based on the provided net.IP +func BigForIP(ip net.IP) *big.Int { + // NOTE: Convert to 16-byte representation so we can + // handle v4 and v6 values the same way. + return big.NewInt(0).SetBytes(ip.To16()) +} + +// AddIPOffset adds the provided integer offset to a base big.Int representing a net.IP +// NOTE: If you started with a v4 address and overflow it, you get a v6 result. +func AddIPOffset(base *big.Int, offset int) net.IP { + r := big.NewInt(0).Add(base, big.NewInt(int64(offset))).Bytes() + r = append(make([]byte, 16), r...) + return net.IP(r[len(r)-16:]) +} + +// RangeSize returns the size of a range in valid addresses. +// returns the size of the subnet (or math.MaxInt64 if the range size would overflow int64) +func RangeSize(subnet *net.IPNet) int64 { + ones, bits := subnet.Mask.Size() + if bits == 32 && (bits-ones) >= 31 || bits == 128 && (bits-ones) >= 127 { + return 0 + } + // this checks that we are not overflowing an int64 + if bits-ones >= 63 { + return math.MaxInt64 + } + return int64(1) << uint(bits-ones) +} + +// GetIndexedIP returns a net.IP that is subnet.IP + index in the contiguous IP space. +func GetIndexedIP(subnet *net.IPNet, index int) (net.IP, error) { + ip := AddIPOffset(BigForIP(subnet.IP), index) + if !subnet.Contains(ip) { + return nil, fmt.Errorf("can't generate IP with index %d from subnet. subnet too small. subnet: %q", index, subnet) + } + return ip, nil +} diff --git a/net/v2/net_test.go b/net/v2/net_test.go new file mode 100644 index 00000000..0c8ffe1d --- /dev/null +++ b/net/v2/net_test.go @@ -0,0 +1,250 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "testing" +) + +func TestParseCIDRs(t *testing.T) { + testCases := []struct { + cidrs []string + errString string + errorExpected bool + }{ + { + cidrs: []string{}, + errString: "should not return an error for an empty slice", + errorExpected: false, + }, + { + cidrs: []string{"10.0.0.0/8", "not-a-valid-cidr", "2000::/10"}, + errString: "should return error for bad cidr", + errorExpected: true, + }, + { + cidrs: []string{"10.0.0.0/8", "2000::/10"}, + errString: "should not return error for good cidrs", + errorExpected: false, + }, + } + + for _, tc := range testCases { + cidrs, err := ParseCIDRs(tc.cidrs) + if tc.errorExpected { + if err == nil { + t.Errorf("%v", tc.errString) + } + continue + } + if err != nil { + t.Errorf("%v error:%v", tc.errString, err) + } + + // validate lengths + if len(cidrs) != len(tc.cidrs) { + t.Errorf("cidrs should be of the same lengths %v != %v", len(cidrs), len(tc.cidrs)) + } + + } +} + +func TestParsePort(t *testing.T) { + var tests = []struct { + name string + port string + allowZero bool + expectedPort int + expectedError bool + }{ + { + name: "valid port: 1", + port: "1", + expectedPort: 1, + }, + { + name: "valid port: 1234", + port: "1234", + expectedPort: 1234, + }, + { + name: "valid port: 65535", + port: "65535", + expectedPort: 65535, + }, + { + name: "invalid port: not a number", + port: "a", + expectedError: true, + allowZero: false, + }, + { + name: "invalid port: too small", + port: "0", + expectedError: true, + }, + { + name: "invalid port: negative", + port: "-10", + expectedError: true, + }, + { + name: "invalid port: too big", + port: "65536", + expectedError: true, + }, + { + name: "zero port: allowed", + port: "0", + allowZero: true, + }, + { + name: "zero port: not allowed", + port: "0", + expectedError: true, + }, + } + + for _, rt := range tests { + t.Run(rt.name, func(t *testing.T) { + actualPort, actualError := ParsePort(rt.port, rt.allowZero) + + if actualError != nil && !rt.expectedError { + t.Errorf("%s unexpected failure: %v", rt.name, actualError) + return + } + if actualError == nil && rt.expectedError { + t.Errorf("%s passed when expected to fail", rt.name) + return + } + if actualPort != rt.expectedPort { + t.Errorf("%s returned wrong port: got %d, expected %d", rt.name, actualPort, rt.expectedPort) + } + }) + } +} + +func TestRangeSize(t *testing.T) { + testCases := []struct { + name string + cidr string + addrs int64 + }{ + { + name: "supported IPv4 cidr", + cidr: "192.168.1.0/24", + addrs: 256, + }, + { + name: "unsupported IPv4 cidr", + cidr: "192.168.1.0/1", + addrs: 0, + }, + { + name: "unsupported IPv6 mask", + cidr: "2001:db8::/1", + addrs: 0, + }, + } + + for _, tc := range testCases { + _, cidr, err := ParseCIDRSloppy(tc.cidr) + if err != nil { + t.Errorf("failed to parse cidr for test %s, unexpected error: '%s'", tc.name, err) + } + if size := RangeSize(cidr); size != tc.addrs { + t.Errorf("test %s failed. %s should have a range size of %d, got %d", + tc.name, tc.cidr, tc.addrs, size) + } + } +} + +func TestGetIndexedIP(t *testing.T) { + testCases := []struct { + cidr string + index int + expectError bool + expectedIP string + }{ + { + cidr: "192.168.1.0/24", + index: 20, + expectError: false, + expectedIP: "192.168.1.20", + }, + { + cidr: "192.168.1.0/30", + index: 10, + expectError: true, + }, + { + cidr: "192.168.1.0/24", + index: 255, + expectError: false, + expectedIP: "192.168.1.255", + }, + { + cidr: "255.255.255.0/24", + index: 256, + expectError: true, + }, + { + cidr: "fd:11:b2:be::/120", + index: 20, + expectError: false, + expectedIP: "fd:11:b2:be::14", + }, + { + cidr: "fd:11:b2:be::/126", + index: 10, + expectError: true, + }, + { + cidr: "fd:11:b2:be::/120", + index: 255, + expectError: false, + expectedIP: "fd:11:b2:be::ff", + }, + { + cidr: "00:00:00:be::/120", + index: 255, + expectError: false, + expectedIP: "::be:0:0:0:ff", + }, + } + + for _, tc := range testCases { + _, subnet, err := ParseCIDRSloppy(tc.cidr) + if err != nil { + t.Errorf("failed to parse cidr %s, unexpected error: '%s'", tc.cidr, err) + } + + ip, err := GetIndexedIP(subnet, tc.index) + if err == nil && tc.expectError || err != nil && !tc.expectError { + t.Errorf("expectedError is %v and err is %s", tc.expectError, err) + continue + } + + if err == nil { + ipString := ip.String() + if ipString != tc.expectedIP { + t.Errorf("expected %s but instead got %s", tc.expectedIP, ipString) + } + } + + } +} diff --git a/net/v2/parse.go b/net/v2/parse.go new file mode 100644 index 00000000..400d364d --- /dev/null +++ b/net/v2/parse.go @@ -0,0 +1,33 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + forkednet "k8s.io/utils/internal/third_party/forked/golang/net" +) + +// ParseIPSloppy is identical to Go's standard net.ParseIP, except that it allows +// leading '0' characters on numbers. Go used to allow this and then changed +// the behavior in 1.17. We're choosing to keep it for compat with potential +// stored values. +var ParseIPSloppy = forkednet.ParseIP + +// ParseCIDRSloppy is identical to Go's standard net.ParseCIDR, except that it allows +// leading '0' characters on numbers. Go used to allow this and then changed +// the behavior in 1.17. We're choosing to keep it for compat with potential +// stored values. +var ParseCIDRSloppy = forkednet.ParseCIDR diff --git a/net/v2/parse_test.go b/net/v2/parse_test.go new file mode 100644 index 00000000..40255927 --- /dev/null +++ b/net/v2/parse_test.go @@ -0,0 +1,107 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "testing" +) + +func TestParseIPSloppy(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestIPs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, str := range tc.strings { + ip := ParseIPSloppy(str) + if ip == nil { + t.Errorf("expected %q to parse, but failed", str) + } + if !ip.Equal(tc.ips[0]) { + t.Errorf("expected string %d %q to parse equal to IP %#v %q but got %#v (%q)", i+1, str, tc.ips[0], tc.ips[0].String(), ip, ip.String()) + } + } + }) + } + + // See test cases in ips_test.go + for _, tc := range badTestIPs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ip := range tc.ips { + errStr := ip.String() + parsedIP := ParseIPSloppy(errStr) + if parsedIP != nil { + t.Errorf("expected IP %d %#v (%q) to not re-parse but got %#v (%q)", i+1, ip, errStr, parsedIP, parsedIP.String()) + } + } + + for i, str := range tc.strings { + ip := ParseIPSloppy(str) + if ip != nil { + t.Errorf("expected string %d %q to not parse but got %#v (%q)", i+1, str, ip, ip.String()) + } + } + }) + } +} + +func TestParseCIDRSloppy(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestCIDRs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, str := range tc.strings { + _, ipnet, err := ParseCIDRSloppy(str) + if err != nil { + t.Errorf("expected %q to parse, but got error %v", str, err) + } + if ipnet.String() != tc.ipnets[0].String() { + t.Errorf("expected string %d %q to parse and re-stringify to %q but got %q", i+1, str, tc.ipnets[0].String(), ipnet.String()) + } + } + }) + } + + // See test cases in ips_test.go + for _, tc := range badTestCIDRs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ipnet := range tc.ipnets { + errStr := ipnet.String() + _, parsedIPNet, err := ParseCIDRSloppy(errStr) + if err == nil { + t.Errorf("expected IPNet %d %q to not parse but got %#v (%q)", i+1, errStr, *parsedIPNet, parsedIPNet.String()) + } + } + + for i, str := range tc.strings { + _, ipnet, err := ParseCIDRSloppy(str) + if err == nil { + t.Errorf("expected string %d %q to not parse but got %#v (%q)", i+1, str, *ipnet, ipnet.String()) + } + } + }) + } +} diff --git a/net/v2/port.go b/net/v2/port.go new file mode 100644 index 00000000..c6a53fa0 --- /dev/null +++ b/net/v2/port.go @@ -0,0 +1,129 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "fmt" + "net" + "strconv" + "strings" +) + +// Protocol is a network protocol support by LocalPort. +type Protocol string + +// Constants for valid protocols: +const ( + TCP Protocol = "TCP" + UDP Protocol = "UDP" +) + +// LocalPort represents an IP address and port pair along with a protocol +// and potentially a specific IP family. +// A LocalPort can be opened and subsequently closed. +type LocalPort struct { + // Description is an arbitrary string. + Description string + // IP is the IP address part of a given local port. + // If this string is empty, the port binds to all local IP addresses. + IP string + // If IPFamily is not empty, the port binds only to addresses of this + // family. + // IF empty along with IP, bind to local addresses of any family. + IPFamily IPFamily + // Port is the port number. + // A value of 0 causes a port to be automatically chosen. + Port int + // Protocol is the protocol, e.g. TCP + Protocol Protocol +} + +// NewLocalPort returns a LocalPort instance and ensures IPFamily and IP are +// consistent and that the given protocol is valid. +func NewLocalPort(desc, ip string, ipFamily IPFamily, port int, protocol Protocol) (*LocalPort, error) { + if protocol != TCP && protocol != UDP { + return nil, fmt.Errorf("Unsupported protocol %s", protocol) + } + if ipFamily != IPFamilyUnknown && ipFamily != IPv4 && ipFamily != IPv6 { + return nil, fmt.Errorf("Invalid IP family %s", ipFamily) + } + if ip != "" { + parsedIP := ParseIPSloppy(ip) + if parsedIP == nil { + return nil, fmt.Errorf("invalid ip address %s", ip) + } + if ipFamily != IPFamilyUnknown { + if IPFamily(parsedIP) != ipFamily { + return nil, fmt.Errorf("ip address and family mismatch %s, %s", ip, ipFamily) + } + } + } + return &LocalPort{Description: desc, IP: ip, IPFamily: ipFamily, Port: port, Protocol: protocol}, nil +} + +func (lp *LocalPort) String() string { + ipPort := net.JoinHostPort(lp.IP, strconv.Itoa(lp.Port)) + return fmt.Sprintf("%q (%s/%s%s)", lp.Description, ipPort, strings.ToLower(string(lp.Protocol)), lp.IPFamily) +} + +// Closeable closes an opened LocalPort. +type Closeable interface { + Close() error +} + +// PortOpener can open a LocalPort and allows later closing it. +type PortOpener interface { + OpenLocalPort(lp *LocalPort) (Closeable, error) +} + +type listenPortOpener struct{} + +// ListenPortOpener opens ports by calling bind() and listen(). +var ListenPortOpener listenPortOpener + +// OpenLocalPort holds the given local port open. +func (l *listenPortOpener) OpenLocalPort(lp *LocalPort) (Closeable, error) { + return openLocalPort(lp) +} + +func openLocalPort(lp *LocalPort) (Closeable, error) { + var socket Closeable + hostPort := net.JoinHostPort(lp.IP, strconv.Itoa(lp.Port)) + switch lp.Protocol { + case TCP: + network := "tcp" + string(lp.IPFamily) + listener, err := net.Listen(network, hostPort) + if err != nil { + return nil, err + } + socket = listener + case UDP: + network := "udp" + string(lp.IPFamily) + addr, err := net.ResolveUDPAddr(network, hostPort) + if err != nil { + return nil, err + } + conn, err := net.ListenUDP(network, addr) + if err != nil { + return nil, err + } + socket = conn + default: + return nil, fmt.Errorf("unknown protocol %q", lp.Protocol) + } + return socket, nil +} diff --git a/net/v2/port_test.go b/net/v2/port_test.go new file mode 100644 index 00000000..0aa986f1 --- /dev/null +++ b/net/v2/port_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "testing" +) + +func ExampleLocalPort() { + lp, err := NewLocalPort( + "TCP port", + "", + IPv4, + 443, + TCP, + ) + if err != nil { + panic(err) + } + port, err := ListenPortOpener.OpenLocalPort(lp) + if err != nil { + panic(err) + } + port.Close() +} + +func TestLocalPortString(t *testing.T) { + testCases := []struct { + description string + ip string + family IPFamily + port int + protocol Protocol + expectedStr string + expectedErr bool + }{ + {"IPv4 UDP", "1.2.3.4", "", 9999, UDP, `"IPv4 UDP" (1.2.3.4:9999/udp)`, false}, + {"IPv4 TCP", "5.6.7.8", "", 1053, TCP, `"IPv4 TCP" (5.6.7.8:1053/tcp)`, false}, + {"IPv6 TCP", "2001:db8::1", "", 80, TCP, `"IPv6 TCP" ([2001:db8::1]:80/tcp)`, false}, + {"IPv4 TCP, all addresses", "", IPv4, 1053, TCP, `"IPv4 TCP, all addresses" (:1053/tcp4)`, false}, + {"IPv6 TCP, all addresses", "", IPv6, 80, TCP, `"IPv6 TCP, all addresses" (:80/tcp6)`, false}, + {"No ip family TCP, all addresses", "", "", 80, TCP, `"No ip family TCP, all addresses" (:80/tcp)`, false}, + {"IP family mismatch", "2001:db8::2", IPv4, 80, TCP, "", true}, + {"IP family mismatch", "1.2.3.4", IPv6, 80, TCP, "", true}, + {"Unsupported protocol", "2001:db8::2", "", 80, "http", "", true}, + {"Invalid IP", "300", "", 80, TCP, "", true}, + {"Invalid ip family", "", "5", 80, TCP, "", true}, + } + + for _, tc := range testCases { + lp, err := NewLocalPort( + tc.description, + tc.ip, + tc.family, + tc.port, + tc.protocol, + ) + if tc.expectedErr { + if err == nil { + t.Errorf("Expected err when creating LocalPort %v", tc) + } + continue + } + if err != nil { + t.Errorf("Unexpected err when creating LocalPort %s", err) + continue + } + str := lp.String() + if str != tc.expectedStr { + t.Errorf("Unexpected output for %s, expected: %s, got: %s", tc.description, tc.expectedStr, str) + } + } +} From 9a069e37333b1b17a7e0ae7090f865e5698e9d13 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 28 Jul 2025 05:42:19 -0400 Subject: [PATCH 03/17] Remove PortOpener from netutilsv2 It's no longer used in k/k and is considered a bad idea now. --- net/v2/port.go | 129 -------------------------------------------- net/v2/port_test.go | 87 ------------------------------ 2 files changed, 216 deletions(-) delete mode 100644 net/v2/port.go delete mode 100644 net/v2/port_test.go diff --git a/net/v2/port.go b/net/v2/port.go deleted file mode 100644 index c6a53fa0..00000000 --- a/net/v2/port.go +++ /dev/null @@ -1,129 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package net - -import ( - "fmt" - "net" - "strconv" - "strings" -) - -// Protocol is a network protocol support by LocalPort. -type Protocol string - -// Constants for valid protocols: -const ( - TCP Protocol = "TCP" - UDP Protocol = "UDP" -) - -// LocalPort represents an IP address and port pair along with a protocol -// and potentially a specific IP family. -// A LocalPort can be opened and subsequently closed. -type LocalPort struct { - // Description is an arbitrary string. - Description string - // IP is the IP address part of a given local port. - // If this string is empty, the port binds to all local IP addresses. - IP string - // If IPFamily is not empty, the port binds only to addresses of this - // family. - // IF empty along with IP, bind to local addresses of any family. - IPFamily IPFamily - // Port is the port number. - // A value of 0 causes a port to be automatically chosen. - Port int - // Protocol is the protocol, e.g. TCP - Protocol Protocol -} - -// NewLocalPort returns a LocalPort instance and ensures IPFamily and IP are -// consistent and that the given protocol is valid. -func NewLocalPort(desc, ip string, ipFamily IPFamily, port int, protocol Protocol) (*LocalPort, error) { - if protocol != TCP && protocol != UDP { - return nil, fmt.Errorf("Unsupported protocol %s", protocol) - } - if ipFamily != IPFamilyUnknown && ipFamily != IPv4 && ipFamily != IPv6 { - return nil, fmt.Errorf("Invalid IP family %s", ipFamily) - } - if ip != "" { - parsedIP := ParseIPSloppy(ip) - if parsedIP == nil { - return nil, fmt.Errorf("invalid ip address %s", ip) - } - if ipFamily != IPFamilyUnknown { - if IPFamily(parsedIP) != ipFamily { - return nil, fmt.Errorf("ip address and family mismatch %s, %s", ip, ipFamily) - } - } - } - return &LocalPort{Description: desc, IP: ip, IPFamily: ipFamily, Port: port, Protocol: protocol}, nil -} - -func (lp *LocalPort) String() string { - ipPort := net.JoinHostPort(lp.IP, strconv.Itoa(lp.Port)) - return fmt.Sprintf("%q (%s/%s%s)", lp.Description, ipPort, strings.ToLower(string(lp.Protocol)), lp.IPFamily) -} - -// Closeable closes an opened LocalPort. -type Closeable interface { - Close() error -} - -// PortOpener can open a LocalPort and allows later closing it. -type PortOpener interface { - OpenLocalPort(lp *LocalPort) (Closeable, error) -} - -type listenPortOpener struct{} - -// ListenPortOpener opens ports by calling bind() and listen(). -var ListenPortOpener listenPortOpener - -// OpenLocalPort holds the given local port open. -func (l *listenPortOpener) OpenLocalPort(lp *LocalPort) (Closeable, error) { - return openLocalPort(lp) -} - -func openLocalPort(lp *LocalPort) (Closeable, error) { - var socket Closeable - hostPort := net.JoinHostPort(lp.IP, strconv.Itoa(lp.Port)) - switch lp.Protocol { - case TCP: - network := "tcp" + string(lp.IPFamily) - listener, err := net.Listen(network, hostPort) - if err != nil { - return nil, err - } - socket = listener - case UDP: - network := "udp" + string(lp.IPFamily) - addr, err := net.ResolveUDPAddr(network, hostPort) - if err != nil { - return nil, err - } - conn, err := net.ListenUDP(network, addr) - if err != nil { - return nil, err - } - socket = conn - default: - return nil, fmt.Errorf("unknown protocol %q", lp.Protocol) - } - return socket, nil -} diff --git a/net/v2/port_test.go b/net/v2/port_test.go deleted file mode 100644 index 0aa986f1..00000000 --- a/net/v2/port_test.go +++ /dev/null @@ -1,87 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package net - -import ( - "testing" -) - -func ExampleLocalPort() { - lp, err := NewLocalPort( - "TCP port", - "", - IPv4, - 443, - TCP, - ) - if err != nil { - panic(err) - } - port, err := ListenPortOpener.OpenLocalPort(lp) - if err != nil { - panic(err) - } - port.Close() -} - -func TestLocalPortString(t *testing.T) { - testCases := []struct { - description string - ip string - family IPFamily - port int - protocol Protocol - expectedStr string - expectedErr bool - }{ - {"IPv4 UDP", "1.2.3.4", "", 9999, UDP, `"IPv4 UDP" (1.2.3.4:9999/udp)`, false}, - {"IPv4 TCP", "5.6.7.8", "", 1053, TCP, `"IPv4 TCP" (5.6.7.8:1053/tcp)`, false}, - {"IPv6 TCP", "2001:db8::1", "", 80, TCP, `"IPv6 TCP" ([2001:db8::1]:80/tcp)`, false}, - {"IPv4 TCP, all addresses", "", IPv4, 1053, TCP, `"IPv4 TCP, all addresses" (:1053/tcp4)`, false}, - {"IPv6 TCP, all addresses", "", IPv6, 80, TCP, `"IPv6 TCP, all addresses" (:80/tcp6)`, false}, - {"No ip family TCP, all addresses", "", "", 80, TCP, `"No ip family TCP, all addresses" (:80/tcp)`, false}, - {"IP family mismatch", "2001:db8::2", IPv4, 80, TCP, "", true}, - {"IP family mismatch", "1.2.3.4", IPv6, 80, TCP, "", true}, - {"Unsupported protocol", "2001:db8::2", "", 80, "http", "", true}, - {"Invalid IP", "300", "", 80, TCP, "", true}, - {"Invalid ip family", "", "5", 80, TCP, "", true}, - } - - for _, tc := range testCases { - lp, err := NewLocalPort( - tc.description, - tc.ip, - tc.family, - tc.port, - tc.protocol, - ) - if tc.expectedErr { - if err == nil { - t.Errorf("Expected err when creating LocalPort %v", tc) - } - continue - } - if err != nil { - t.Errorf("Unexpected err when creating LocalPort %s", err) - continue - } - str := lp.String() - if str != tc.expectedStr { - t.Errorf("Unexpected output for %s, expected: %s, got: %s", tc.description, tc.expectedStr, str) - } - } -} From b352a35198221c790607e55905543ca7fbe0aed0 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 28 Jul 2025 05:43:02 -0400 Subject: [PATCH 04/17] Remove IPSet and IPNetSet from netutilsv2 These aren't currently used by k/k anyway, and could be replaced with set[netip.Addr] and set[netip.Prefix] in new code. Also, ParseIPSet() and ParseIPNets() had strange whitespace-ignoring semantics that we don't want anyway. --- net/v2/ipnet.go | 221 ------------------------------ net/v2/ipnet_test.go | 314 ------------------------------------------- 2 files changed, 535 deletions(-) delete mode 100644 net/v2/ipnet.go delete mode 100644 net/v2/ipnet_test.go diff --git a/net/v2/ipnet.go b/net/v2/ipnet.go deleted file mode 100644 index 2f3ee37f..00000000 --- a/net/v2/ipnet.go +++ /dev/null @@ -1,221 +0,0 @@ -/* -Copyright 2016 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package net - -import ( - "fmt" - "net" - "strings" -) - -// IPNetSet maps string to net.IPNet. -type IPNetSet map[string]*net.IPNet - -// ParseIPNets parses string slice to IPNetSet. -func ParseIPNets(specs ...string) (IPNetSet, error) { - ipnetset := make(IPNetSet) - for _, spec := range specs { - spec = strings.TrimSpace(spec) - _, ipnet, err := ParseCIDRSloppy(spec) - if err != nil { - return nil, err - } - k := ipnet.String() // In case of normalization - ipnetset[k] = ipnet - } - return ipnetset, nil -} - -// Insert adds items to the set. -func (s IPNetSet) Insert(items ...*net.IPNet) { - for _, item := range items { - s[item.String()] = item - } -} - -// Delete removes all items from the set. -func (s IPNetSet) Delete(items ...*net.IPNet) { - for _, item := range items { - delete(s, item.String()) - } -} - -// Has returns true if and only if item is contained in the set. -func (s IPNetSet) Has(item *net.IPNet) bool { - _, contained := s[item.String()] - return contained -} - -// HasAll returns true if and only if all items are contained in the set. -func (s IPNetSet) HasAll(items ...*net.IPNet) bool { - for _, item := range items { - if !s.Has(item) { - return false - } - } - return true -} - -// Difference returns a set of objects that are not in s2 -// For example: -// s1 = {a1, a2, a3} -// s2 = {a1, a2, a4, a5} -// s1.Difference(s2) = {a3} -// s2.Difference(s1) = {a4, a5} -func (s IPNetSet) Difference(s2 IPNetSet) IPNetSet { - result := make(IPNetSet) - for k, i := range s { - _, found := s2[k] - if found { - continue - } - result[k] = i - } - return result -} - -// StringSlice returns a []string with the String representation of each element in the set. -// Order is undefined. -func (s IPNetSet) StringSlice() []string { - a := make([]string, 0, len(s)) - for k := range s { - a = append(a, k) - } - return a -} - -// IsSuperset returns true if and only if s1 is a superset of s2. -func (s IPNetSet) IsSuperset(s2 IPNetSet) bool { - for k := range s2 { - _, found := s[k] - if !found { - return false - } - } - return true -} - -// Equal returns true if and only if s1 is equal (as a set) to s2. -// Two sets are equal if their membership is identical. -// (In practice, this means same elements, order doesn't matter) -func (s IPNetSet) Equal(s2 IPNetSet) bool { - return len(s) == len(s2) && s.IsSuperset(s2) -} - -// Len returns the size of the set. -func (s IPNetSet) Len() int { - return len(s) -} - -// IPSet maps string to net.IP -type IPSet map[string]net.IP - -// ParseIPSet parses string slice to IPSet -func ParseIPSet(items ...string) (IPSet, error) { - ipset := make(IPSet) - for _, item := range items { - ip := ParseIPSloppy(strings.TrimSpace(item)) - if ip == nil { - return nil, fmt.Errorf("error parsing IP %q", item) - } - - ipset[ip.String()] = ip - } - - return ipset, nil -} - -// Insert adds items to the set. -func (s IPSet) Insert(items ...net.IP) { - for _, item := range items { - s[item.String()] = item - } -} - -// Delete removes all items from the set. -func (s IPSet) Delete(items ...net.IP) { - for _, item := range items { - delete(s, item.String()) - } -} - -// Has returns true if and only if item is contained in the set. -func (s IPSet) Has(item net.IP) bool { - _, contained := s[item.String()] - return contained -} - -// HasAll returns true if and only if all items are contained in the set. -func (s IPSet) HasAll(items ...net.IP) bool { - for _, item := range items { - if !s.Has(item) { - return false - } - } - return true -} - -// Difference returns a set of objects that are not in s2 -// For example: -// s1 = {a1, a2, a3} -// s2 = {a1, a2, a4, a5} -// s1.Difference(s2) = {a3} -// s2.Difference(s1) = {a4, a5} -func (s IPSet) Difference(s2 IPSet) IPSet { - result := make(IPSet) - for k, i := range s { - _, found := s2[k] - if found { - continue - } - result[k] = i - } - return result -} - -// StringSlice returns a []string with the String representation of each element in the set. -// Order is undefined. -func (s IPSet) StringSlice() []string { - a := make([]string, 0, len(s)) - for k := range s { - a = append(a, k) - } - return a -} - -// IsSuperset returns true if and only if s1 is a superset of s2. -func (s IPSet) IsSuperset(s2 IPSet) bool { - for k := range s2 { - _, found := s[k] - if !found { - return false - } - } - return true -} - -// Equal returns true if and only if s1 is equal (as a set) to s2. -// Two sets are equal if their membership is identical. -// (In practice, this means same elements, order doesn't matter) -func (s IPSet) Equal(s2 IPSet) bool { - return len(s) == len(s2) && s.IsSuperset(s2) -} - -// Len returns the size of the set. -func (s IPSet) Len() int { - return len(s) -} diff --git a/net/v2/ipnet_test.go b/net/v2/ipnet_test.go deleted file mode 100644 index 6d6e1b94..00000000 --- a/net/v2/ipnet_test.go +++ /dev/null @@ -1,314 +0,0 @@ -/* -Copyright 2014 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package net - -import ( - "net" - "reflect" - "sort" - "testing" -) - -func parseIPNet(s string) *net.IPNet { - _, net, err := ParseCIDRSloppy(s) - if err != nil { - panic(err) - } - return net -} - -func TestIPNets(t *testing.T) { - s := IPNetSet{} - s2 := IPNetSet{} - if len(s) != 0 { - t.Errorf("Expected len=0: %d", len(s)) - } - a := parseIPNet("1.0.0.0/8") - b := parseIPNet("2.0.0.0/8") - c := parseIPNet("3.0.0.0/8") - d := parseIPNet("4.0.0.0/8") - - s.Insert(a, b) - if len(s) != 2 { - t.Errorf("Expected len=2: %d", len(s)) - } - s.Insert(c) - if s.Has(d) { - t.Errorf("Unexpected contents: %#v", s) - } - if !s.Has(a) { - t.Errorf("Missing contents: %#v", s) - } - s.Delete(a) - if s.Has(a) { - t.Errorf("Unexpected contents: %#v", s) - } - s.Insert(a) - if s.HasAll(a, b, d) { - t.Errorf("Unexpected contents: %#v", s) - } - if !s.HasAll(a, b) { - t.Errorf("Missing contents: %#v", s) - } - s2.Insert(a, b, d) - if s.IsSuperset(s2) { - t.Errorf("Unexpected contents: %#v", s) - } - s2.Delete(d) - if !s.IsSuperset(s2) { - t.Errorf("Missing contents: %#v", s) - } -} - -func TestIPNetSetDeleteMultiples(t *testing.T) { - s := IPNetSet{} - a := parseIPNet("1.0.0.0/8") - b := parseIPNet("2.0.0.0/8") - c := parseIPNet("3.0.0.0/8") - - s.Insert(a, b, c) - if len(s) != 3 { - t.Errorf("Expected len=3: %d", len(s)) - } - - s.Delete(a, c) - if len(s) != 1 { - t.Errorf("Expected len=1: %d", len(s)) - } - if s.Has(a) { - t.Errorf("Unexpected contents: %#v", s) - } - if s.Has(c) { - t.Errorf("Unexpected contents: %#v", s) - } - if !s.Has(b) { - t.Errorf("Missing contents: %#v", s) - } -} - -func TestNewIPNetSet(t *testing.T) { - s, err := ParseIPNets("1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8") - if err != nil { - t.Errorf("error parsing IPNets: %v", err) - } - if len(s) != 3 { - t.Errorf("Expected len=3: %d", len(s)) - } - a := parseIPNet("1.0.0.0/8") - b := parseIPNet("2.0.0.0/8") - c := parseIPNet("3.0.0.0/8") - - if !s.Has(a) || !s.Has(b) || !s.Has(c) { - t.Errorf("Unexpected contents: %#v", s) - } -} - -func TestIPNetSetDifference(t *testing.T) { - l, err := ParseIPNets("1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8") - if err != nil { - t.Errorf("error parsing IPNets: %v", err) - } - r, err := ParseIPNets("1.0.0.0/8", "2.0.0.0/8", "4.0.0.0/8", "5.0.0.0/8") - if err != nil { - t.Errorf("error parsing IPNets: %v", err) - } - c := l.Difference(r) - d := r.Difference(l) - if len(c) != 1 { - t.Errorf("Expected len=1: %d", len(c)) - } - if !c.Has(parseIPNet("3.0.0.0/8")) { - t.Errorf("Unexpected contents: %#v", c) - } - if len(d) != 2 { - t.Errorf("Expected len=2: %d", len(d)) - } - if !d.Has(parseIPNet("4.0.0.0/8")) || !d.Has(parseIPNet("5.0.0.0/8")) { - t.Errorf("Unexpected contents: %#v", d) - } -} - -func TestIPNetSetList(t *testing.T) { - s, err := ParseIPNets("3.0.0.0/8", "1.0.0.0/8", "2.0.0.0/8") - if err != nil { - t.Errorf("error parsing IPNets: %v", err) - } - l := s.StringSlice() - sort.Strings(l) - if !reflect.DeepEqual(l, []string{"1.0.0.0/8", "2.0.0.0/8", "3.0.0.0/8"}) { - t.Errorf("List gave unexpected result: %#v", l) - } -} - -func TestIPSet(t *testing.T) { - s := IPSet{} - s2 := IPSet{} - - a := ParseIPSloppy("1.0.0.0") - b := ParseIPSloppy("2.0.0.0") - c := ParseIPSloppy("3.0.0.0") - d := ParseIPSloppy("4.0.0.0") - - s.Insert(a, b) - if len(s) != 2 { - t.Errorf("Expected len=2: %d", len(s)) - } - if !s.Has(a) { - t.Errorf("Missing contents: %#v", s) - } - - s.Insert(c) - if s.Has(d) { - t.Errorf("Unexpected contents: %#v", s) - } - - s.Delete(a) - if s.Has(a) { - t.Errorf("Unexpected contents: %#v", s) - } - s.Insert(a) - if s.HasAll(a, b, d) { - t.Errorf("Unexpected contents: %#v", s) - } - if !s.HasAll(a, b) { - t.Errorf("Missing contents: %#v", s) - } - s2.Insert(a, b, d) - if s.IsSuperset(s2) { - t.Errorf("Unexpected contents: %#v", s) - } - s2.Delete(d) - if !s.IsSuperset(s2) { - t.Errorf("Missing contents: %#v", s) - } -} - -func TestIPSetDeleteMultiples(t *testing.T) { - s := IPSet{} - a := ParseIPSloppy("1.0.0.0") - b := ParseIPSloppy("2.0.0.0") - c := ParseIPSloppy("3.0.0.0") - - s.Insert(a, b, c) - if len(s) != 3 { - t.Errorf("Expected len=3: %d", len(s)) - } - - s.Delete(a, c) - if len(s) != 1 { - t.Errorf("Expected len=1: %d", len(s)) - } - if s.Has(a) { - t.Errorf("Unexpected contents: %#v", s) - } - if s.Has(c) { - t.Errorf("Unexpected contents: %#v", s) - } - if !s.Has(b) { - t.Errorf("Missing contents: %#v", s) - } -} - -func TestParseIPSet(t *testing.T) { - s, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0", "::ffff:4.0.0.0") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - if len(s) != 4 { - t.Errorf("Expected len=3: %d", len(s)) - } - a := ParseIPSloppy("1.0.0.0") - b := ParseIPSloppy("2.0.0.0") - c := ParseIPSloppy("3.0.0.0") - d := ParseIPSloppy("::ffff:4.0.0.0") - e := ParseIPSloppy("4.0.0.0") - - if !s.Has(a) || !s.Has(b) || !s.Has(c) || !s.Has(d) || !s.Has(e) { - t.Errorf("Unexpected contents: %#v", s) - } -} - -func TestIPSetDifference(t *testing.T) { - l, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - r, err := ParseIPSet("1.0.0.0", "2.0.0.0", "4.0.0.0", "5.0.0.0") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - c := l.Difference(r) - d := r.Difference(l) - if len(c) != 1 { - t.Errorf("Expected len=1: %d", len(c)) - } - if !c.Has(ParseIPSloppy("3.0.0.0")) { - t.Errorf("Unexpected contents: %#v", c) - } - if len(d) != 2 { - t.Errorf("Expected len=2: %d", len(d)) - } - if !d.Has(ParseIPSloppy("4.0.0.0")) || !d.Has(ParseIPSloppy("5.0.0.0")) { - t.Errorf("Unexpected contents: %#v", d) - } -} - -func TestIPSetList(t *testing.T) { - // NOTE: IPv4-in-IPv6 addresses are represented as IPv4 in its string value - s, err := ParseIPSet("3.0.0.0", "1.0.0.0", "2.0.0.0", "::ffff:1.2.3.4") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - - l := s.StringSlice() - sort.Strings(l) - if !reflect.DeepEqual(l, []string{"1.0.0.0", "1.2.3.4", "2.0.0.0", "3.0.0.0"}) { - t.Errorf("List gave unexpected result: %#v", l) - } -} - -func TestIPSetEqual(t *testing.T) { - // IPv4-in-IPv6 addresses are equal to their IPv4 equivalents - set1, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0", "::ffff:4.0.0.0") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - - set2, err := ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0", "4.0.0.0") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - - if !set1.Equal(set2) { - t.Errorf("sets %v and %v are not equal", set1, set2) - } - - // order shouldn't matter - set1, err = ParseIPSet("1.0.0.0", "2.0.0.0", "3.0.0.0") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - - set2, err = ParseIPSet("3.0.0.0", "1.0.0.0", "2.0.0.0") - if err != nil { - t.Errorf("error parsing IPSet: %v", err) - } - - if !set1.Equal(set2) { - t.Errorf("sets %v and %v are not equal", set1, set2) - } -} From 3b92cea6b1b8a12e6cae65884bebdfe41342988e Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Fri, 25 Jul 2025 07:17:36 -0400 Subject: [PATCH 05/17] Add netip.Addr/netip.Prefix test data to netutilsv2 ips_test.go --- net/v2/ips_test.go | 198 +++++++++++++++++++++++++++++++++++++++++-- net/v2/parse_test.go | 16 ++++ 2 files changed, 208 insertions(+), 6 deletions(-) diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go index 1a267d99..32c8fe81 100644 --- a/net/v2/ips_test.go +++ b/net/v2/ips_test.go @@ -18,6 +18,7 @@ package net import ( "net" + "net/netip" "testing" ) @@ -27,6 +28,7 @@ type testIP struct { family IPFamily strings []string ips []net.IP + addrs []netip.Addr skipFamily bool skipParse bool @@ -37,6 +39,8 @@ type testIP struct { // Preconditions (not involving functions in netutils): // - Each element of .ips is the same (i.e., .Equal()). // - Each element of .ips stringifies to .strings[0]. +// - Each element of .addrs is the same (i.e., ==). +// - Each element of .addrs stringifies to .strings[0]. // // IPFamily tests (unless `skipFamily: true`): // - Each element of .strings should be identified as .family. @@ -60,6 +64,11 @@ var goodTestIPs = []testIP{ func() net.IP { ip, _, _ := net.ParseCIDR("192.168.0.5/24"); return ip }(), func() net.IP { _, ipnet, _ := net.ParseCIDR("192.168.0.5/32"); return ipnet.IP }(), }, + addrs: []netip.Addr{ + netip.AddrFrom4([4]byte{192, 168, 0, 5}), + netip.MustParseAddr("192.168.0.5"), + netip.MustParsePrefix("192.168.0.5/24").Addr(), + }, }, { desc: "IPv4 all-zeros", @@ -75,6 +84,11 @@ var goodTestIPs = []testIP{ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 0, 0, 0, 0}, net.ParseIP("0.0.0.0"), }, + addrs: []netip.Addr{ + netip.IPv4Unspecified(), + netip.AddrFrom4([4]byte{0, 0, 0, 0}), + netip.MustParseAddr("0.0.0.0"), + }, }, { desc: "IPv4 broadcast", @@ -91,6 +105,10 @@ var goodTestIPs = []testIP{ // A /32 IPMask is equivalent to 255.255.255.255 func() net.IP { _, ipnet, _ := net.ParseCIDR("1.2.3.4/32"); return net.IP(ipnet.Mask) }(), }, + addrs: []netip.Addr{ + netip.AddrFrom4([4]byte{0xFF, 0xFF, 0xFF, 0xFF}), + netip.MustParseAddr("255.255.255.255"), + }, }, { desc: "IPv6", @@ -106,6 +124,11 @@ var goodTestIPs = []testIP{ func() net.IP { ip, _, _ := net.ParseCIDR("2001:db8::5/64"); return ip }(), func() net.IP { _, ipnet, _ := net.ParseCIDR("2001:db8::5/128"); return ipnet.IP }(), }, + addrs: []netip.Addr{ + netip.AddrFrom16([16]byte{0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x05}), + netip.MustParseAddr("2001:db8::5"), + netip.MustParsePrefix("2001:db8::5/64").Addr(), + }, }, { desc: "IPv6 all-zeros", @@ -126,6 +149,12 @@ var goodTestIPs = []testIP{ func() net.IP { _, ipnet, _ := net.ParseCIDR("::/0"); return ipnet.IP }(), func() net.IP { _, ipnet, _ := net.ParseCIDR("::/0"); return net.IP(ipnet.Mask) }(), }, + addrs: []netip.Addr{ + netip.IPv6Unspecified(), + netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + netip.MustParseAddr("::"), + netip.MustParsePrefix("::/0").Addr(), + }, }, { desc: "IPv6 loopback", @@ -139,6 +168,11 @@ var goodTestIPs = []testIP{ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, net.ParseIP("::1"), }, + addrs: []netip.Addr{ + netip.IPv6Loopback(), + netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), + netip.MustParseAddr("::1"), + }, }, { desc: "IPv4-mapped IPv6", @@ -149,8 +183,9 @@ var goodTestIPs = []testIP{ // - The 4-byte and 16-byte forms of a given net.IP compare as .Equal(). // - Our parsers parse the plain IPv4 and IPv4-mapped IPv6 forms of an // IPv4 string to the same thing. - // - The 4-byte and 16-byte forms of a given net.IP all stringify - // to the plain IPv4 string form (i.e., .strings[0]). + // - The 4-byte and 16-byte forms of a given net.IP, and the 4-byte + // (but *not* 16-byte) form of netip.Addr, all stringify to the plain + // IPv4 string form (i.e., .strings[0]). family: IPv4, strings: []string{ "192.168.0.5", @@ -167,6 +202,30 @@ var goodTestIPs = []testIP{ net.ParseIP("::ffff:192.168.0.5").To4(), net.ParseIP("::ffff:192.168.0.5").To16(), }, + addrs: []netip.Addr{ + netip.AddrFrom4([4]byte{192, 168, 0, 5}), + netip.MustParseAddr("192.168.0.5"), + }, + }, + { + desc: "IPv4-mapped IPv6 (netip.Addr)", + // In constrast to net.IP, netip.Addr considers plain IPv4 and IPv4-mapped + // IPv6 to be distinct things, and netip.ParseAddr will parse the plain + // IPv4 and IPv4-mapped IPv6 strings into distinct netip.Addr values + // (where the IPv4-mapped IPv6 netip.Addr value does not correspond + // exactly to any net.IP value). + family: IPv4, + strings: []string{ + "::ffff:192.168.0.5", + }, + addrs: []netip.Addr{ + netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 192, 168, 0, 5}), + netip.MustParseAddr("::ffff:192.168.0.5"), + }, + + // Skip the parsing tests, because no netutils method will parse + // .strings[0] to .addrs[0]. + skipParse: true, }, } @@ -179,6 +238,7 @@ var goodTestIPs = []testIP{ // Parsing tests (unless `skipParse: true`): // - Each element of .strings should fail to parse. // - Each element of .ips should stringify to an error value that fails to parse. +// - Each element of .addrs should stringify to an error value that fails to parse. var badTestIPs = []testIP{ { desc: "empty string is not an IP", @@ -265,14 +325,21 @@ var badTestIPs = []testIP{ {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}, }, }, + { + desc: "the zero netip.Addr is invalid", + addrs: []netip.Addr{ + {}, + }, + }, } // testCIDR represents a set of equivalent CIDR representations. type testCIDR struct { - desc string - family IPFamily - strings []string - ipnets []*net.IPNet + desc string + family IPFamily + strings []string + ipnets []*net.IPNet + prefixes []netip.Prefix skipFamily bool skipParse bool @@ -282,6 +349,8 @@ type testCIDR struct { // // Preconditions: // - Each element of .ipnets stringifies to .strings[0]. +// - Each element of .prefixes is the same (i.e., ==). +// - Each element of .prefixes stringifies to .strings[0]. // // IPFamily tests (unless `skipFamily: true`): // - Each element of .strings should be identified as .family. @@ -305,6 +374,11 @@ var goodTestCIDRs = []testCIDR{ {IP: net.ParseIP("1.2.3.0"), Mask: net.CIDRMask(24, 32)}, func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.2.3.0/24"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.MustParsePrefix("1.2.3.0/24"), + netip.PrefixFrom(netip.MustParseAddr("1.2.3.0"), 24), + netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 0}), 24), + }, }, { desc: "IPv4, single IP", @@ -316,6 +390,10 @@ var goodTestCIDRs = []testCIDR{ {IP: net.IPv4(1, 1, 1, 1), Mask: net.CIDRMask(32, 32)}, func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.1.1.1/32"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.MustParsePrefix("1.1.1.1/32"), + netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32), + }, }, { desc: "IPv4, all IPs", @@ -329,6 +407,11 @@ var goodTestCIDRs = []testCIDR{ {IP: net.IPv4(0, 0, 0, 0), Mask: net.CIDRMask(0, 32)}, func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("0.0.0.0/0"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.MustParsePrefix("0.0.0.0/0"), + netip.PrefixFrom(netip.AddrFrom4([4]byte{0, 0, 0, 0}), 0), + netip.PrefixFrom(netip.IPv4Unspecified(), 0), + }, }, { desc: "IPv4 ifaddr (masked)", @@ -346,6 +429,12 @@ var goodTestCIDRs = []testCIDR{ func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.2.3.0/24"); return ipnet }(), func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("1.2.3.4/24"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 0}), 24), + netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 24).Masked(), + netip.MustParsePrefix("1.2.3.0/24"), + netip.MustParsePrefix("1.2.3.4/24").Masked(), + }, }, { desc: "IPv4 ifaddr", @@ -356,6 +445,10 @@ var goodTestCIDRs = []testCIDR{ ipnets: []*net.IPNet{ {IP: net.IPv4(1, 2, 3, 4), Mask: net.CIDRMask(24, 32)}, }, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 24), + netip.MustParsePrefix("1.2.3.4/24"), + }, // The *net.IPNet return value of ParseCIDRSloppy() masks out the lower // bits, so the parsed version won't compare equal to .ipnets[0] @@ -374,6 +467,10 @@ var goodTestCIDRs = []testCIDR{ {IP: net.ParseIP("2001:db8::"), Mask: net.CIDRMask(64, 128)}, func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("2001:db8::/64"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.MustParsePrefix("2001:db8::/64"), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), 64), + }, }, { desc: "IPv6, all IPs", @@ -386,6 +483,11 @@ var goodTestCIDRs = []testCIDR{ {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.CIDRMask(0, 128)}, func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("::/0"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.MustParsePrefix("::/0"), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), 0), + netip.PrefixFrom(netip.IPv6Unspecified(), 0), + }, }, { desc: "IPv6, single IP", @@ -398,6 +500,10 @@ var goodTestCIDRs = []testCIDR{ {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}, Mask: net.CIDRMask(128, 128)}, func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("::1/128"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.MustParsePrefix("::1/128"), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), 128), + }, }, { desc: "IPv6 ifaddr (masked)", @@ -415,6 +521,12 @@ var goodTestCIDRs = []testCIDR{ func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("2001:db8::/64"); return ipnet }(), func() *net.IPNet { _, ipnet, _ := net.ParseCIDR("2001:db8::1/64"); return ipnet }(), }, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}), 64), + netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), 64).Masked(), + netip.MustParsePrefix("2001:db8::/64"), + netip.MustParsePrefix("2001:db8::1/64").Masked(), + }, }, { desc: "IPv6 ifaddr", @@ -425,6 +537,10 @@ var goodTestCIDRs = []testCIDR{ ipnets: []*net.IPNet{ {IP: net.IP{0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, Mask: net.CIDRMask(64, 128)}, }, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), 64), + netip.MustParsePrefix("2001:db8::1/64"), + }, // The *net.IPNet return value of ParseCIDRSloppy() masks out the lower // bits, so the parsed version won't compare equal to .ipnets[0] @@ -454,6 +570,27 @@ var goodTestCIDRs = []testCIDR{ {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 1, 1, 0}, Mask: net.IPMask{255, 255, 255, 0}}, {IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 1, 1, 0}, Mask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0}}, }, + prefixes: []netip.Prefix{ + netip.MustParsePrefix("1.1.1.0/24"), + netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 0}), 24), + }, + }, + { + // As in the IP/Addr tests, additional checks for IPv4-mapped IPv6 netip + // values. + desc: "IPv4-mapped IPv6 (netip.Prefix)", + family: IPv4, + strings: []string{ + "::ffff:1.1.1.0/120", + }, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 1, 1, 0}), 120), + netip.MustParsePrefix("::ffff:1.1.1.0/120"), + }, + + // Skip the parsing tests, because no netutils method will parse + // .strings[0] to .prefixes[0]. + skipParse: true, }, } @@ -466,6 +603,7 @@ var goodTestCIDRs = []testCIDR{ // Parsing tests (unless `skipParse: true`): // - Each element of .strings should fail to parse. // - Each element of .ipnets should stringify to some error value that fails to parse. +// - Each element of .prefixes should stringify to some error value that fails to parse. var badTestCIDRs = []testCIDR{ { desc: "empty string is not a CIDR", @@ -546,6 +684,34 @@ var badTestCIDRs = []testCIDR{ // IPFamilyOfCIDR only looks at IP and doesn't notice that Mask is invalid skipFamily: true, }, + { + desc: "the zero netip.Prefix is invalid", + family: IPFamilyUnknown, + prefixes: []netip.Prefix{ + {}, + }, + }, + { + desc: "Prefix containing an invalid Addr is invalid", + family: IPFamilyUnknown, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.Addr{}, 24), + }, + }, + { + desc: "Prefix containing a negative length is invalid", + family: IPv4, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.IPv4Unspecified(), -1), + }, + }, + { + desc: "Prefix containing a too-long length is invalid", + family: IPv4, + prefixes: []netip.Prefix{ + netip.PrefixFrom(netip.IPv4Unspecified(), 64), + }, + }, } // TestGoodTestIPs confirms the Preconditions for goodTestIPs. @@ -561,6 +727,16 @@ func TestGoodTestIPs(t *testing.T) { t.Errorf("BAD TEST DATA: IP %d %#v %q does not stringify to %q", i+1, ip, ip, tc.strings[0]) } } + + for i, addr := range tc.addrs { + if addr != tc.addrs[0] { + t.Errorf("BAD TEST DATA: Addr %d %#v %q does not equal %#v %q", i+1, addr, addr, tc.addrs[0], tc.addrs[0]) + } + str := addr.String() + if str != tc.strings[0] { + t.Errorf("BAD TEST DATA: Addr %d %#v %q does not stringify to %q", i+1, addr, addr, tc.strings[0]) + } + } }) } } @@ -574,6 +750,16 @@ func TestGoodTestCIDRs(t *testing.T) { t.Errorf("BAD TEST DATA: IPNet %d %#v %q does not stringify to %q", i+1, ipnet, ipnet, tc.strings[0]) } } + + for i, prefix := range tc.prefixes { + if prefix != tc.prefixes[0] { + t.Errorf("BAD TEST DATA: Prefix %d %#v %q does not equal %#v %q", i+1, prefix, prefix, tc.prefixes[0], tc.prefixes[0]) + } + str := prefix.String() + if str != tc.strings[0] { + t.Errorf("BAD TEST DATA: Prefix %d %#v %q does not stringify to %q", i+1, prefix, prefix, tc.strings[0]) + } + } }) } } diff --git a/net/v2/parse_test.go b/net/v2/parse_test.go index 40255927..7d451cf6 100644 --- a/net/v2/parse_test.go +++ b/net/v2/parse_test.go @@ -53,6 +53,14 @@ func TestParseIPSloppy(t *testing.T) { } } + for i, addr := range tc.addrs { + errStr := addr.String() + parsedIP := ParseIPSloppy(errStr) + if parsedIP != nil { + t.Errorf("expected Addr %d %#v (%q) to not re-parse but got %#v (%q)", i+1, addr, errStr, parsedIP, parsedIP.String()) + } + } + for i, str := range tc.strings { ip := ParseIPSloppy(str) if ip != nil { @@ -96,6 +104,14 @@ func TestParseCIDRSloppy(t *testing.T) { } } + for i, prefix := range tc.prefixes { + errStr := prefix.String() + _, parsedIPNet, err := ParseCIDRSloppy(errStr) + if err == nil { + t.Errorf("expected Prefix %d %#v %q to not parse but got %#v (%q)", i+1, prefix, errStr, *parsedIPNet, parsedIPNet.String()) + } + } + for i, str := range tc.strings { _, ipnet, err := ParseCIDRSloppy(str) if err == nil { From ebc1d8115314a5f78720454141f115ad2b683ca6 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 21 Jul 2025 17:11:29 -0400 Subject: [PATCH 06/17] Remove error returns for netutilsv2.IsDualStack* funcs 99% of callers are calling these functions on already-validated data, making the error return values just annoying. --- net/v2/ipfamily.go | 27 ++++++++++----------- net/v2/ipfamily_test.go | 54 +++-------------------------------------- 2 files changed, 17 insertions(+), 64 deletions(-) diff --git a/net/v2/ipfamily.go b/net/v2/ipfamily.go index 1a51fa39..7b28975f 100644 --- a/net/v2/ipfamily.go +++ b/net/v2/ipfamily.go @@ -17,7 +17,6 @@ limitations under the License. package net import ( - "fmt" "net" ) @@ -35,32 +34,32 @@ const ( // IsDualStackIPs returns true if: // - all elements of ips are valid // - at least one IP from each family (v4 and v6) is present -func IsDualStackIPs(ips []net.IP) (bool, error) { +func IsDualStackIPs(ips []net.IP) bool { v4Found := false v6Found := false - for i, ip := range ips { + for _, ip := range ips { switch IPFamilyOf(ip) { case IPv4: v4Found = true case IPv6: v6Found = true default: - return false, fmt.Errorf("invalid IP[%d]: %v", i, ip) + return false } } - return (v4Found && v6Found), nil + return (v4Found && v6Found) } // IsDualStackIPStrings returns true if: // - all elements of ips can be parsed as IPs // - at least one IP from each family (v4 and v6) is present -func IsDualStackIPStrings(ips []string) (bool, error) { +func IsDualStackIPStrings(ips []string) bool { parsedIPs := make([]net.IP, 0, len(ips)) - for i, ip := range ips { + for _, ip := range ips { parsedIP := ParseIPSloppy(ip) if parsedIP == nil { - return false, fmt.Errorf("invalid IP[%d]: %v", i, ip) + return false } parsedIPs = append(parsedIPs, parsedIP) } @@ -70,30 +69,30 @@ func IsDualStackIPStrings(ips []string) (bool, error) { // IsDualStackCIDRs returns true if: // - all elements of cidrs are non-nil // - at least one CIDR from each family (v4 and v6) is present -func IsDualStackCIDRs(cidrs []*net.IPNet) (bool, error) { +func IsDualStackCIDRs(cidrs []*net.IPNet) bool { v4Found := false v6Found := false - for i, cidr := range cidrs { + for _, cidr := range cidrs { switch IPFamilyOfCIDR(cidr) { case IPv4: v4Found = true case IPv6: v6Found = true default: - return false, fmt.Errorf("invalid CIDR[%d]: %v", i, cidr) + return false } } - return (v4Found && v6Found), nil + return (v4Found && v6Found) } // IsDualStackCIDRStrings returns if // - all elements of cidrs can be parsed as CIDRs // - at least one CIDR from each family (v4 and v6) is present -func IsDualStackCIDRStrings(cidrs []string) (bool, error) { +func IsDualStackCIDRStrings(cidrs []string) bool { parsedCIDRs, err := ParseCIDRs(cidrs) if err != nil { - return false, err + return false } return IsDualStackCIDRs(parsedCIDRs) } diff --git a/net/v2/ipfamily_test.go b/net/v2/ipfamily_test.go index 13dd954d..76433086 100644 --- a/net/v2/ipfamily_test.go +++ b/net/v2/ipfamily_test.go @@ -27,85 +27,67 @@ func TestDualStackIPs(t *testing.T) { desc string ips []string expectedResult bool - expectError bool }{ { desc: "should fail because length is not at least 2", ips: []string{"1.1.1.1"}, expectedResult: false, - expectError: false, }, { desc: "should fail because length is not at least 2", ips: []string{}, expectedResult: false, - expectError: false, }, { desc: "should fail because all are v4", ips: []string{"1.1.1.1", "2.2.2.2", "3.3.3.3"}, expectedResult: false, - expectError: false, }, { desc: "should fail because all are v6", ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff0", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff1"}, expectedResult: false, - expectError: false, }, { desc: "should fail because 2nd ip is invalid", ips: []string{"1.1.1.1", "not-a-valid-ip"}, expectedResult: false, - expectError: true, }, { desc: "should fail because 1st ip is invalid", ips: []string{"not-a-valid-ip", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff"}, expectedResult: false, - expectError: true, }, { desc: "should fail despite dual-stack because 3rd ip is invalid", ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "not-a-valid-ip"}, expectedResult: false, - expectError: true, }, { desc: "dual-stack ipv4-primary", ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff"}, expectedResult: true, - expectError: false, }, { desc: "dual-stack, multiple ipv6", ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:fff0"}, expectedResult: true, - expectError: false, }, { desc: "dual-stack, multiple ipv4", ips: []string{"1.1.1.1", "fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "10.0.0.0"}, expectedResult: true, - expectError: false, }, { desc: "dual-stack, ipv6-primary", ips: []string{"fd92:20ba:ca:34f7:ffff:ffff:ffff:ffff", "1.1.1.1"}, expectedResult: true, - expectError: false, }, } // for each test case, test the regular func and the string func for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - dualStack, err := IsDualStackIPStrings(tc.ips) - if err == nil && tc.expectError { - t.Fatalf("expected an error from IsDualStackIPStrings") - } - if err != nil && !tc.expectError { - t.Fatalf("unexpected error from IsDualStackIPStrings: %v", err) - } + dualStack := IsDualStackIPStrings(tc.ips) if dualStack != tc.expectedResult { t.Errorf("expected IsDualStackIPStrings=%v, got %v", tc.expectedResult, dualStack) } @@ -115,13 +97,7 @@ func TestDualStackIPs(t *testing.T) { parsedIP := ParseIPSloppy(ip) ips = append(ips, parsedIP) } - dualStack, err = IsDualStackIPs(ips) - if err == nil && tc.expectError { - t.Fatalf("expected an error from IsDualStackIPs") - } - if err != nil && !tc.expectError { - t.Fatalf("unexpected error from IsDualStackIPs: %v", err) - } + dualStack = IsDualStackIPs(ips) if dualStack != tc.expectedResult { t.Errorf("expected IsDualStackIPs=%v, got %v", tc.expectedResult, dualStack) } @@ -134,74 +110,58 @@ func TestDualStackCIDRs(t *testing.T) { desc string cidrs []string expectedResult bool - expectError bool }{ { desc: "should fail because length is not at least 2", cidrs: []string{"10.10.10.10/8"}, expectedResult: false, - expectError: false, }, { desc: "should fail because length is not at least 2", cidrs: []string{}, expectedResult: false, - expectError: false, }, { desc: "should fail because all cidrs are v4", cidrs: []string{"10.10.10.10/8", "20.20.20.20/8", "30.30.30.30/8"}, expectedResult: false, - expectError: false, }, { desc: "should fail because all cidrs are v6", cidrs: []string{"2000::/10", "3000::/10"}, expectedResult: false, - expectError: false, }, { desc: "should fail because 2nd cidr is invalid", cidrs: []string{"10.10.10.10/8", "not-a-valid-cidr"}, expectedResult: false, - expectError: true, }, { desc: "should fail because 1st cidr is invalid", cidrs: []string{"not-a-valid-ip", "2000::/10"}, expectedResult: false, - expectError: true, }, { desc: "dual-stack, ipv4-primary", cidrs: []string{"10.10.10.10/8", "2000::/10"}, expectedResult: true, - expectError: false, }, { desc: "dual-stack, ipv6-primary", cidrs: []string{"2000::/10", "10.10.10.10/8"}, expectedResult: true, - expectError: false, }, { desc: "dual-stack, multiple IPv6", cidrs: []string{"2000::/10", "10.10.10.10/8", "3000::/10"}, expectedResult: true, - expectError: false, }, } // for each test case, test the regular func and the string func for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - dualStack, err := IsDualStackCIDRStrings(tc.cidrs) - if err == nil && tc.expectError { - t.Fatalf("expected an error from IsDualStackCIDRStrings") - } - if err != nil && !tc.expectError { - t.Fatalf("unexpected error from IsDualStackCIDRStrings: %v", err) - } + dualStack := IsDualStackCIDRStrings(tc.cidrs) if dualStack != tc.expectedResult { t.Errorf("expected IsDualStackCIDRStrings=%v, got %v", tc.expectedResult, dualStack) } @@ -212,13 +172,7 @@ func TestDualStackCIDRs(t *testing.T) { cidrs = append(cidrs, parsedCIDR) } - dualStack, err = IsDualStackCIDRs(cidrs) - if err == nil && tc.expectError { - t.Fatalf("expected an error from IsDualStackCIDRs") - } - if err != nil && !tc.expectError { - t.Fatalf("unexpected error from IsDualStackCIDRs: %v", err) - } + dualStack = IsDualStackCIDRs(cidrs) if dualStack != tc.expectedResult { t.Errorf("expected IsDualStackCIDRs=%v, got %v", tc.expectedResult, dualStack) } From 128102a5d0f363d96d28396485fcf87c8c451b06 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Sun, 27 Jul 2025 17:12:08 -0400 Subject: [PATCH 07/17] Fix IPFamilyOf to notice bad *net.IPNets A manually-constructed IPNet could have a Mask that doesn't match its IP (in which case .String() would return "" and .Contains() would always return false). So treat that as IPFamilyUnknown. --- net/v2/ipfamily.go | 12 +++++++++--- net/v2/ips_test.go | 6 ------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/net/v2/ipfamily.go b/net/v2/ipfamily.go index 7b28975f..d1d970ef 100644 --- a/net/v2/ipfamily.go +++ b/net/v2/ipfamily.go @@ -117,10 +117,16 @@ func IPFamilyOfString(ip string) IPFamily { // IPFamilyOfCIDR returns the IP family of cidr. func IPFamilyOfCIDR(cidr *net.IPNet) IPFamily { - if cidr == nil { - return IPFamilyUnknown + if cidr != nil { + family := IPFamilyOf(cidr.IP) + // An IPv6 CIDR must have a 128-bit mask. An IPv4 CIDR must have a + // 32- or 128-bit mask. (Any other mask length is invalid.) + _, masklen := cidr.Mask.Size() + if masklen == 128 || (family == IPv4 && masklen == 32) { + return family + } } - return IPFamilyOf(cidr.IP) + return IPFamilyUnknown } // IPFamilyOfCIDRString returns the IP family of cidr. diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go index 32c8fe81..3e1577e3 100644 --- a/net/v2/ips_test.go +++ b/net/v2/ips_test.go @@ -671,18 +671,12 @@ var badTestCIDRs = []testCIDR{ ipnets: []*net.IPNet{ {IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 0, 255, 0}}, }, - - // IPFamilyOfCIDR only looks at IP and doesn't notice that Mask is invalid - skipFamily: true, }, { desc: "IPNet containing IPv6 IP and IPv4 Mask is invalid", ipnets: []*net.IPNet{ {IP: net.IP{0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, Mask: net.CIDRMask(24, 32)}, }, - - // IPFamilyOfCIDR only looks at IP and doesn't notice that Mask is invalid - skipFamily: true, }, { desc: "the zero netip.Prefix is invalid", From 744d44a16a0c54af927528f12a2d973a1f168f78 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 21 Jul 2025 17:07:16 -0400 Subject: [PATCH 08/17] Change netutilsv2.IPFamily to match corev1.IPFamily (so you can cast between them) --- net/v2/ipfamily.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/net/v2/ipfamily.go b/net/v2/ipfamily.go index d1d970ef..fef4f81a 100644 --- a/net/v2/ipfamily.go +++ b/net/v2/ipfamily.go @@ -20,15 +20,19 @@ import ( "net" ) -// IPFamily refers to a specific family if not empty, i.e. "4" or "6". +// IPFamily refers to the IP family of an address or CIDR value. Its values are +// intentionally identical to those of "k8s.io/api/core/v1".IPFamily and +// "k8s.io/discovery/v1".AddressType, so you can cast values between these types. type IPFamily string -// Constants for valid IPFamilys: const ( - IPFamilyUnknown IPFamily = "" + // IPv4 indicates an IPv4 IP or CIDR. + IPv4 IPFamily = "IPv4" + // IPv6 indicates an IPv4 IP or CIDR. + IPv6 IPFamily = "IPv6" - IPv4 IPFamily = "4" - IPv6 IPFamily = "6" + // IPFamilyUnknown indicates an unspecified or invalid IP family. + IPFamilyUnknown IPFamily = "" ) // IsDualStackIPs returns true if: From c16281d83634f60cfd21ba261413b1a99bdb6847 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Sun, 17 Nov 2024 15:59:46 -0500 Subject: [PATCH 09/17] Streamline netutilsv2 IP family functions, add a few more - Squash the String functions (eg, IsIPv4String, IsDualStackCIDRStrings) into their corresponding non-string versions (eg, IsIPv4, IsDualStackCIDRs). The new versions use generics to support both types. - Add IsDualStackPair and IsDualStackCIDRPair - Add OtherIPFamily --- net/v2/ipfamily.go | 218 +++++++++++++++++++--------------------- net/v2/ipfamily_test.go | 79 ++++++++------- 2 files changed, 148 insertions(+), 149 deletions(-) diff --git a/net/v2/ipfamily.go b/net/v2/ipfamily.go index fef4f81a..3200e5bf 100644 --- a/net/v2/ipfamily.go +++ b/net/v2/ipfamily.go @@ -35,14 +35,54 @@ const ( IPFamilyUnknown IPFamily = "" ) -// IsDualStackIPs returns true if: -// - all elements of ips are valid -// - at least one IP from each family (v4 and v6) is present -func IsDualStackIPs(ips []net.IP) bool { +type ipOrString interface { + net.IP | string +} + +type cidrOrString interface { + *net.IPNet | string +} + +// IPFamilyOf returns the IP family of val (or IPFamilyUnknown if val is nil or invalid). +// IPv6-encoded IPv4 addresses (e.g., "::ffff:1.2.3.4") are considered IPv4. val can be a +// net.IP or a string containing a single IP address. +// +// Note that "k8s.io/utils/net/v2".IPFamily intentionally has identical values to +// "k8s.io/api/core/v1".IPFamily and "k8s.io/discovery/v1".AddressType, so you can cast +// the return value of this function to those types. +func IPFamilyOf[T ipOrString](val T) IPFamily { + switch typedVal := interface{}(val).(type) { + case net.IP: + switch { + case typedVal.To4() != nil: + return IPv4 + case typedVal.To16() != nil: + return IPv6 + } + case string: + return IPFamilyOf(ParseIPSloppy(typedVal)) + } + + return IPFamilyUnknown +} + +// IsIPv4 returns true if IPFamilyOf(val) is IPv4 (and false if it is IPv6 or invalid). +func IsIPv4[T ipOrString](val T) bool { + return IPFamilyOf(val) == IPv4 +} + +// IsIPv6 returns true if IPFamilyOf(val) is IPv6 (and false if it is IPv4 or invalid). +func IsIPv6[T ipOrString](val T) bool { + return IPFamilyOf(val) == IPv6 +} + +// IsDualStack returns true if vals contains at least one IPv4 address and at least one +// IPv6 address (and no invalid values). +func IsDualStack[T ipOrString](vals []T) bool { v4Found := false v6Found := false - for _, ip := range ips { - switch IPFamilyOf(ip) { + for _, val := range vals { + switch IPFamilyOf(val) { case IPv4: v4Found = true case IPv6: @@ -55,29 +95,56 @@ func IsDualStackIPs(ips []net.IP) bool { return (v4Found && v6Found) } -// IsDualStackIPStrings returns true if: -// - all elements of ips can be parsed as IPs -// - at least one IP from each family (v4 and v6) is present -func IsDualStackIPStrings(ips []string) bool { - parsedIPs := make([]net.IP, 0, len(ips)) - for _, ip := range ips { - parsedIP := ParseIPSloppy(ip) - if parsedIP == nil { - return false +// IsDualStackPair returns true if vals contains exactly 1 IPv4 address and 1 IPv6 address +// (in either order). +func IsDualStackPair[T ipOrString](vals []T) bool { + return len(vals) == 2 && IsDualStack(vals) +} + +// IPFamilyOfCIDR returns the IP family of val (or IPFamilyUnknown if val is nil or +// invalid). IPv6-encoded IPv4 addresses (e.g., "::ffff:1.2.3.0/120") are considered IPv4. +// val can be a *net.IPNet or a string containing a single CIDR value. +// +// Note that "k8s.io/utils/net/v2".IPFamily intentionally has identical values to +// "k8s.io/api/core/v1".IPFamily and "k8s.io/discovery/v1".AddressType, so you can cast +// the return value of this function to those types. +func IPFamilyOfCIDR[T cidrOrString](val T) IPFamily { + switch typedVal := interface{}(val).(type) { + case *net.IPNet: + if typedVal != nil { + family := IPFamilyOf(typedVal.IP) + // An IPv6 CIDR must have a 128-bit mask. An IPv4 CIDR must have a + // 32- or 128-bit mask. (Any other mask length is invalid.) + _, masklen := typedVal.Mask.Size() + if masklen == 128 || (family == IPv4 && masklen == 32) { + return family + } } - parsedIPs = append(parsedIPs, parsedIP) + case string: + parsedIP, _, _ := ParseCIDRSloppy(typedVal) + return IPFamilyOf(parsedIP) } - return IsDualStackIPs(parsedIPs) + + return IPFamilyUnknown +} + +// IsIPv4CIDR returns true if IPFamilyOfCIDR(val) is IPv4 (and false if it is IPv6 or invalid). +func IsIPv4CIDR[T cidrOrString](val T) bool { + return IPFamilyOfCIDR(val) == IPv4 } -// IsDualStackCIDRs returns true if: -// - all elements of cidrs are non-nil -// - at least one CIDR from each family (v4 and v6) is present -func IsDualStackCIDRs(cidrs []*net.IPNet) bool { +// IsIPv6CIDR returns true if IPFamilyOfCIDR(val) is IPv6 (and false if it is IPv4 or invalid). +func IsIPv6CIDR[T cidrOrString](val T) bool { + return IPFamilyOfCIDR(val) == IPv6 +} + +// IsDualStackCIDRs returns true if vals contains at least one IPv4 CIDR value and at +// least one IPv6 CIDR value (and no invalid values). +func IsDualStackCIDRs[T cidrOrString](vals []T) bool { v4Found := false v6Found := false - for _, cidr := range cidrs { - switch IPFamilyOfCIDR(cidr) { + for _, val := range vals { + switch IPFamilyOfCIDR(val) { case IPv4: v4Found = true case IPv6: @@ -90,101 +157,24 @@ func IsDualStackCIDRs(cidrs []*net.IPNet) bool { return (v4Found && v6Found) } -// IsDualStackCIDRStrings returns if -// - all elements of cidrs can be parsed as CIDRs -// - at least one CIDR from each family (v4 and v6) is present -func IsDualStackCIDRStrings(cidrs []string) bool { - parsedCIDRs, err := ParseCIDRs(cidrs) - if err != nil { - return false - } - return IsDualStackCIDRs(parsedCIDRs) +// IsDualStackCIDRPair returns true if vals contains exactly 1 IPv4 CIDR value and 1 IPv6 +// CIDR value (in either order). +func IsDualStackCIDRPair[T cidrOrString](vals []T) bool { + return len(vals) == 2 && IsDualStackCIDRs(vals) } -// IPFamilyOf returns the IP family of ip, or IPFamilyUnknown if it is invalid. -func IPFamilyOf(ip net.IP) IPFamily { - switch { - case ip.To4() != nil: - return IPv4 - case ip.To16() != nil: +// OtherIPFamily returns the other IP family from ipFamily. +// +// Note that "k8s.io/utils/net/v2".IPFamily intentionally has identical values to +// "k8s.io/api/core/v1".IPFamily and "k8s.io/discovery/v1".AddressType, so you can cast +// the input/output values of this function between these types. +func OtherIPFamily(ipFamily IPFamily) IPFamily { + switch ipFamily { + case IPv4: return IPv6 + case IPv6: + return IPv4 default: return IPFamilyUnknown } } - -// IPFamilyOfString returns the IP family of ip, or IPFamilyUnknown if ip cannot -// be parsed as an IP. -func IPFamilyOfString(ip string) IPFamily { - return IPFamilyOf(ParseIPSloppy(ip)) -} - -// IPFamilyOfCIDR returns the IP family of cidr. -func IPFamilyOfCIDR(cidr *net.IPNet) IPFamily { - if cidr != nil { - family := IPFamilyOf(cidr.IP) - // An IPv6 CIDR must have a 128-bit mask. An IPv4 CIDR must have a - // 32- or 128-bit mask. (Any other mask length is invalid.) - _, masklen := cidr.Mask.Size() - if masklen == 128 || (family == IPv4 && masklen == 32) { - return family - } - } - return IPFamilyUnknown -} - -// IPFamilyOfCIDRString returns the IP family of cidr. -func IPFamilyOfCIDRString(cidr string) IPFamily { - ip, _, _ := ParseCIDRSloppy(cidr) - return IPFamilyOf(ip) -} - -// IsIPv6 returns true if netIP is IPv6 (and false if it is IPv4, nil, or invalid). -func IsIPv6(netIP net.IP) bool { - return IPFamilyOf(netIP) == IPv6 -} - -// IsIPv6String returns true if ip contains a single IPv6 address and nothing else. It -// returns false if ip is an empty string, an IPv4 address, or anything else that is not a -// single IPv6 address. -func IsIPv6String(ip string) bool { - return IPFamilyOfString(ip) == IPv6 -} - -// IsIPv6CIDR returns true if a cidr is a valid IPv6 CIDR. It returns false if cidr is -// nil or an IPv4 CIDR. Its behavior is not defined if cidr is invalid. -func IsIPv6CIDR(cidr *net.IPNet) bool { - return IPFamilyOfCIDR(cidr) == IPv6 -} - -// IsIPv6CIDRString returns true if cidr contains a single IPv6 CIDR and nothing else. It -// returns false if cidr is an empty string, an IPv4 CIDR, or anything else that is not a -// single valid IPv6 CIDR. -func IsIPv6CIDRString(cidr string) bool { - return IPFamilyOfCIDRString(cidr) == IPv6 -} - -// IsIPv4 returns true if netIP is IPv4 (and false if it is IPv6, nil, or invalid). -func IsIPv4(netIP net.IP) bool { - return IPFamilyOf(netIP) == IPv4 -} - -// IsIPv4String returns true if ip contains a single IPv4 address and nothing else. It -// returns false if ip is an empty string, an IPv6 address, or anything else that is not a -// single IPv4 address. -func IsIPv4String(ip string) bool { - return IPFamilyOfString(ip) == IPv4 -} - -// IsIPv4CIDR returns true if cidr is a valid IPv4 CIDR. It returns false if cidr is nil -// or an IPv6 CIDR. Its behavior is not defined if cidr is invalid. -func IsIPv4CIDR(cidr *net.IPNet) bool { - return IPFamilyOfCIDR(cidr) == IPv4 -} - -// IsIPv4CIDRString returns true if cidr contains a single IPv4 CIDR and nothing else. It -// returns false if cidr is an empty string, an IPv6 CIDR, or anything else that is not a -// single valid IPv4 CIDR. -func IsIPv4CIDRString(cidr string) bool { - return IPFamilyOfCIDRString(cidr) == IPv4 -} diff --git a/net/v2/ipfamily_test.go b/net/v2/ipfamily_test.go index 76433086..d8514df9 100644 --- a/net/v2/ipfamily_test.go +++ b/net/v2/ipfamily_test.go @@ -1,5 +1,5 @@ /* -Copyright 2018 The Kubernetes Authors. +Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import ( "testing" ) -func TestDualStackIPs(t *testing.T) { +func TestIsDualStack(t *testing.T) { testCases := []struct { desc string ips []string @@ -84,28 +84,33 @@ func TestDualStackIPs(t *testing.T) { expectedResult: true, }, } - // for each test case, test the regular func and the string func for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - dualStack := IsDualStackIPStrings(tc.ips) - if dualStack != tc.expectedResult { - t.Errorf("expected IsDualStackIPStrings=%v, got %v", tc.expectedResult, dualStack) + netips := make([]net.IP, len(tc.ips)) + for i := range tc.ips { + netips[i] = ParseIPSloppy(tc.ips[i]) } - ips := make([]net.IP, 0, len(tc.ips)) - for _, ip := range tc.ips { - parsedIP := ParseIPSloppy(ip) - ips = append(ips, parsedIP) + dualStack := IsDualStack(tc.ips) + if dualStack != tc.expectedResult { + t.Errorf("expected %v, []string got %v", tc.expectedResult, dualStack) } - dualStack = IsDualStackIPs(ips) + if IsDualStackPair(tc.ips) != (dualStack && len(tc.ips) == 2) { + t.Errorf("IsDualStackIPPair gave wrong result for []string") + } + + dualStack = IsDualStack(netips) if dualStack != tc.expectedResult { - t.Errorf("expected IsDualStackIPs=%v, got %v", tc.expectedResult, dualStack) + t.Errorf("expected %v []net.IP got %v", tc.expectedResult, dualStack) + } + if IsDualStackPair(netips) != (dualStack && len(tc.ips) == 2) { + t.Errorf("IsDualStackIPPair gave wrong result for []net.IP") } }) } } -func TestDualStackCIDRs(t *testing.T) { +func TestIsDualStackCIDRs(t *testing.T) { testCases := []struct { desc string cidrs []string @@ -158,23 +163,27 @@ func TestDualStackCIDRs(t *testing.T) { }, } - // for each test case, test the regular func and the string func for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - dualStack := IsDualStackCIDRStrings(tc.cidrs) - if dualStack != tc.expectedResult { - t.Errorf("expected IsDualStackCIDRStrings=%v, got %v", tc.expectedResult, dualStack) + ipnets := make([]*net.IPNet, len(tc.cidrs)) + for i := range tc.cidrs { + _, ipnets[i], _ = ParseCIDRSloppy(tc.cidrs[i]) } - cidrs := make([]*net.IPNet, 0, len(tc.cidrs)) - for _, cidr := range tc.cidrs { - _, parsedCIDR, _ := ParseCIDRSloppy(cidr) - cidrs = append(cidrs, parsedCIDR) + dualStack := IsDualStackCIDRs(tc.cidrs) + if dualStack != tc.expectedResult { + t.Errorf("expected %v []string got %v", tc.expectedResult, dualStack) + } + if IsDualStackCIDRPair(tc.cidrs) != (dualStack && len(tc.cidrs) == 2) { + t.Errorf("IsDualStackCIDRPair gave wrong result for []string") } - dualStack = IsDualStackCIDRs(cidrs) + dualStack = IsDualStackCIDRs(ipnets) if dualStack != tc.expectedResult { - t.Errorf("expected IsDualStackCIDRs=%v, got %v", tc.expectedResult, dualStack) + t.Errorf("expected %v []*net.IPNet got %v", tc.expectedResult, dualStack) + } + if IsDualStackCIDRPair(ipnets) != (dualStack && len(tc.cidrs) == 2) { + t.Errorf("IsDualStackCIDRPair gave wrong result for []*net.IPNet") } }) } @@ -201,9 +210,9 @@ func TestIPFamilyOf(t *testing.T) { } t.Run(tc.desc, func(t *testing.T) { for _, str := range tc.strings { - family := IPFamilyOfString(str) - isIPv4 := IsIPv4String(str) - isIPv6 := IsIPv6String(str) + family := IPFamilyOf(str) + isIPv4 := IsIPv4(str) + isIPv6 := IsIPv6(str) checkOneIPFamily(t, str, tc.family, family, isIPv4, isIPv6) } for _, ip := range tc.ips { @@ -228,9 +237,9 @@ func TestIPFamilyOf(t *testing.T) { checkOneIPFamily(t, fmt.Sprintf("%#v", ip), IPFamilyUnknown, family, isIPv4, isIPv6) } for _, str := range tc.strings { - family := IPFamilyOfString(str) - isIPv4 := IsIPv4String(str) - isIPv6 := IsIPv6String(str) + family := IPFamilyOf(str) + isIPv4 := IsIPv4(str) + isIPv6 := IsIPv6(str) checkOneIPFamily(t, str, IPFamilyUnknown, family, isIPv4, isIPv6) } }) @@ -245,9 +254,9 @@ func TestIPFamilyOfCIDR(t *testing.T) { } t.Run(tc.desc, func(t *testing.T) { for _, str := range tc.strings { - family := IPFamilyOfCIDRString(str) - isIPv4 := IsIPv4CIDRString(str) - isIPv6 := IsIPv6CIDRString(str) + family := IPFamilyOfCIDR(str) + isIPv4 := IsIPv4CIDR(str) + isIPv6 := IsIPv6CIDR(str) checkOneIPFamily(t, str, tc.family, family, isIPv4, isIPv6) } for _, ipnet := range tc.ipnets { @@ -276,9 +285,9 @@ func TestIPFamilyOfCIDR(t *testing.T) { checkOneIPFamily(t, str, IPFamilyUnknown, family, isIPv4, isIPv6) } for _, str := range tc.strings { - family := IPFamilyOfCIDRString(str) - isIPv4 := IsIPv4CIDRString(str) - isIPv6 := IsIPv6CIDRString(str) + family := IPFamilyOfCIDR(str) + isIPv4 := IsIPv4CIDR(str) + isIPv6 := IsIPv6CIDR(str) checkOneIPFamily(t, str, IPFamilyUnknown, family, isIPv4, isIPv6) } }) From afefe9803cea60b112ac19ab068f42e48d5143ed Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Sun, 17 Nov 2024 15:59:46 -0500 Subject: [PATCH 10/17] Add support for netip.Addr and netip.Prefix to IPFamily functions. All the functions that previously took a net.IP or string now also take a netip.Addr. All the functions that previously took a *net.IPNet or string now also take a netip.Prefix. --- net/v2/ipfamily.go | 21 +++++++++++++++---- net/v2/ipfamily_test.go | 45 +++++++++++++++++++++++++++++++++++++++++ net/v2/ips_test.go | 4 ++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/net/v2/ipfamily.go b/net/v2/ipfamily.go index 3200e5bf..1585f670 100644 --- a/net/v2/ipfamily.go +++ b/net/v2/ipfamily.go @@ -18,6 +18,7 @@ package net import ( "net" + "net/netip" ) // IPFamily refers to the IP family of an address or CIDR value. Its values are @@ -36,16 +37,16 @@ const ( ) type ipOrString interface { - net.IP | string + net.IP | netip.Addr | string } type cidrOrString interface { - *net.IPNet | string + *net.IPNet | netip.Prefix | string } // IPFamilyOf returns the IP family of val (or IPFamilyUnknown if val is nil or invalid). // IPv6-encoded IPv4 addresses (e.g., "::ffff:1.2.3.4") are considered IPv4. val can be a -// net.IP or a string containing a single IP address. +// net.IP, a netip.Addr, or a string containing a single IP address. // // Note that "k8s.io/utils/net/v2".IPFamily intentionally has identical values to // "k8s.io/api/core/v1".IPFamily and "k8s.io/discovery/v1".AddressType, so you can cast @@ -59,6 +60,13 @@ func IPFamilyOf[T ipOrString](val T) IPFamily { case typedVal.To16() != nil: return IPv6 } + case netip.Addr: + switch { + case typedVal.Is4(), typedVal.Is4In6(): + return IPv4 + case typedVal.Is6(): + return IPv6 + } case string: return IPFamilyOf(ParseIPSloppy(typedVal)) } @@ -103,7 +111,7 @@ func IsDualStackPair[T ipOrString](vals []T) bool { // IPFamilyOfCIDR returns the IP family of val (or IPFamilyUnknown if val is nil or // invalid). IPv6-encoded IPv4 addresses (e.g., "::ffff:1.2.3.0/120") are considered IPv4. -// val can be a *net.IPNet or a string containing a single CIDR value. +// val can be a *net.IPNet, a netip.Prefix, or a string containing a single CIDR value. // // Note that "k8s.io/utils/net/v2".IPFamily intentionally has identical values to // "k8s.io/api/core/v1".IPFamily and "k8s.io/discovery/v1".AddressType, so you can cast @@ -120,6 +128,11 @@ func IPFamilyOfCIDR[T cidrOrString](val T) IPFamily { return family } } + case netip.Prefix: + if !typedVal.IsValid() { + return IPFamilyUnknown + } + return IPFamilyOf(typedVal.Addr()) case string: parsedIP, _, _ := ParseCIDRSloppy(typedVal) return IPFamilyOf(parsedIP) diff --git a/net/v2/ipfamily_test.go b/net/v2/ipfamily_test.go index d8514df9..6788985c 100644 --- a/net/v2/ipfamily_test.go +++ b/net/v2/ipfamily_test.go @@ -19,6 +19,7 @@ package net import ( "fmt" "net" + "net/netip" "testing" ) @@ -87,8 +88,10 @@ func TestIsDualStack(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { netips := make([]net.IP, len(tc.ips)) + addrs := make([]netip.Addr, len(tc.ips)) for i := range tc.ips { netips[i] = ParseIPSloppy(tc.ips[i]) + addrs[i], _ = netip.ParseAddr(tc.ips[i]) } dualStack := IsDualStack(tc.ips) @@ -106,6 +109,14 @@ func TestIsDualStack(t *testing.T) { if IsDualStackPair(netips) != (dualStack && len(tc.ips) == 2) { t.Errorf("IsDualStackIPPair gave wrong result for []net.IP") } + + dualStack = IsDualStack(addrs) + if dualStack != tc.expectedResult { + t.Errorf("expected %v []netip.Addr got %v", tc.expectedResult, dualStack) + } + if IsDualStackPair(addrs) != (dualStack && len(tc.ips) == 2) { + t.Errorf("IsDualStackIPPair gave wrong result for []netip.Addr") + } }) } } @@ -166,8 +177,10 @@ func TestIsDualStackCIDRs(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { ipnets := make([]*net.IPNet, len(tc.cidrs)) + prefixes := make([]netip.Prefix, len(tc.cidrs)) for i := range tc.cidrs { _, ipnets[i], _ = ParseCIDRSloppy(tc.cidrs[i]) + prefixes[i], _ = netip.ParsePrefix(tc.cidrs[i]) } dualStack := IsDualStackCIDRs(tc.cidrs) @@ -185,6 +198,14 @@ func TestIsDualStackCIDRs(t *testing.T) { if IsDualStackCIDRPair(ipnets) != (dualStack && len(tc.cidrs) == 2) { t.Errorf("IsDualStackCIDRPair gave wrong result for []*net.IPNet") } + + dualStack = IsDualStackCIDRs(prefixes) + if dualStack != tc.expectedResult { + t.Errorf("expected %v []netip.Prefix got %v", tc.expectedResult, dualStack) + } + if IsDualStackCIDRPair(prefixes) != (dualStack && len(tc.cidrs) == 2) { + t.Errorf("IsDualStackCIDRPair gave wrong result for []netip.Prefix") + } }) } } @@ -221,6 +242,12 @@ func TestIPFamilyOf(t *testing.T) { isIPv6 := IsIPv6(ip) checkOneIPFamily(t, ip.String(), tc.family, family, isIPv4, isIPv6) } + for _, addr := range tc.addrs { + family := IPFamilyOf(addr) + isIPv4 := IsIPv4(addr) + isIPv6 := IsIPv6(addr) + checkOneIPFamily(t, addr.String(), tc.family, family, isIPv4, isIPv6) + } }) } @@ -236,6 +263,12 @@ func TestIPFamilyOf(t *testing.T) { isIPv6 := IsIPv6(ip) checkOneIPFamily(t, fmt.Sprintf("%#v", ip), IPFamilyUnknown, family, isIPv4, isIPv6) } + for _, addr := range tc.addrs { + family := IPFamilyOf(addr) + isIPv4 := IsIPv4(addr) + isIPv6 := IsIPv6(addr) + checkOneIPFamily(t, fmt.Sprintf("%#v", addr), IPFamilyUnknown, family, isIPv4, isIPv6) + } for _, str := range tc.strings { family := IPFamilyOf(str) isIPv4 := IsIPv4(str) @@ -265,6 +298,12 @@ func TestIPFamilyOfCIDR(t *testing.T) { isIPv6 := IsIPv6CIDR(ipnet) checkOneIPFamily(t, ipnet.String(), tc.family, family, isIPv4, isIPv6) } + for _, prefix := range tc.prefixes { + family := IPFamilyOfCIDR(prefix) + isIPv4 := IsIPv4CIDR(prefix) + isIPv6 := IsIPv6CIDR(prefix) + checkOneIPFamily(t, prefix.String(), tc.family, family, isIPv4, isIPv6) + } }) } @@ -284,6 +323,12 @@ func TestIPFamilyOfCIDR(t *testing.T) { } checkOneIPFamily(t, str, IPFamilyUnknown, family, isIPv4, isIPv6) } + for _, prefix := range tc.prefixes { + family := IPFamilyOfCIDR(prefix) + isIPv4 := IsIPv4CIDR(prefix) + isIPv6 := IsIPv6CIDR(prefix) + checkOneIPFamily(t, fmt.Sprintf("%#v", prefix), IPFamilyUnknown, family, isIPv4, isIPv6) + } for _, str := range tc.strings { family := IPFamilyOfCIDR(str) isIPv4 := IsIPv4CIDR(str) diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go index 3e1577e3..87445324 100644 --- a/net/v2/ips_test.go +++ b/net/v2/ips_test.go @@ -45,6 +45,7 @@ type testIP struct { // IPFamily tests (unless `skipFamily: true`): // - Each element of .strings should be identified as .family. // - Each element of .ips should be identified as .family. +// - Each element of .addrs should be identified as .family. // // Parsing tests (unless `skipParse: true`): // - Each element of .strings should parse to a value equal to .ips[0]. @@ -234,6 +235,7 @@ var goodTestIPs = []testIP{ // IPFamily tests (unless `skipFamily: true`): // - Each element of .strings should be identified as IPFamilyUnknown. // - Each element of .ips should be identified as IPFamilyUnknown. +// - Each element of .addrs should be identified as IPFamilyUnknown. // // Parsing tests (unless `skipParse: true`): // - Each element of .strings should fail to parse. @@ -355,6 +357,7 @@ type testCIDR struct { // IPFamily tests (unless `skipFamily: true`): // - Each element of .strings should be identified as .family. // - Each element of .ipnets should be identified as .family. +// - Each element of .prefixes should be identified as .family. // // Parsing tests (unless `skipParse: true`): // - Each element of .strings should parse to a value "equal" to .ipnets[0]. @@ -599,6 +602,7 @@ var goodTestCIDRs = []testCIDR{ // IPFamily tests (unless `skipFamily: true`): // - Each element of .strings should be identified as IPFamilyUnknown. // - Each element of .ipnets should be identified as IPFamilyUnknown. +// - Each element of .prefixes should be identified as IPFamilyUnknown. // // Parsing tests (unless `skipParse: true`): // - Each element of .strings should fail to parse. From 4ebd29e882669c18f5aa39df90a5b20ff5bf2b6c Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Tue, 21 May 2024 09:34:22 -0400 Subject: [PATCH 11/17] Add net/netip conversion functions to netutils We will need to deal with both net.IP/net.IPNet and netip.Addr/netip.Prefix for a while, so add functions to convert between the types. --- net/v2/convert.go | 166 +++++++++++++++++++++++++++++ net/v2/convert_test.go | 234 +++++++++++++++++++++++++++++++++++++++++ net/v2/ips_test.go | 40 ++++++- 3 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 net/v2/convert.go create mode 100644 net/v2/convert_test.go diff --git a/net/v2/convert.go b/net/v2/convert.go new file mode 100644 index 00000000..cdeeb70d --- /dev/null +++ b/net/v2/convert.go @@ -0,0 +1,166 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "net" + "net/netip" +) + +// AddrFromIP converts a net.IP to a netip.Addr. Given valid input this will always +// succeed; it will return the invalid netip.Addr on nil or garbage input. +// +// Use this rather than netip.AddrFromSlice(), which (despite the claims of its +// documentation) does not always do what you would expect if you pass it a net.IP. +func AddrFromIP(ip net.IP) netip.Addr { + // Naively using netip.AddrFromSlice() gives unexpected results: + // + // ip := net.ParseIP("1.2.3.4") + // addr, _ := netip.AddrFromSlice(ip) + // addr.String() => "::ffff:1.2.3.4" + // addr.Is4() => false + // addr.Is6() => true + // + // This is because net.IP and netip.Addr have different ideas about how to handle + // "IPv4-mapped IPv6" addresses, but netip.AddrFromSlice ignores that fact. + // + // In net.IP, parsing either "1.2.3.4" or "::ffff:1.2.3.4", will give you the + // same result: + // + // ip1 := net.ParseIP("1.2.3.4") + // []byte(ip1) => []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 2, 3, 4} + // ip1.String() => "1.2.3.4" + // ip2 := net.ParseIP("::ffff:1.2.3.4") + // []byte(ip2) => []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 2, 3, 4} + // ip2.String() => "1.2.3.4" + // + // net.IP normally stores IPv4 addresses as 16-byte IPv4-mapped IPv6 addresses, + // but it hides that from the user, and it never stringifies an IPv4 IP to an + // IPv4-mapped IPv6 form, even if that was the format you started with. + // + // net.IP *can* represent IPv4 addresses in a 4-byte format, but this is treated + // as completly equivalent to the 16-byte representation: + // + // ip4 := ip1.To4() + // []byte(ip4) => []byte{1, 2, 3, 4} + // ip4.String() => "1.2.3.4" + // ip1.Equal(ip4) => true + // + // netip.Addr, on the other hand, treats "plain" IPv4 and IPv4-mapped IPv6 as two + // completely separate things: + // + // a1 := netip.MustParseAddr("1.2.3.4") + // a2 := netip.MustParseAddr("::ffff:1.2.3.4") + // a1.String() => "1.2.3.4" + // a2.String() => "::ffff:1.2.3.4" + // a1 == a2 => false + // + // which would be fine, except that netip.AddrFromSlice breaks net.IP's normal + // semantics by converting the 4-byte and 16-byte net.IP forms to different + // netip.Addr values, giving the confusing results above. + // + // In order to correctly convert an IPv4 address from net.IP to netip.Addr, you + // need to either call .To4() on it before converting, or call .Unmap() on it + // after converting. (The latter option is slightly simpler for us here because we + // can just do it unconditionally, since it's a no-op in the IPv6 and invalid + // cases). + + addr, _ := netip.AddrFromSlice(ip) + return addr.Unmap() +} + +// IPFromAddr converts a netip.Addr to a net.IP. Given valid input this will always +// succeed; it will return nil if addr is the invalid netip.Addr. +func IPFromAddr(addr netip.Addr) net.IP { + // addr.AsSlice() returns: + // - a []byte of length 4 if addr is a normal IPv4 address + // - a []byte of length 16 if addr is an IPv6 address (including IPv4-mapped IPv6) + // - nil if addr is the zero Addr (which is the only other possibility) + // + // Any of those values can be correctly cast directly to a net.IP. + // + // Note that we don't bother to do any "cleanup" here like in the AddrFromIP case, + // so converting a plain IPv4 netip.Addr to net.IP gives a different result than + // converting an IPv4-mapped IPv6 netip.Addr: + // + // ip1 := netutils.IPFromAddr(netip.MustParseAddr("1.2.3.4")) + // []byte(ip1) => []byte{1, 2, 3, 4} + // + // ip2 := netutils.IPFromAddr(netip.MustParseAddr("::ffff:1.2.3.4")) + // []byte(ip2) => []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 2, 3, 4} + // + // However, the net.IP API treats the two values as the same anyway, so it doesn't + // matter. + // + // ip1.String() => "1.2.3.4" + // ip2.String() => "1.2.3.4" + // ip2.Equal(ip1) => true + + return net.IP(addr.AsSlice()) +} + +// PrefixFromIPNet converts a *net.IPNet to a netip.Prefix. Given valid input this will +// always succeed; it will return the invalid netip.Prefix on nil or garbage input. +func PrefixFromIPNet(ipnet *net.IPNet) netip.Prefix { + if ipnet == nil { + return netip.Prefix{} + } + + addr := AddrFromIP(ipnet.IP) + if !addr.IsValid() { + return netip.Prefix{} + } + + prefixLen, bits := ipnet.Mask.Size() + if prefixLen == 0 && bits == 0 { + // non-CIDR Mask representation; not representible as a netip.Prefix + return netip.Prefix{} + } + if bits == 128 && addr.Is4() && (bits-prefixLen <= 32) { + // In the same way that net.IP allows an IPv4 IP to be either 4 or 16 + // bytes (32 or 128 bits), *net.IPNet allows an IPv4 CIDR to have either a + // 32-bit or a 128-bit mask. If the mask is 128 bits, we discard the + // leftmost 96 bits. + prefixLen -= 128 - 32 + } else if bits != addr.BitLen() { + // invalid IPv4/IPv6 mix + return netip.Prefix{} + } + + return netip.PrefixFrom(addr, prefixLen) +} + +// IPNetFromPrefix converts a netip.Prefix to a *net.IPNet. Given valid input this will +// always succeed; it will return nil if prefix is the invalid netip.Prefix or is +// otherwise invalid. +func IPNetFromPrefix(prefix netip.Prefix) *net.IPNet { + addr := prefix.Addr() + bits := prefix.Bits() + if bits == -1 || !addr.IsValid() { + return nil + } + addrLen := addr.BitLen() + + // (As with IPFromAddr, a plain IPv4 netip.Prefix and an equivalent IPv4-mapped + // IPv6 netip.Prefix will get converted to distinct *net.IPNet values, but + // *net.IPNet will treat them equivalently.) + + return &net.IPNet{ + IP: IPFromAddr(addr), + Mask: net.CIDRMask(bits, addrLen), + } +} diff --git a/net/v2/convert_test.go b/net/v2/convert_test.go new file mode 100644 index 00000000..2be46dee --- /dev/null +++ b/net/v2/convert_test.go @@ -0,0 +1,234 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "fmt" + "net" + "net/netip" + "testing" +) + +func TestAddrFromIP(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestIPs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ip := range tc.ips { + addr := AddrFromIP(ip) + if tc.addrs[0] != addr { + t.Errorf("IP %d %#v %s converted to addr %q, but expected %q", i+1, ip, ip, addr, tc.addrs[0]) + } + + // No net.IP should convert to an IPv4-mapped IPv6 netip.Addr + if addr.Is4In6() { + t.Errorf("AddrFromIP() converted IP %d %#v %s to IPv4-mapped IPv6 Addr %#v %s", i+1, ip, ip, addr, addr) + } + // And thus every value should round-trip. + rtIP := IPFromAddr(addr) + if !ip.Equal(rtIP) { + t.Errorf("IP %d %#v %s round-tripped to %#v %s", i+1, ip, ip, rtIP, rtIP) + } + } + }) + } + + // See test cases in ips_test.go + for _, tc := range badTestIPs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ip := range tc.ips { + addr := AddrFromIP(ip) + if addr.IsValid() { + t.Errorf("Expected IP %d %#v to convert to invalid netip.Addr but got %#v %s", i+1, ip, addr, addr) + } + } + }) + } +} + +func TestIPFromAddr(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestIPs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, addr := range tc.addrs { + ip := IPFromAddr(addr) + if !ip.Equal(tc.ips[0]) { + t.Errorf("addr %d %#v %s converted to ip %q, but expected %q", i, addr, addr, ip, tc.ips[0]) + } + + // As long as addr is not IPv4-mapped IPv6, it should round-trip. + if !addr.Is4In6() { + rtAddr := AddrFromIP(ip) + if addr != rtAddr { + t.Errorf("Addr %d %#v %s round-tripped to %#v %s", i+1, addr, addr, rtAddr, rtAddr) + } + } + } + }) + } + + // Conversion of IPv4-mapped IPv6 is asymmetric because netip.Addr distinguishes + // plain IPv4 from IPv4-mapped IPv6, while net.IP does not. The "IPv4-mapped IPv6" + // test case in goodTestIPs covers most of the cases, but goodTestIPs has no way + // to describe the asymmetric part. + t.Run("IPv4-mapped IPv6 conversion from netip.Addr", func(t *testing.T) { + addr := netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 1, 2, 3, 4}) + if !addr.Is4In6() { + panic("failed to create IPv4-mapped IPv6 netip.Addr?") + } + + ip := IPFromAddr(addr) + expectedIP := net.IP{1, 2, 3, 4} + if !ip.Equal(expectedIP) { + t.Errorf("netip.Addr %q converted to %q, expected %q", addr, ip, expectedIP) + } + rtAddr := AddrFromIP(ip) + if rtAddr == addr { + t.Errorf("IPv4-mapped IPv6 netip.Addr unexpectedly round-tripped through net.IP!") + } + }) + + // See test cases in ips_test.go + for _, tc := range badTestIPs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, addr := range tc.addrs { + ip := IPFromAddr(addr) + if ip != nil { + t.Errorf("Expected Addr %d %#v to convert to invalid net.IP but got %#v %s", i+1, addr, ip, ip) + } + } + }) + } +} + +func TestPrefixFromIPNet(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestCIDRs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ipnet := range tc.ipnets { + prefix := PrefixFromIPNet(ipnet) + if tc.prefixes[0] != prefix { + t.Errorf("IPNet %d %#v %s converted to prefix %q, but expected %q", i+1, *ipnet, ipnet, prefix, tc.prefixes[0]) + } + + // No net.IPNet should convert to an IPv4-mapped IPv6 netip.Prefix + if prefix.Addr().Is4In6() { + t.Errorf("PrefixFromIPNet() converted IPNet %d %#v %s to IPv4-mapped IPv6 prefix %#v %s", i+1, *ipnet, ipnet, prefix, prefix) + } + // And thus every value should round-trip. + rtIPNet := IPNetFromPrefix(prefix) + if rtIPNet.String() != ipnet.String() { + t.Errorf("IPNet %d %#v %s round-tripped to %#v %s", i+1, *ipnet, ipnet, *rtIPNet, rtIPNet) + } + } + }) + } + + // See test cases in ips_test.go + for _, tc := range badTestCIDRs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ipnet := range tc.ipnets { + prefix := PrefixFromIPNet(ipnet) + if prefix.IsValid() { + str := "" + if ipnet != nil { + str = fmt.Sprintf("%#v", *ipnet) + } + t.Errorf("Expected IPNet %d %s to convert to invalid netip.Prefix but got %#v %s", i+1, str, prefix, prefix) + } + } + }) + } +} + +func TestIPNetFromPrefix(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestCIDRs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, prefix := range tc.prefixes { + ipnet := IPNetFromPrefix(prefix) + if ipnet.String() != tc.ipnets[0].String() { + t.Errorf("prefix %d %#v %s converted to ipnet %q, but expected %q", i, prefix, prefix, ipnet, tc.ipnets[0]) + } + + // As long as addr is not IPv4-mapped IPv6, it should round-trip. + if !prefix.Addr().Is4In6() { + rtPrefix := PrefixFromIPNet(ipnet) + if prefix != rtPrefix { + t.Errorf("prefix %d %#v %s round-tripped to %#v %s", i+1, prefix, prefix, rtPrefix, rtPrefix) + } + } + } + }) + } + + // Conversion of IPv4-mapped IPv6 is asymmetric because netip.Addr distinguishes + // plain IPv4 from IPv4-mapped IPv6, while net.IP does not. The "IPv4-mapped IPv6" + // test case in goodTestCIDRs covers most of the cases, but goodTestCIDRs has no way + // to describe the asymmetric part. + t.Run("IPv4-mapped IPv6 conversion from netip.Prefix", func(t *testing.T) { + prefix := netip.MustParsePrefix("::ffff:1.2.3.0/120") + if !prefix.Addr().Is4In6() { + panic("failed to create IPv4-mapped IPv6 netip.Addr?") + } + + ipnet := IPNetFromPrefix(prefix) + expected := "1.2.3.0/24" + if ipnet.String() != expected { + t.Errorf("netip.Prefix %q converted to %q, expected %q", prefix, ipnet.String(), expected) + } + rtPrefix := PrefixFromIPNet(ipnet) + if rtPrefix == prefix { + t.Errorf("IPv4-mapped IPv6 netip.Prefix unexpectedly round-tripped through net.IPNet!") + } + }) + + // See test cases in ips_test.go + for _, tc := range badTestCIDRs { + if tc.skipConvert { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, prefix := range tc.prefixes { + ipnet := IPNetFromPrefix(prefix) + if ipnet != nil { + t.Errorf("Expected Prefix %d %#v to convert to invalid net.IPNet but got %#v %s", i+1, prefix, *ipnet, ipnet) + } + } + }) + } +} diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go index 87445324..f1152580 100644 --- a/net/v2/ips_test.go +++ b/net/v2/ips_test.go @@ -30,8 +30,9 @@ type testIP struct { ips []net.IP addrs []netip.Addr - skipFamily bool - skipParse bool + skipFamily bool + skipParse bool + skipConvert bool } // goodTestIPs are "good" test IP values. For each item: @@ -49,6 +50,10 @@ type testIP struct { // // Parsing tests (unless `skipParse: true`): // - Each element of .strings should parse to a value equal to .ips[0]. +// +// Conversion tests (unless `skipConvert: true`): +// - Each element of .ips should convert to a value equal to .addrs[0]. +// - Each element of .addrs should convert to a value equal to .ips[0]. var goodTestIPs = []testIP{ { desc: "IPv4", @@ -227,6 +232,13 @@ var goodTestIPs = []testIP{ // Skip the parsing tests, because no netutils method will parse // .strings[0] to .addrs[0]. skipParse: true, + + // Skip the conversion tests, because there is no net.IP value that + // unambiguously corresponds to these netip.Addr values. TestIPFromAddr() + // has a special case to test that an IPv4-mapped IPv6 netip.Addr maps to + // the expected net.IP value (which then doesn't round-trip back to the + // original netip.Addr value). + skipConvert: true, }, } @@ -241,6 +253,10 @@ var goodTestIPs = []testIP{ // - Each element of .strings should fail to parse. // - Each element of .ips should stringify to an error value that fails to parse. // - Each element of .addrs should stringify to an error value that fails to parse. +// +// Conversion tests (unless `skipConvert: true`: +// - Each element of .ips should convert to an invalid netip.Addr. +// - Each element of .addrs should convert to a nil net.IP. var badTestIPs = []testIP{ { desc: "empty string is not an IP", @@ -343,8 +359,9 @@ type testCIDR struct { ipnets []*net.IPNet prefixes []netip.Prefix - skipFamily bool - skipParse bool + skipFamily bool + skipParse bool + skipConvert bool } // goodTestCIDRs are "good" test CIDR values. For each item: @@ -362,6 +379,10 @@ type testCIDR struct { // Parsing tests (unless `skipParse: true`): // - Each element of .strings should parse to a value "equal" to .ipnets[0]. // +// Conversion tests (unless `skipConvert: true`): +// - Each element of .ipnets should convert to a value equal to .prefixes[0]. +// - Each element of .prefixes should convert to a value "equal" to .ipnets[0]. +// // (Unlike net.IP, *net.IPNet has no `.Equal()` method, and testing equality "by hand" is // complicated (there are 4 equivalent representations of every IPv4 CIDR value), so we // just consider two *net.IPNet values to be equal if they stringify to the same value.) @@ -594,6 +615,13 @@ var goodTestCIDRs = []testCIDR{ // Skip the parsing tests, because no netutils method will parse // .strings[0] to .prefixes[0]. skipParse: true, + + // Skip the conversion tests, because there is no *net.IPNet value that + // unambiguously corresponds to these netip.Prefix values. + // TestIPNetFromPrefix() has a special case to test that a netip.Prefix + // with an IPv4-mapped IPv6 address maps to the expected *net.IPNet value + // (which then doesn't round-trip back to the original netip.Prefix value). + skipConvert: true, }, } @@ -608,6 +636,10 @@ var goodTestCIDRs = []testCIDR{ // - Each element of .strings should fail to parse. // - Each element of .ipnets should stringify to some error value that fails to parse. // - Each element of .prefixes should stringify to some error value that fails to parse. +// +// Conversion tests (unless `skipConvert: true`): +// - Each element of .ipnets should convert to an invalid netip.Prefix. +// - Each element of .prefixes should convert to a nil *net.IPNet. var badTestCIDRs = []testCIDR{ { desc: "empty string is not a CIDR", From 95913290691fa86df49ecfb8d98870a65e9451cd Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Tue, 21 May 2024 09:38:01 -0400 Subject: [PATCH 12/17] Add net.InterfaceAddrs() conversion functions to netutils net.InterfaceAddrs() has a weird and useless return value that various pieces of code parse in variously correct ways. Add functions to reliably convert its return values to net.IP or netip.Addr. --- net/v2/convert.go | 35 +++++++++++++++++ net/v2/convert_test.go | 85 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/net/v2/convert.go b/net/v2/convert.go index cdeeb70d..4898a13d 100644 --- a/net/v2/convert.go +++ b/net/v2/convert.go @@ -19,6 +19,7 @@ package net import ( "net" "net/netip" + "strings" ) // AddrFromIP converts a net.IP to a netip.Addr. Given valid input this will always @@ -113,6 +114,40 @@ func IPFromAddr(addr netip.Addr) net.IP { return net.IP(addr.AsSlice()) } +// IPFromInterfaceAddr can be used to extract the underlying IP address value (as a +// net.IP) from the return values of net.InterfaceAddrs(), net.Interface.Addrs(), or +// net.Interface.MulticastAddrs(). (net.Addr is also used in some other APIs, but this +// function should not be used on net.Addrs that are not "interface addresses".) +func IPFromInterfaceAddr(ifaddr net.Addr) net.IP { + // On both Linux and Windows, the values returned from the "interface address" + // methods are currently *net.IPNet for unicast addresses or *net.IPAddr for + // multicast addresses. + if ipnet, ok := ifaddr.(*net.IPNet); ok { + return ipnet.IP + } else if ipaddr, ok := ifaddr.(*net.IPAddr); ok { + return ipaddr.IP + } + + // Try to deal with other similar types... in particular, this is needed for + // some existing unit tests... + addrStr := ifaddr.String() + // If it has a subnet length (like net.IPNet) or optional zone identifier (like + // net.IPAddr), trim that away. + if end := strings.IndexAny(addrStr, "/%"); end != -1 { + addrStr = addrStr[:end] + } + // What's left is either an IP address, or something we can't parse. + return ParseIPSloppy(addrStr) +} + +// AddrFromInterfaceAddr can be used to extract the underlying IP address value (as a +// netip.Addr) from the return values of net.InterfaceAddrs(), net.Interface.Addrs(), or +// net.Interface.MulticastAddrs(). (net.Addr is also used in some other APIs, but this +// function should not be used on net.Addrs that are not "interface addresses".) +func AddrFromInterfaceAddr(ifaddr net.Addr) netip.Addr { + return AddrFromIP(IPFromInterfaceAddr(ifaddr)) +} + // PrefixFromIPNet converts a *net.IPNet to a netip.Prefix. Given valid input this will // always succeed; it will return the invalid netip.Prefix on nil or garbage input. func PrefixFromIPNet(ipnet *net.IPNet) netip.Prefix { diff --git a/net/v2/convert_test.go b/net/v2/convert_test.go index 2be46dee..96798ff9 100644 --- a/net/v2/convert_test.go +++ b/net/v2/convert_test.go @@ -126,6 +126,91 @@ func TestIPFromAddr(t *testing.T) { } } +type dummyNetAddr string + +func (d dummyNetAddr) Network() string { + return "dummy" +} +func (d dummyNetAddr) String() string { + return string(d) +} + +func TestIPFromInterfaceAddr_AddrFromInterfaceAddr(t *testing.T) { + testCases := []struct { + desc string + ifaddr net.Addr + out string + }{ + { + desc: "net.IPNet", + ifaddr: &net.IPNet{IP: net.IP{192, 168, 1, 1}, Mask: net.CIDRMask(24, 32)}, + out: "192.168.1.1", + }, + { + desc: "net.IPAddr", + ifaddr: &net.IPAddr{IP: net.IP{192, 168, 1, 2}}, + out: "192.168.1.2", + }, + { + desc: "net.IPAddr with zone", + ifaddr: &net.IPAddr{IP: net.IP{192, 168, 1, 3}, Zone: "eth0"}, + out: "192.168.1.3", + }, + { + desc: "net.TCPAddr", + ifaddr: &net.TCPAddr{IP: net.IP{192, 168, 1, 4}, Port: 80}, + out: "", + }, + { + desc: "unknown plain IP", + ifaddr: dummyNetAddr("192.168.1.5"), + out: "192.168.1.5", + }, + { + desc: "unknown CIDR", + ifaddr: dummyNetAddr("192.168.1.6/24"), + out: "192.168.1.6", + }, + { + desc: "unknown IP with zone", + ifaddr: dummyNetAddr("192.168.1.7%eth0"), + out: "192.168.1.7", + }, + { + desc: "unknown sockaddr", + ifaddr: dummyNetAddr("192.168.1.8:80"), + out: "", + }, + { + desc: "unknown junk", + ifaddr: dummyNetAddr("junk"), + out: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + ip := IPFromInterfaceAddr(tc.ifaddr) + addr := AddrFromInterfaceAddr(tc.ifaddr) + if tc.out == "" { + if ip != nil { + t.Errorf("expected IPFromInterfaceAddr to return nil but got %q", ip.String()) + } + if addr.IsValid() { + t.Errorf("expected AddrFromInterfaceAddr to return zero but got %q", addr.String()) + } + } else { + if ip.String() != tc.out { + t.Errorf("expected IPFromInterfaceAddr to return %q but got %q", tc.out, ip.String()) + } + if addr.String() != tc.out { + t.Errorf("expected AddrFromInterfaceAddr to return %q but got %q", tc.out, addr.String()) + } + } + }) + } +} + func TestPrefixFromIPNet(t *testing.T) { // See test cases in ips_test.go for _, tc := range goodTestCIDRs { From e02f2037773b1fd1851b5e79dad9fbdd43395a3c Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 21 Jul 2025 17:40:33 -0400 Subject: [PATCH 13/17] Add replacements for Parse{IP,CIDR}Sloppy to netutils v2 Replace ParseIPSloppy and ParseCIDRSloppy with ParseIP and ParseIPNet, where both have a single return value plus an error. --- net/v2/convert.go | 3 +- net/v2/ipfamily.go | 9 +++-- net/v2/ipfamily_test.go | 4 +-- net/v2/ips_test.go | 7 ++-- net/v2/multi_listen_test.go | 55 +++++++++++++++++-------------- net/v2/net.go | 2 +- net/v2/net_test.go | 4 +-- net/v2/parse.go | 66 ++++++++++++++++++++++++++++++------- net/v2/parse_test.go | 24 +++++++------- 9 files changed, 112 insertions(+), 62 deletions(-) diff --git a/net/v2/convert.go b/net/v2/convert.go index 4898a13d..f37e0c87 100644 --- a/net/v2/convert.go +++ b/net/v2/convert.go @@ -137,7 +137,8 @@ func IPFromInterfaceAddr(ifaddr net.Addr) net.IP { addrStr = addrStr[:end] } // What's left is either an IP address, or something we can't parse. - return ParseIPSloppy(addrStr) + ip, _ := ParseIP(addrStr) + return ip } // AddrFromInterfaceAddr can be used to extract the underlying IP address value (as a diff --git a/net/v2/ipfamily.go b/net/v2/ipfamily.go index 1585f670..93b71b2e 100644 --- a/net/v2/ipfamily.go +++ b/net/v2/ipfamily.go @@ -68,7 +68,9 @@ func IPFamilyOf[T ipOrString](val T) IPFamily { return IPv6 } case string: - return IPFamilyOf(ParseIPSloppy(typedVal)) + if ip, _ := ParseIP(typedVal); ip != nil { + return IPFamilyOf(ip) + } } return IPFamilyUnknown @@ -134,8 +136,9 @@ func IPFamilyOfCIDR[T cidrOrString](val T) IPFamily { } return IPFamilyOf(typedVal.Addr()) case string: - parsedIP, _, _ := ParseCIDRSloppy(typedVal) - return IPFamilyOf(parsedIP) + if ipnet, _ := ParseIPNet(typedVal); ipnet != nil { + return IPFamilyOf(ipnet.IP) + } } return IPFamilyUnknown diff --git a/net/v2/ipfamily_test.go b/net/v2/ipfamily_test.go index 6788985c..0a4d0fa0 100644 --- a/net/v2/ipfamily_test.go +++ b/net/v2/ipfamily_test.go @@ -90,7 +90,7 @@ func TestIsDualStack(t *testing.T) { netips := make([]net.IP, len(tc.ips)) addrs := make([]netip.Addr, len(tc.ips)) for i := range tc.ips { - netips[i] = ParseIPSloppy(tc.ips[i]) + netips[i], _ = ParseIP(tc.ips[i]) addrs[i], _ = netip.ParseAddr(tc.ips[i]) } @@ -179,7 +179,7 @@ func TestIsDualStackCIDRs(t *testing.T) { ipnets := make([]*net.IPNet, len(tc.cidrs)) prefixes := make([]netip.Prefix, len(tc.cidrs)) for i := range tc.cidrs { - _, ipnets[i], _ = ParseCIDRSloppy(tc.cidrs[i]) + ipnets[i], _ = ParseIPNet(tc.cidrs[i]) prefixes[i], _ = netip.ParsePrefix(tc.cidrs[i]) } diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go index f1152580..529935d8 100644 --- a/net/v2/ips_test.go +++ b/net/v2/ips_test.go @@ -440,8 +440,8 @@ var goodTestCIDRs = []testCIDR{ { desc: "IPv4 ifaddr (masked)", // This tests that if you try to parse an "ifaddr-style" CIDR string with - // ParseCIDR/ParseCIDRSloppy, the *net.IPNet return value has the bits - // beyond the prefix length masked out. + // ParseIPNet/ParsePrefix, the return value has the bits beyond the prefix + // length masked out. family: IPv4, strings: []string{ "1.2.3.0/24", @@ -532,8 +532,7 @@ var goodTestCIDRs = []testCIDR{ { desc: "IPv6 ifaddr (masked)", // This tests that if you try to parse an "ifaddr-style" CIDR string with - // ParseCIDRSloppy, the *net.IPNet return value has the bits beyond the - // prefix length masked out. + // ParseIPNet, it value has the bits beyond the prefix length masked out. family: IPv6, strings: []string{ "2001:db8::/64", diff --git a/net/v2/multi_listen_test.go b/net/v2/multi_listen_test.go index 9a10feb7..712758df 100644 --- a/net/v2/multi_listen_test.go +++ b/net/v2/multi_listen_test.go @@ -26,8 +26,13 @@ import ( "sync/atomic" "testing" "time" + + forkednet "k8s.io/utils/internal/third_party/forked/golang/net" ) +// Temporary +var parseIPSloppy = forkednet.ParseIP + type fakeCon struct { remoteAddr net.Addr } @@ -113,7 +118,7 @@ func listenFuncFactory(listeners []*fakeListener) func(_ context.Context, networ } listener := listeners[index] addr := &net.TCPAddr{ - IP: ParseIPSloppy(host), + IP: parseIPSloppy(host), Port: port, } if err != nil { @@ -265,14 +270,14 @@ func TestMultiListen_Close(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{{ - conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}, + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}, }}}, { connErrPairs: []connErrPair{{ - conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}, + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}, }, }}, { connErrPairs: []connErrPair{{ - conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}, + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}, }}, }}, }, @@ -294,13 +299,13 @@ func TestMultiListen_Close(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}}, }, }}, acceptCalls: 3, @@ -381,13 +386,13 @@ func TestMultiListen_Accept(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}}, }, }}, acceptCalls: 3, @@ -412,24 +417,24 @@ func TestMultiListen_Accept(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 30001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 30001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 40001}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 40002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 40001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 40002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50001}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50002}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50003}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("172.16.20.10"), Port: 50004}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50004}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60001}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60002}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60003}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60004}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 60005}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60004}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60005}}}, }, }}, acceptCalls: 3, @@ -452,13 +457,13 @@ func TestMultiListen_Accept(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("10.10.10.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("192.168.1.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: ParseIPSloppy("127.0.0.1"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}}, }, }}, acceptCalls: 1, diff --git a/net/v2/net.go b/net/v2/net.go index 704c1f23..c0c0f3c2 100644 --- a/net/v2/net.go +++ b/net/v2/net.go @@ -30,7 +30,7 @@ import ( func ParseCIDRs(cidrsString []string) ([]*net.IPNet, error) { cidrs := make([]*net.IPNet, 0, len(cidrsString)) for i, cidrString := range cidrsString { - _, cidr, err := ParseCIDRSloppy(cidrString) + cidr, err := ParseIPNet(cidrString) if err != nil { return nil, fmt.Errorf("invalid CIDR[%d]: %v (%v)", i, cidr, err) } diff --git a/net/v2/net_test.go b/net/v2/net_test.go index 0c8ffe1d..d8ce7c79 100644 --- a/net/v2/net_test.go +++ b/net/v2/net_test.go @@ -162,7 +162,7 @@ func TestRangeSize(t *testing.T) { } for _, tc := range testCases { - _, cidr, err := ParseCIDRSloppy(tc.cidr) + cidr, err := ParseIPNet(tc.cidr) if err != nil { t.Errorf("failed to parse cidr for test %s, unexpected error: '%s'", tc.name, err) } @@ -228,7 +228,7 @@ func TestGetIndexedIP(t *testing.T) { } for _, tc := range testCases { - _, subnet, err := ParseCIDRSloppy(tc.cidr) + subnet, err := ParseIPNet(tc.cidr) if err != nil { t.Errorf("failed to parse cidr %s, unexpected error: '%s'", tc.cidr, err) } diff --git a/net/v2/parse.go b/net/v2/parse.go index 400d364d..0b2b3b08 100644 --- a/net/v2/parse.go +++ b/net/v2/parse.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Kubernetes Authors. +Copyright 2024 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,17 +17,59 @@ limitations under the License. package net import ( + "fmt" + "net" + forkednet "k8s.io/utils/internal/third_party/forked/golang/net" ) -// ParseIPSloppy is identical to Go's standard net.ParseIP, except that it allows -// leading '0' characters on numbers. Go used to allow this and then changed -// the behavior in 1.17. We're choosing to keep it for compat with potential -// stored values. -var ParseIPSloppy = forkednet.ParseIP - -// ParseCIDRSloppy is identical to Go's standard net.ParseCIDR, except that it allows -// leading '0' characters on numbers. Go used to allow this and then changed -// the behavior in 1.17. We're choosing to keep it for compat with potential -// stored values. -var ParseCIDRSloppy = forkednet.ParseCIDR +// ParseIP parses an IPv4 or IPv6 address to a net.IP. This accepts both fully-valid IP +// addresses and irregular/ambiguous forms, making it usable for both validated and +// non-validated input strings. It should be used instead of net.ParseIP (which rejects +// some strings we need to accept for backward compatibility) and the old +// netutilsv1.ParseIPSloppy. +func ParseIP(ipStr string) (net.IP, error) { + // Note: if we want to get rid of forkednet, we should be able to use some + // invocation of regexp.ReplaceAllString to get rid of leading 0s in ipStr. + ip := forkednet.ParseIP(ipStr) + if ip != nil { + return ip, nil + } + + if ipStr == "" { + return nil, fmt.Errorf("expected an IP address") + } + // NB: we use forkednet.ParseCIDR directly, not ParseIPNet, to avoid recursing + // between ParseIP and ParseIPNet. + if _, _, err := forkednet.ParseCIDR(ipStr); err == nil { + return nil, fmt.Errorf("expected an IP address, got a CIDR value") + } + return nil, fmt.Errorf("not a valid IP address") +} + +// ParseIPNet parses an IPv4 or IPv6 CIDR string representing a subnet or mask, to a +// *net.IPNet. This accepts both fully-valid CIDR values and irregular/ambiguous forms, +// making it usable for both validated and non-validated input strings. It should be used +// instead of net.ParseCIDR (which rejects some strings that we need to accept for +// backward-compatibility) and the old netutilsv1.ParseCIDRSloppy. +// +// The return value is equivalent to the second return value from net.ParseCIDR. Note that +// this means that if the CIDR string has bits set beyond the prefix length (e.g., the "5" +// in "192.168.1.5/24"), those bits are simply discarded. +func ParseIPNet(cidrStr string) (*net.IPNet, error) { + // Note: if we want to get rid of forkednet, we should be able to use some + // invocation of regexp.ReplaceAllString to get rid of leading 0s in cidrStr. + if _, ipnet, err := forkednet.ParseCIDR(cidrStr); err == nil { + return ipnet, nil + } + + if cidrStr == "" { + return nil, fmt.Errorf("expected a CIDR value") + } + // NB: we use forkednet.ParseIP directly, not our own ParseIP, to avoid recursing + // between ParseIPNet and ParseIP. + if forkednet.ParseIP(cidrStr) != nil { + return nil, fmt.Errorf("expected a CIDR value, but got IP address") + } + return nil, fmt.Errorf("not a valid CIDR value") +} diff --git a/net/v2/parse_test.go b/net/v2/parse_test.go index 7d451cf6..e655dd64 100644 --- a/net/v2/parse_test.go +++ b/net/v2/parse_test.go @@ -20,7 +20,7 @@ import ( "testing" ) -func TestParseIPSloppy(t *testing.T) { +func TestParseIP(t *testing.T) { // See test cases in ips_test.go for _, tc := range goodTestIPs { if tc.skipParse { @@ -28,9 +28,9 @@ func TestParseIPSloppy(t *testing.T) { } t.Run(tc.desc, func(t *testing.T) { for i, str := range tc.strings { - ip := ParseIPSloppy(str) - if ip == nil { - t.Errorf("expected %q to parse, but failed", str) + ip, err := ParseIP(str) + if err != nil { + t.Errorf("expected %q to parse, but got error %v", str, err) } if !ip.Equal(tc.ips[0]) { t.Errorf("expected string %d %q to parse equal to IP %#v %q but got %#v (%q)", i+1, str, tc.ips[0], tc.ips[0].String(), ip, ip.String()) @@ -47,7 +47,7 @@ func TestParseIPSloppy(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { for i, ip := range tc.ips { errStr := ip.String() - parsedIP := ParseIPSloppy(errStr) + parsedIP, _ := ParseIP(errStr) if parsedIP != nil { t.Errorf("expected IP %d %#v (%q) to not re-parse but got %#v (%q)", i+1, ip, errStr, parsedIP, parsedIP.String()) } @@ -55,14 +55,14 @@ func TestParseIPSloppy(t *testing.T) { for i, addr := range tc.addrs { errStr := addr.String() - parsedIP := ParseIPSloppy(errStr) + parsedIP, _ := ParseIP(errStr) if parsedIP != nil { t.Errorf("expected Addr %d %#v (%q) to not re-parse but got %#v (%q)", i+1, addr, errStr, parsedIP, parsedIP.String()) } } for i, str := range tc.strings { - ip := ParseIPSloppy(str) + ip, _ := ParseIP(str) if ip != nil { t.Errorf("expected string %d %q to not parse but got %#v (%q)", i+1, str, ip, ip.String()) } @@ -71,7 +71,7 @@ func TestParseIPSloppy(t *testing.T) { } } -func TestParseCIDRSloppy(t *testing.T) { +func TestParseIPNet(t *testing.T) { // See test cases in ips_test.go for _, tc := range goodTestCIDRs { if tc.skipParse { @@ -79,7 +79,7 @@ func TestParseCIDRSloppy(t *testing.T) { } t.Run(tc.desc, func(t *testing.T) { for i, str := range tc.strings { - _, ipnet, err := ParseCIDRSloppy(str) + ipnet, err := ParseIPNet(str) if err != nil { t.Errorf("expected %q to parse, but got error %v", str, err) } @@ -98,7 +98,7 @@ func TestParseCIDRSloppy(t *testing.T) { t.Run(tc.desc, func(t *testing.T) { for i, ipnet := range tc.ipnets { errStr := ipnet.String() - _, parsedIPNet, err := ParseCIDRSloppy(errStr) + parsedIPNet, err := ParseIPNet(errStr) if err == nil { t.Errorf("expected IPNet %d %q to not parse but got %#v (%q)", i+1, errStr, *parsedIPNet, parsedIPNet.String()) } @@ -106,14 +106,14 @@ func TestParseCIDRSloppy(t *testing.T) { for i, prefix := range tc.prefixes { errStr := prefix.String() - _, parsedIPNet, err := ParseCIDRSloppy(errStr) + parsedIPNet, err := ParseIPNet(errStr) if err == nil { t.Errorf("expected Prefix %d %#v %q to not parse but got %#v (%q)", i+1, prefix, errStr, *parsedIPNet, parsedIPNet.String()) } } for i, str := range tc.strings { - _, ipnet, err := ParseCIDRSloppy(str) + ipnet, err := ParseIPNet(str) if err == nil { t.Errorf("expected string %d %q to not parse but got %#v (%q)", i+1, str, *ipnet, ipnet.String()) } From 37e5ce7d536e8f1017f2cefaec819b077abd6eac Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 21 Jul 2025 17:47:20 -0400 Subject: [PATCH 14/17] Add ParseAddr, ParsePrefix to netutilsv2 --- net/v2/ipfamily_test.go | 4 +- net/v2/ips_test.go | 4 ++ net/v2/parse.go | 46 ++++++++++++++++++ net/v2/parse_test.go | 101 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 2 deletions(-) diff --git a/net/v2/ipfamily_test.go b/net/v2/ipfamily_test.go index 0a4d0fa0..886addff 100644 --- a/net/v2/ipfamily_test.go +++ b/net/v2/ipfamily_test.go @@ -91,7 +91,7 @@ func TestIsDualStack(t *testing.T) { addrs := make([]netip.Addr, len(tc.ips)) for i := range tc.ips { netips[i], _ = ParseIP(tc.ips[i]) - addrs[i], _ = netip.ParseAddr(tc.ips[i]) + addrs[i], _ = ParseAddr(tc.ips[i]) } dualStack := IsDualStack(tc.ips) @@ -180,7 +180,7 @@ func TestIsDualStackCIDRs(t *testing.T) { prefixes := make([]netip.Prefix, len(tc.cidrs)) for i := range tc.cidrs { ipnets[i], _ = ParseIPNet(tc.cidrs[i]) - prefixes[i], _ = netip.ParsePrefix(tc.cidrs[i]) + prefixes[i], _ = ParsePrefix(tc.cidrs[i]) } dualStack := IsDualStackCIDRs(tc.cidrs) diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go index 529935d8..c916841f 100644 --- a/net/v2/ips_test.go +++ b/net/v2/ips_test.go @@ -50,6 +50,7 @@ type testIP struct { // // Parsing tests (unless `skipParse: true`): // - Each element of .strings should parse to a value equal to .ips[0]. +// - Each element of .strings should parse to a value equal to .addrs[0]. // // Conversion tests (unless `skipConvert: true`): // - Each element of .ips should convert to a value equal to .addrs[0]. @@ -184,6 +185,8 @@ var goodTestIPs = []testIP{ desc: "IPv4-mapped IPv6", // net.IP can represent an IPv4 address internally as either a 4-byte // value or a 16-byte value, but it treats the two forms as equivalent. + // Because IPv4-mapped IPv6 is annoying, we make our ParseAddr() behave + // this way too, even though that's *not* how netip.ParseAddr() behaves. // // This test case confirms that: // - The 4-byte and 16-byte forms of a given net.IP compare as .Equal(). @@ -378,6 +381,7 @@ type testCIDR struct { // // Parsing tests (unless `skipParse: true`): // - Each element of .strings should parse to a value "equal" to .ipnets[0]. +// - Each element of .strings should parse to a value equal to .prefixes[0]. // // Conversion tests (unless `skipConvert: true`): // - Each element of .ipnets should convert to a value equal to .prefixes[0]. diff --git a/net/v2/parse.go b/net/v2/parse.go index 0b2b3b08..136e6d98 100644 --- a/net/v2/parse.go +++ b/net/v2/parse.go @@ -19,6 +19,7 @@ package net import ( "fmt" "net" + "net/netip" forkednet "k8s.io/utils/internal/third_party/forked/golang/net" ) @@ -28,6 +29,8 @@ import ( // non-validated input strings. It should be used instead of net.ParseIP (which rejects // some strings we need to accept for backward compatibility) and the old // netutilsv1.ParseIPSloppy. +// +// Compare ParseAddr, which returns a netip.Addr but is otherwise identical. func ParseIP(ipStr string) (net.IP, error) { // Note: if we want to get rid of forkednet, we should be able to use some // invocation of regexp.ReplaceAllString to get rid of leading 0s in ipStr. @@ -47,6 +50,25 @@ func ParseIP(ipStr string) (net.IP, error) { return nil, fmt.Errorf("not a valid IP address") } +// ParseAddr parses an IPv4 or IPv6 address to a netip.Addr. This accepts both fully-valid +// IP addresses and irregular/ambiguous forms, making it usable for both validated and +// non-validated input strings. As compared to netip.ParseAddr: +// +// - It accepts IPv4 IPs with extra leading "0"s, for backward compatibility. +// - It rejects IPs with attached zone identifiers (e.g. `"fe80::1234%eth0"`). +// - It converts "IPv4-mapped IPv6" addresses (e.g. `"::ffff:1.2.3.4"`) to the +// corresponding "plain" IPv4 values (e.g. `"1.2.3.4"`). That is, it never returns an +// address for which `Is4In6()` would return `true`. +// +// Compare ParseIP, which returns a net.IP but is otherwise identical. +func ParseAddr(ipStr string) (netip.Addr, error) { + // To ensure identical parsing, we use ParseIP and then convert. (If ParseIP + // returns a nil ip, AddrFromIP will convert that to the zero/invalid netip.Addr, + // which is what we want.) + ip, err := ParseIP(ipStr) + return AddrFromIP(ip), err +} + // ParseIPNet parses an IPv4 or IPv6 CIDR string representing a subnet or mask, to a // *net.IPNet. This accepts both fully-valid CIDR values and irregular/ambiguous forms, // making it usable for both validated and non-validated input strings. It should be used @@ -56,6 +78,8 @@ func ParseIP(ipStr string) (net.IP, error) { // The return value is equivalent to the second return value from net.ParseCIDR. Note that // this means that if the CIDR string has bits set beyond the prefix length (e.g., the "5" // in "192.168.1.5/24"), those bits are simply discarded. +// +// Compare ParsePrefix, which returns a netip.Prefix but is otherwise identical. func ParseIPNet(cidrStr string) (*net.IPNet, error) { // Note: if we want to get rid of forkednet, we should be able to use some // invocation of regexp.ReplaceAllString to get rid of leading 0s in cidrStr. @@ -73,3 +97,25 @@ func ParseIPNet(cidrStr string) (*net.IPNet, error) { } return nil, fmt.Errorf("not a valid CIDR value") } + +// ParsePrefix parses an IPv4 or IPv6 CIDR string representing a subnet or mask, to a +// netip.Prefix. This accepts both fully-valid CIDR values and irregular/ambiguous forms, +// making it usable for both validated and non-validated input strings. As compared to +// netip.ParsePrefix: +// +// - It accepts IPv4 IPs with extra leading "0"s, for backward compatibility. +// - It converts "IPv4-mapped IPv6" addresses (e.g. `"::ffff:1.2.3.0/120"`) to the +// corresponding "plain" IPv4 values (e.g. `"1.2.3.0/24"`). That is, it never returns +// a prefix for which `.Addr().Is4In6()` would return `true`. +// - When given a CIDR string with bits set beyond the prefix length, like +// `"192.168.1.5/24"`, it discards those extra bits (the equivalent of calling +// .Masked() on the return value of netip.ParsePrefix). +// +// Compare ParseIPNet, which returns a *net.IPNet but is otherwise identical. +func ParsePrefix(cidrStr string) (netip.Prefix, error) { + // To ensure identical parsing, we use ParseIPNet and then convert. (If ParseIPNet + // returns nil, PrefixFromIPNet will convert that to the zero/invalid + // netip.Prefix, which is what we want.) + ipnet, err := ParseIPNet(cidrStr) + return PrefixFromIPNet(ipnet), err +} diff --git a/net/v2/parse_test.go b/net/v2/parse_test.go index e655dd64..c0703351 100644 --- a/net/v2/parse_test.go +++ b/net/v2/parse_test.go @@ -71,6 +71,56 @@ func TestParseIP(t *testing.T) { } } +func TestParseAddr(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestIPs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, str := range tc.strings { + addr, err := ParseAddr(str) + if err != nil { + t.Errorf("expected %q to parse, but got error %v", str, err) + } + if addr != tc.addrs[0] { + t.Errorf("expected string %d %q to parse equal to Addr %#v %q but got %#v (%q)", i+1, str, tc.addrs[0], tc.addrs[0].String(), addr, addr.String()) + } + } + }) + } + + for _, tc := range badTestIPs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ip := range tc.ips { + errStr := ip.String() + parsedAddr, err := ParseAddr(errStr) + if err == nil { + t.Errorf("expected IP %d %#v (%q) to not re-parse but got %#v (%q)", i+1, ip, errStr, parsedAddr, parsedAddr.String()) + } + } + + for i, addr := range tc.addrs { + errStr := addr.String() + parsedAddr, err := ParseAddr(errStr) + if err == nil { + t.Errorf("expected Addr %d %#v (%q) to not re-parse but got %#v (%q)", i+1, addr, errStr, parsedAddr, parsedAddr.String()) + } + } + + for i, str := range tc.strings { + addr, err := ParseAddr(str) + if err == nil { + t.Errorf("expected string %d %q to not parse but got %#v (%q)", i+1, str, addr, addr.String()) + } + } + }) + } +} + func TestParseIPNet(t *testing.T) { // See test cases in ips_test.go for _, tc := range goodTestCIDRs { @@ -121,3 +171,54 @@ func TestParseIPNet(t *testing.T) { }) } } + +func TestParsePrefix(t *testing.T) { + // See test cases in ips_test.go + for _, tc := range goodTestCIDRs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, str := range tc.strings { + prefix, err := ParsePrefix(str) + if err != nil { + t.Errorf("expected %q to parse, but got error %v", str, err) + } + if prefix != tc.prefixes[0] { + t.Errorf("expected string %d %q to parse equal to Prefix %#v %q but got %#v (%q)", i+1, str, tc.prefixes[0], tc.prefixes[0].String(), prefix, prefix.String()) + } + } + }) + } + + // See test cases in ips_test.go + for _, tc := range badTestCIDRs { + if tc.skipParse { + continue + } + t.Run(tc.desc, func(t *testing.T) { + for i, ipnet := range tc.ipnets { + errStr := ipnet.String() + parsedPrefix, err := ParsePrefix(errStr) + if err == nil { + t.Errorf("expected IPNet %d %q to not parse but got %#v (%q)", i+1, errStr, parsedPrefix, parsedPrefix.String()) + } + } + + for i, prefix := range tc.prefixes { + errStr := prefix.String() + parsedPrefix, err := ParsePrefix(errStr) + if err == nil { + t.Errorf("expected Prefix %d %q to not parse but got %#v (%q)", i+1, errStr, parsedPrefix, parsedPrefix.String()) + } + } + + for i, str := range tc.strings { + prefix, err := ParsePrefix(str) + if err == nil { + t.Errorf("expected string %d %q to not parse but got %#v (%q)", i+1, str, prefix, prefix.String()) + } + } + }) + } +} From a96029c45d577360d37501819dc7cea62b9e9c67 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Mon, 21 Jul 2025 17:40:33 -0400 Subject: [PATCH 15/17] Add ParseIPAsIPNet/ParseAddrAsPrefix to parse "ifaddr"-type CIDR strings --- net/v2/ips_test.go | 23 ++++++++--------- net/v2/parse.go | 60 ++++++++++++++++++++++++++++++++++++++------ net/v2/parse_test.go | 52 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 19 deletions(-) diff --git a/net/v2/ips_test.go b/net/v2/ips_test.go index c916841f..8013b815 100644 --- a/net/v2/ips_test.go +++ b/net/v2/ips_test.go @@ -357,6 +357,7 @@ var badTestIPs = []testIP{ // testCIDR represents a set of equivalent CIDR representations. type testCIDR struct { desc string + ifaddr bool family IPFamily strings []string ipnets []*net.IPNet @@ -380,8 +381,10 @@ type testCIDR struct { // - Each element of .prefixes should be identified as .family. // // Parsing tests (unless `skipParse: true`): -// - Each element of .strings should parse to a value "equal" to .ipnets[0]. -// - Each element of .strings should parse to a value equal to .prefixes[0]. +// - Each element of .strings should parse to a value "equal" to .ipnets[0] +// (via ParseIPNet if .ifaddr is false, or ParseIPAsIPNet if .ifaddr is true). +// - Each element of .strings should parse to a value equal to .prefixes[0] +// (via ParsePrefix if .ifaddr is false, or ParseAddrAsPrefix if .ifaddr is true). // // Conversion tests (unless `skipConvert: true`): // - Each element of .ipnets should convert to a value equal to .prefixes[0]. @@ -442,7 +445,8 @@ var goodTestCIDRs = []testCIDR{ }, }, { - desc: "IPv4 ifaddr (masked)", + desc: "IPv4 ifaddr (masked)", + ifaddr: false, // This tests that if you try to parse an "ifaddr-style" CIDR string with // ParseIPNet/ParsePrefix, the return value has the bits beyond the prefix // length masked out. @@ -466,6 +470,7 @@ var goodTestCIDRs = []testCIDR{ }, { desc: "IPv4 ifaddr", + ifaddr: true, family: IPv4, strings: []string{ "1.2.3.4/24", @@ -477,10 +482,6 @@ var goodTestCIDRs = []testCIDR{ netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 2, 3, 4}), 24), netip.MustParsePrefix("1.2.3.4/24"), }, - - // The *net.IPNet return value of ParseCIDRSloppy() masks out the lower - // bits, so the parsed version won't compare equal to .ipnets[0] - skipParse: true, }, { desc: "IPv6", @@ -534,7 +535,8 @@ var goodTestCIDRs = []testCIDR{ }, }, { - desc: "IPv6 ifaddr (masked)", + desc: "IPv6 ifaddr (masked)", + ifaddr: false, // This tests that if you try to parse an "ifaddr-style" CIDR string with // ParseIPNet, it value has the bits beyond the prefix length masked out. family: IPv6, @@ -557,6 +559,7 @@ var goodTestCIDRs = []testCIDR{ }, { desc: "IPv6 ifaddr", + ifaddr: true, family: IPv6, strings: []string{ "2001:db8::1/64", @@ -568,10 +571,6 @@ var goodTestCIDRs = []testCIDR{ netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}), 64), netip.MustParsePrefix("2001:db8::1/64"), }, - - // The *net.IPNet return value of ParseCIDRSloppy() masks out the lower - // bits, so the parsed version won't compare equal to .ipnets[0] - skipParse: true, }, { desc: "IPv4-mapped IPv6", diff --git a/net/v2/parse.go b/net/v2/parse.go index 136e6d98..8b80adb7 100644 --- a/net/v2/parse.go +++ b/net/v2/parse.go @@ -77,25 +77,51 @@ func ParseAddr(ipStr string) (netip.Addr, error) { // // The return value is equivalent to the second return value from net.ParseCIDR. Note that // this means that if the CIDR string has bits set beyond the prefix length (e.g., the "5" -// in "192.168.1.5/24"), those bits are simply discarded. +// in "192.168.1.5/24"), those bits are simply discarded. Use ParseIPAsIPNet instead if +// you want a *net.IPNet value containing the complete IP. // // Compare ParsePrefix, which returns a netip.Prefix but is otherwise identical. func ParseIPNet(cidrStr string) (*net.IPNet, error) { + _, ipnet, err := parseIPNetInternal(cidrStr) + return ipnet, err +} + +// ParseIPAsIPNet parses an IPv4 or IPv6 CIDR string representing an IP address and subnet +// mask, to a *net.IPNet. This accepts both fully-valid CIDR values and +// irregular/ambiguous forms, making it usable for both validated and non-validated input +// strings. It should be used instead of net.ParseCIDR (which rejects some strings that we +// need to accept for backward-compatibility) and the old netutilsv1.ParseCIDRSloppy. +// +// The return value combines the two return values of net.ParseCIDR; the returned +// *net.IPNet's IP field will contain a net.IP corresponding to the full IP address from +// the CIDR string, while the Mask field will contain a net.IPMask corresponding to the +// CIDR's prefix length. +// +// Compare ParseAddrAsPrefix, which returns a netip.Prefix, but is otherwise identical. +func ParseIPAsIPNet(cidrStr string) (*net.IPNet, error) { + ip, ipnet, err := parseIPNetInternal(cidrStr) + if err != nil { + return nil, err + } + return &net.IPNet{IP: ip, Mask: ipnet.Mask}, nil +} + +func parseIPNetInternal(cidrStr string) (net.IP, *net.IPNet, error) { // Note: if we want to get rid of forkednet, we should be able to use some // invocation of regexp.ReplaceAllString to get rid of leading 0s in cidrStr. - if _, ipnet, err := forkednet.ParseCIDR(cidrStr); err == nil { - return ipnet, nil + if ip, ipnet, err := forkednet.ParseCIDR(cidrStr); err == nil { + return ip, ipnet, nil } if cidrStr == "" { - return nil, fmt.Errorf("expected a CIDR value") + return nil, nil, fmt.Errorf("expected a CIDR value") } // NB: we use forkednet.ParseIP directly, not our own ParseIP, to avoid recursing // between ParseIPNet and ParseIP. if forkednet.ParseIP(cidrStr) != nil { - return nil, fmt.Errorf("expected a CIDR value, but got IP address") + return nil, nil, fmt.Errorf("expected a CIDR value, but got IP address") } - return nil, fmt.Errorf("not a valid CIDR value") + return nil, nil, fmt.Errorf("not a valid CIDR value") } // ParsePrefix parses an IPv4 or IPv6 CIDR string representing a subnet or mask, to a @@ -109,7 +135,8 @@ func ParseIPNet(cidrStr string) (*net.IPNet, error) { // a prefix for which `.Addr().Is4In6()` would return `true`. // - When given a CIDR string with bits set beyond the prefix length, like // `"192.168.1.5/24"`, it discards those extra bits (the equivalent of calling -// .Masked() on the return value of netip.ParsePrefix). +// .Masked() on the return value of netip.ParsePrefix). Use ParseAddrAsPrefix instead +// if you want a netip.Prefix value containing the complete IP. // // Compare ParseIPNet, which returns a *net.IPNet but is otherwise identical. func ParsePrefix(cidrStr string) (netip.Prefix, error) { @@ -119,3 +146,22 @@ func ParsePrefix(cidrStr string) (netip.Prefix, error) { ipnet, err := ParseIPNet(cidrStr) return PrefixFromIPNet(ipnet), err } + +// ParseAddrAsPrefix parses an IPv4 or IPv6 CIDR string representing an IP address and +// subnet mask, to a netip.Prefix. This accepts both fully-valid CIDR values and +// irregular/ambiguous forms, making it usable for both validated and non-validated input +// strings. As with ParsePrefix, this should be used rather than netip.ParsePrefix, +// for backward-compatibility and better handling of ambiguous values. +// +// The return value is identical to the value returned from ParsePrefix except in the +// case of CIDR strings with bits set beyond the prefix length, which are preserved by +// ParseAddrAsPrefix but discarded by ParsePrefix. +// +// Compare ParseIPAsIPNet, which returns a *net.IPNet, but is otherwise identical. +func ParseAddrAsPrefix(cidrStr string) (netip.Prefix, error) { + // To ensure identical parsing, we use ParseIPAsIPNet and then convert. (If + // ParseIPAsIPNet returns nil, PrefixFromIPNet will convert that to the + // zero/invalid netip.Prefix, which is what we want.) + ipnet, err := ParseIPAsIPNet(cidrStr) + return PrefixFromIPNet(ipnet), err +} diff --git a/net/v2/parse_test.go b/net/v2/parse_test.go index c0703351..c42d1f04 100644 --- a/net/v2/parse_test.go +++ b/net/v2/parse_test.go @@ -133,6 +133,32 @@ func TestParseIPNet(t *testing.T) { if err != nil { t.Errorf("expected %q to parse, but got error %v", str, err) } + ifaddr, err := ParseIPAsIPNet(str) + if err != nil { + t.Errorf("expected %q to parse via ParseIPAsIPNet, but got error %v", str, err) + } + + if tc.ifaddr { + // The test case expects ParseIPNet and + // ParseIPAsIPNet to parse to different values. + if ipnet.String() == ifaddr.String() { + t.Errorf("expected %q to parse differently with ParseIPNet and ParseIPAsIPNet but got %q for both", str, ipnet.String()) + } + // In this case, it's the ParseIPAsIPNet value + // that should re-stringify correctly. (ParseIPNet + // will have discarded the trailing bits.) + ipnet = ifaddr + } else { + // Some strings might parse to the same value and + // others might parse to different values. + // However, in all cases, the ParseIPAsIPNet value + // should be the same as the ParseIPNet value + // after masking it. + if !ipnet.IP.Equal(ifaddr.IP.Mask(ifaddr.Mask)) { + t.Errorf("expected %q to parse similarly with ParseIPNet and ParseIPAsIPNet but got IPs %q and %q->%q", str, ipnet.IP, ifaddr, ifaddr.IP.Mask(ifaddr.Mask)) + } + } + if ipnet.String() != tc.ipnets[0].String() { t.Errorf("expected string %d %q to parse and re-stringify to %q but got %q", i+1, str, tc.ipnets[0].String(), ipnet.String()) } @@ -184,6 +210,32 @@ func TestParsePrefix(t *testing.T) { if err != nil { t.Errorf("expected %q to parse, but got error %v", str, err) } + ifaddr, err := ParseAddrAsPrefix(str) + if err != nil { + t.Errorf("expected %q to parse via ParseAddrAsPrefix, but got error %v", str, err) + } + + if tc.ifaddr { + // The test case expects ParsePrefix and + // ParseAddrAsPrefix to parse to different values. + if prefix == ifaddr { + t.Errorf("expected %q to parse differently with ParsePrefix and ParseAddrAsPrefix but got %#v %q for both", str, prefix, prefix) + } + // In this case, it's the ParseAddrAsPrefix value + // that should re-stringify correctly. (ParsePrefix + // will have discarded the trailing bits.) + prefix = ifaddr + } else { + // Some strings might parse to the same value and + // others might parse to different values. + // However, in all cases, the ParseAddrAsPrefix + // value should be the same as the ParsePrefix + // value after masking it. + if prefix != ifaddr.Masked() { + t.Errorf("expected %q to parse similarly with ParsePrefix and ParseAddrAsPrefix but got %q and %q->%q", str, prefix, ifaddr, ifaddr.Masked()) + } + } + if prefix != tc.prefixes[0] { t.Errorf("expected string %d %q to parse equal to Prefix %#v %q but got %#v (%q)", i+1, str, tc.prefixes[0], tc.prefixes[0].String(), prefix, prefix.String()) } From 606e2e625f077cd32ea21522f8272bd2156e9b9b Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Fri, 4 Oct 2024 19:34:19 -0400 Subject: [PATCH 16/17] Add MustParse* variants --- net/v2/multi_listen_test.go | 55 +++++++++++++++++-------------------- net/v2/parse.go | 30 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/net/v2/multi_listen_test.go b/net/v2/multi_listen_test.go index 712758df..e2121f9b 100644 --- a/net/v2/multi_listen_test.go +++ b/net/v2/multi_listen_test.go @@ -26,13 +26,8 @@ import ( "sync/atomic" "testing" "time" - - forkednet "k8s.io/utils/internal/third_party/forked/golang/net" ) -// Temporary -var parseIPSloppy = forkednet.ParseIP - type fakeCon struct { remoteAddr net.Addr } @@ -118,7 +113,7 @@ func listenFuncFactory(listeners []*fakeListener) func(_ context.Context, networ } listener := listeners[index] addr := &net.TCPAddr{ - IP: parseIPSloppy(host), + IP: MustParseIP(host), Port: port, } if err != nil { @@ -270,14 +265,14 @@ func TestMultiListen_Close(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{{ - conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}, + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("10.10.10.10"), Port: 50001}}, }}}, { connErrPairs: []connErrPair{{ - conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}, + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("192.168.1.10"), Port: 50002}}, }, }}, { connErrPairs: []connErrPair{{ - conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}, + conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 50003}}, }}, }}, }, @@ -299,13 +294,13 @@ func TestMultiListen_Close(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("10.10.10.10"), Port: 50001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("192.168.1.10"), Port: 50002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 50003}}}, }, }}, acceptCalls: 3, @@ -386,13 +381,13 @@ func TestMultiListen_Accept(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("10.10.10.10"), Port: 50001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("192.168.1.10"), Port: 50002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 50003}}}, }, }}, acceptCalls: 3, @@ -417,24 +412,24 @@ func TestMultiListen_Accept(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 30001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("10.10.10.10"), Port: 30001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 40001}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 40002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("192.168.1.10"), Port: 40001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("192.168.1.10"), Port: 40002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50001}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50002}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50003}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("172.16.20.10"), Port: 50004}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("172.16.20.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("172.16.20.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("172.16.20.10"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("172.16.20.10"), Port: 50004}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60001}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60002}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60003}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60004}}}, - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 60005}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 60001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 60002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 60003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 60004}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 60005}}}, }, }}, acceptCalls: 3, @@ -457,13 +452,13 @@ func TestMultiListen_Accept(t *testing.T) { }, fakeListeners: []*fakeListener{{ connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("10.10.10.10"), Port: 50001}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("10.10.10.10"), Port: 50001}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("192.168.1.10"), Port: 50002}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("192.168.1.10"), Port: 50002}}}, }}, { connErrPairs: []connErrPair{ - {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: parseIPSloppy("127.0.0.1"), Port: 50003}}}, + {conn: &fakeCon{remoteAddr: &net.TCPAddr{IP: MustParseIP("127.0.0.1"), Port: 50003}}}, }, }}, acceptCalls: 1, diff --git a/net/v2/parse.go b/net/v2/parse.go index 8b80adb7..fe586d25 100644 --- a/net/v2/parse.go +++ b/net/v2/parse.go @@ -165,3 +165,33 @@ func ParseAddrAsPrefix(cidrStr string) (netip.Prefix, error) { ipnet, err := ParseIPAsIPNet(cidrStr) return PrefixFromIPNet(ipnet), err } + +type parser[T any] func(string) (T, error) + +func must[T any](parse parser[T]) func(string) T { + return func(str string) T { + ret, err := parse(str) + if err != nil { + panic(err) + } + return ret + } +} + +// MustParseIP is like ParseIP, but it panics on error instead of returning an error value. +var MustParseIP = must(ParseIP) + +// MustParseIPNet is like ParseIPNet, but it panics on error instead of returning an error value. +var MustParseIPNet = must(ParseIPNet) + +// MustParseAddr is like ParseAddr, but it panics on error instead of returning an error value. +var MustParseAddr = must(ParseAddr) + +// MustParsePrefix is like ParsePrefix, but it panics on error instead of returning an error value. +var MustParsePrefix = must(ParsePrefix) + +// MustParseIPAsIPNet is like ParseIPAsIPNet, but it panics on error instead of returning an error value. +var MustParseIPAsIPNet = must(ParseIPAsIPNet) + +// MustParseAddrAsPrefix is like ParseAddrAsPrefix, but it panics on error instead of returning an error value. +var MustParseAddrAsPrefix = must(ParseAddrAsPrefix) From 4005e1e58e31dee17bf031a05ba9c1a323626cd3 Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Fri, 4 Oct 2024 19:34:19 -0400 Subject: [PATCH 17/17] Add Parse*List and MustParse*List variants, replace ParseCIDRs --- net/v2/net.go | 14 ----------- net/v2/net_test.go | 43 -------------------------------- net/v2/parse.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 57 deletions(-) diff --git a/net/v2/net.go b/net/v2/net.go index c0c0f3c2..233733cd 100644 --- a/net/v2/net.go +++ b/net/v2/net.go @@ -25,20 +25,6 @@ import ( "strconv" ) -// ParseCIDRs parses a list of cidrs and return error if any is invalid. -// order is maintained -func ParseCIDRs(cidrsString []string) ([]*net.IPNet, error) { - cidrs := make([]*net.IPNet, 0, len(cidrsString)) - for i, cidrString := range cidrsString { - cidr, err := ParseIPNet(cidrString) - if err != nil { - return nil, fmt.Errorf("invalid CIDR[%d]: %v (%v)", i, cidr, err) - } - cidrs = append(cidrs, cidr) - } - return cidrs, nil -} - // ParsePort parses a string representing an IP port. If the string is not a // valid port number, this returns an error. func ParsePort(port string, allowZero bool) (int, error) { diff --git a/net/v2/net_test.go b/net/v2/net_test.go index d8ce7c79..e9770f2e 100644 --- a/net/v2/net_test.go +++ b/net/v2/net_test.go @@ -20,49 +20,6 @@ import ( "testing" ) -func TestParseCIDRs(t *testing.T) { - testCases := []struct { - cidrs []string - errString string - errorExpected bool - }{ - { - cidrs: []string{}, - errString: "should not return an error for an empty slice", - errorExpected: false, - }, - { - cidrs: []string{"10.0.0.0/8", "not-a-valid-cidr", "2000::/10"}, - errString: "should return error for bad cidr", - errorExpected: true, - }, - { - cidrs: []string{"10.0.0.0/8", "2000::/10"}, - errString: "should not return error for good cidrs", - errorExpected: false, - }, - } - - for _, tc := range testCases { - cidrs, err := ParseCIDRs(tc.cidrs) - if tc.errorExpected { - if err == nil { - t.Errorf("%v", tc.errString) - } - continue - } - if err != nil { - t.Errorf("%v error:%v", tc.errString, err) - } - - // validate lengths - if len(cidrs) != len(tc.cidrs) { - t.Errorf("cidrs should be of the same lengths %v != %v", len(cidrs), len(tc.cidrs)) - } - - } -} - func TestParsePort(t *testing.T) { var tests = []struct { name string diff --git a/net/v2/parse.go b/net/v2/parse.go index fe586d25..b7e9e8b9 100644 --- a/net/v2/parse.go +++ b/net/v2/parse.go @@ -195,3 +195,65 @@ var MustParseIPAsIPNet = must(ParseIPAsIPNet) // MustParseAddrAsPrefix is like ParseAddrAsPrefix, but it panics on error instead of returning an error value. var MustParseAddrAsPrefix = must(ParseAddrAsPrefix) + +type listParser[T any] func(...string) ([]T, error) + +func list[T any](parse parser[T]) listParser[T] { + return func(strs ...string) ([]T, error) { + var err error + ret := make([]T, len(strs)) + for i, str := range strs { + ret[i], err = parse(str) + if err != nil { + return nil, err + } + } + return ret, nil + } +} + +// ParseIPList parses a list of strings with ParseIP and returns a []net.IP or an error. +var ParseIPList = list(ParseIP) + +// ParseIPNetList parses a list of strings with ParseIPNet and returns a []*net.IPNet or an error. +var ParseIPNetList = list(ParseIPNet) + +// ParseAddrList parses a list of strings with ParseAddr and returns a []netip.Addr or an error. +var ParseAddrList = list(ParseAddr) + +// ParsePrefixList parses a list of strings with ParsePrefix and returns a []netip.Prefix or an error. +var ParsePrefixList = list(ParsePrefix) + +// ParseIPAsIPNetList parses a list of strings with ParseIPAsIPNet and returns a []*net.IPNet or an error. +var ParseIPAsIPNetList = list(ParseIPAsIPNet) + +// ParseAddrAsPrefixList parses a list of strings with ParseAddrAsPrefix and returns a []netip.Prefix or an error. +var ParseAddrAsPrefixList = list(ParseAddrAsPrefix) + +func mustlist[T any](parse listParser[T]) func(...string) []T { + return func(strs ...string) []T { + ret, err := parse(strs...) + if err != nil { + panic(err) + } + return ret + } +} + +// MustParseIPList parses a list of strings with ParseIP and returns a []net.IP or else panics on error. +var MustParseIPList = mustlist(ParseIPList) + +// MustParseIPNetList parses a list of strings with ParseIPNet and returns a []*net.IPNet or else panics on error. +var MustParseIPNetList = mustlist(ParseIPNetList) + +// MustParseAddrList parses a list of strings with ParseAddr and returns a []netip.Addr or else panics on error. +var MustParseAddrList = mustlist(ParseAddrList) + +// MustParsePrefixList parses a list of strings with ParsePrefix and returns a []netip.Prefix or else panics on error. +var MustParsePrefixList = mustlist(ParsePrefixList) + +// MustParseIPAsIPNetList parses a list of strings with ParseIPAsIPNet and returns a []*net.IPNet or else panics on error. +var MustParseIPAsIPNetList = mustlist(ParseIPAsIPNetList) + +// MustParseAddrAsPrefixList parses a list of strings with ParseAddrAsPrefix and returns a []netip.Prefix or else panics on error. +var MustParseAddrAsPrefixList = mustlist(ParseAddrAsPrefixList)