Split out IP validation functions into their own file

(No code changes.)
This commit is contained in:
Dan Winship 2023-12-26 19:26:24 -05:00
parent 5e067b6781
commit 34717000da
4 changed files with 381 additions and 336 deletions

View File

@ -0,0 +1,61 @@
/*
Copyright 2023 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 validation
import (
"k8s.io/apimachinery/pkg/util/validation/field"
netutils "k8s.io/utils/net"
)
// IsValidIP tests that the argument is a valid IP address.
func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
if netutils.ParseIPSloppy(value) == nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)").WithOrigin("format=ip-sloppy"))
}
return allErrors
}
// IsValidIPv4Address tests that the argument is a valid IPv4 address.
func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
ip := netutils.ParseIPSloppy(value)
if ip == nil || ip.To4() == nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address"))
}
return allErrors
}
// IsValidIPv6Address tests that the argument is a valid IPv6 address.
func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
ip := netutils.ParseIPSloppy(value)
if ip == nil || ip.To4() != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address"))
}
return allErrors
}
// IsValidCIDR tests that the argument is a valid CIDR value.
func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
_, _, err := netutils.ParseCIDRSloppy(value)
if err != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
}
return allErrors
}

View File

@ -0,0 +1,320 @@
/*
Copyright 2023 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 validation
import (
"strings"
"testing"
"k8s.io/apimachinery/pkg/util/validation/field"
)
func TestIsValidIP(t *testing.T) {
for _, tc := range []struct {
name string
in string
family int
err string
}{
// GOOD VALUES
{
name: "ipv4",
in: "1.2.3.4",
family: 4,
},
{
name: "ipv4, all zeros",
in: "0.0.0.0",
family: 4,
},
{
name: "ipv4, max",
in: "255.255.255.255",
family: 4,
},
{
name: "ipv6",
in: "1234::abcd",
family: 6,
},
{
name: "ipv6, all zeros, collapsed",
in: "::",
family: 6,
},
{
name: "ipv6, max",
in: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
family: 6,
},
// GOOD, THOUGH NON-CANONICAL, VALUES
{
name: "ipv6, all zeros, expanded (non-canonical)",
in: "0:0:0:0:0:0:0:0",
family: 6,
},
{
name: "ipv6, leading 0s (non-canonical)",
in: "0001:002:03:4::",
family: 6,
},
{
name: "ipv6, capital letters (non-canonical)",
in: "1234::ABCD",
family: 6,
},
// BAD VALUES WE CURRENTLY CONSIDER GOOD
{
name: "ipv4 with leading 0s",
in: "1.1.1.01",
family: 4,
},
{
name: "ipv4-in-ipv6 value",
in: "::ffff:1.1.1.1",
family: 4,
},
// BAD VALUES
{
name: "empty string",
in: "",
err: "must be a valid IP address",
},
{
name: "junk",
in: "aaaaaaa",
err: "must be a valid IP address",
},
{
name: "domain name",
in: "myhost.mydomain",
err: "must be a valid IP address",
},
{
name: "cidr",
in: "1.2.3.0/24",
err: "must be a valid IP address",
},
{
name: "ipv4 with out-of-range octets",
in: "1.2.3.400",
err: "must be a valid IP address",
},
{
name: "ipv4 with negative octets",
in: "-1.0.0.0",
err: "must be a valid IP address",
},
{
name: "ipv6 with out-of-range segment",
in: "2001:db8::10005",
err: "must be a valid IP address",
},
{
name: "ipv4:port",
in: "1.2.3.4:80",
err: "must be a valid IP address",
},
{
name: "ipv6 with brackets",
in: "[2001:db8::1]",
err: "must be a valid IP address",
},
{
name: "[ipv6]:port",
in: "[2001:db8::1]:80",
err: "must be a valid IP address",
},
{
name: "host:port",
in: "example.com:80",
err: "must be a valid IP address",
},
{
name: "ipv6 with zone",
in: "1234::abcd%eth0",
err: "must be a valid IP address",
},
{
name: "ipv4 with zone",
in: "169.254.0.0%eth0",
err: "must be a valid IP address",
},
} {
t.Run(tc.name, func(t *testing.T) {
errs := IsValidIP(field.NewPath(""), tc.in)
if tc.err == "" {
if len(errs) != 0 {
t.Errorf("expected %q to be valid but got: %v", tc.in, errs)
}
} else {
if len(errs) != 1 {
t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs)
} else if !strings.Contains(errs[0].Detail, tc.err) {
t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail)
}
}
errs = IsValidIPv4Address(field.NewPath(""), tc.in)
if tc.family == 4 {
if len(errs) != 0 {
t.Errorf("expected %q to pass IsValidIPv4Address but got: %v", tc.in, errs)
}
} else {
if len(errs) == 0 {
t.Errorf("expected %q to fail IsValidIPv4Address", tc.in)
}
}
errs = IsValidIPv6Address(field.NewPath(""), tc.in)
if tc.family == 6 {
if len(errs) != 0 {
t.Errorf("expected %q to pass IsValidIPv6Address but got: %v", tc.in, errs)
}
} else {
if len(errs) == 0 {
t.Errorf("expected %q to fail IsValidIPv6Address", tc.in)
}
}
})
}
}
func TestIsValidCIDR(t *testing.T) {
for _, tc := range []struct {
name string
in string
err string
}{
// GOOD VALUES
{
name: "ipv4",
in: "1.0.0.0/8",
},
{
name: "ipv4, all IPs",
in: "0.0.0.0/0",
},
{
name: "ipv4, single IP",
in: "1.1.1.1/32",
},
{
name: "ipv6",
in: "2001:4860:4860::/48",
},
{
name: "ipv6, all IPs",
in: "::/0",
},
{
name: "ipv6, single IP",
in: "::1/128",
},
// GOOD, THOUGH NON-CANONICAL, VALUES
{
name: "ipv6, extra 0s (non-canonical)",
in: "2a00:79e0:2:0::/64",
},
{
name: "ipv6, capital letters (non-canonical)",
in: "2001:DB8::/64",
},
// BAD VALUES WE CURRENTLY CONSIDER GOOD
{
name: "ipv4 with leading 0s",
in: "1.1.01.0/24",
},
{
name: "ipv4-in-ipv6 with ipv4-sized prefix",
in: "::ffff:1.1.1.0/24",
},
{
name: "ipv4-in-ipv6 with ipv6-sized prefix",
in: "::ffff:1.1.1.0/120",
},
{
name: "ipv4 with bits past prefix",
in: "1.2.3.4/24",
},
{
name: "ipv6 with bits past prefix",
in: "2001:db8::1/64",
},
{
name: "prefix length with leading 0s",
in: "192.168.0.0/016",
},
// BAD VALUES
{
name: "empty string",
in: "",
err: "must be a valid CIDR value",
},
{
name: "junk",
in: "aaaaaaa",
err: "must be a valid CIDR value",
},
{
name: "IP address",
in: "1.2.3.4",
err: "must be a valid CIDR value",
},
{
name: "partial URL",
in: "192.168.0.1/healthz",
err: "must be a valid CIDR value",
},
{
name: "partial URL 2",
in: "192.168.0.1/0/99",
err: "must be a valid CIDR value",
},
{
name: "negative prefix length",
in: "192.168.0.0/-16",
err: "must be a valid CIDR value",
},
{
name: "prefix length with sign",
in: "192.168.0.0/+16",
err: "must be a valid CIDR value",
},
} {
t.Run(tc.name, func(t *testing.T) {
errs := IsValidCIDR(field.NewPath(""), tc.in)
if tc.err == "" {
if len(errs) != 0 {
t.Errorf("expected %q to be valid but got: %v", tc.in, errs)
}
} else {
if len(errs) != 1 {
t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs)
} else if !strings.Contains(errs[0].Detail, tc.err) {
t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail)
}
}
})
}
}

View File

@ -24,7 +24,6 @@ import (
"unicode"
"k8s.io/apimachinery/pkg/util/validation/field"
netutils "k8s.io/utils/net"
)
const qnameCharFmt string = "[A-Za-z0-9]"
@ -369,45 +368,6 @@ func IsValidPortName(port string) []string {
return errs
}
// IsValidIP tests that the argument is a valid IP address.
func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
if netutils.ParseIPSloppy(value) == nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)").WithOrigin("format=ip-sloppy"))
}
return allErrors
}
// IsValidIPv4Address tests that the argument is a valid IPv4 address.
func IsValidIPv4Address(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
ip := netutils.ParseIPSloppy(value)
if ip == nil || ip.To4() == nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv4 address"))
}
return allErrors
}
// IsValidIPv6Address tests that the argument is a valid IPv6 address.
func IsValidIPv6Address(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
ip := netutils.ParseIPSloppy(value)
if ip == nil || ip.To4() != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IPv6 address"))
}
return allErrors
}
// IsValidCIDR tests that the argument is a valid CIDR value.
func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
var allErrors field.ErrorList
_, _, err := netutils.ParseCIDRSloppy(value)
if err != nil {
allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
}
return allErrors
}
const percentFmt string = "[0-9]+%"
const percentErrMsg string = "a valid percent string must be a numeric string followed by an ending '%'"

View File

@ -322,302 +322,6 @@ func TestIsValidLabelValue(t *testing.T) {
}
}
func TestIsValidIP(t *testing.T) {
for _, tc := range []struct {
name string
in string
family int
err string
}{
// GOOD VALUES
{
name: "ipv4",
in: "1.2.3.4",
family: 4,
},
{
name: "ipv4, all zeros",
in: "0.0.0.0",
family: 4,
},
{
name: "ipv4, max",
in: "255.255.255.255",
family: 4,
},
{
name: "ipv6",
in: "1234::abcd",
family: 6,
},
{
name: "ipv6, all zeros, collapsed",
in: "::",
family: 6,
},
{
name: "ipv6, max",
in: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
family: 6,
},
// GOOD, THOUGH NON-CANONICAL, VALUES
{
name: "ipv6, all zeros, expanded (non-canonical)",
in: "0:0:0:0:0:0:0:0",
family: 6,
},
{
name: "ipv6, leading 0s (non-canonical)",
in: "0001:002:03:4::",
family: 6,
},
{
name: "ipv6, capital letters (non-canonical)",
in: "1234::ABCD",
family: 6,
},
// BAD VALUES WE CURRENTLY CONSIDER GOOD
{
name: "ipv4 with leading 0s",
in: "1.1.1.01",
family: 4,
},
{
name: "ipv4-in-ipv6 value",
in: "::ffff:1.1.1.1",
family: 4,
},
// BAD VALUES
{
name: "empty string",
in: "",
err: "must be a valid IP address",
},
{
name: "junk",
in: "aaaaaaa",
err: "must be a valid IP address",
},
{
name: "domain name",
in: "myhost.mydomain",
err: "must be a valid IP address",
},
{
name: "cidr",
in: "1.2.3.0/24",
err: "must be a valid IP address",
},
{
name: "ipv4 with out-of-range octets",
in: "1.2.3.400",
err: "must be a valid IP address",
},
{
name: "ipv4 with negative octets",
in: "-1.0.0.0",
err: "must be a valid IP address",
},
{
name: "ipv6 with out-of-range segment",
in: "2001:db8::10005",
err: "must be a valid IP address",
},
{
name: "ipv4:port",
in: "1.2.3.4:80",
err: "must be a valid IP address",
},
{
name: "ipv6 with brackets",
in: "[2001:db8::1]",
err: "must be a valid IP address",
},
{
name: "[ipv6]:port",
in: "[2001:db8::1]:80",
err: "must be a valid IP address",
},
{
name: "host:port",
in: "example.com:80",
err: "must be a valid IP address",
},
{
name: "ipv6 with zone",
in: "1234::abcd%eth0",
err: "must be a valid IP address",
},
{
name: "ipv4 with zone",
in: "169.254.0.0%eth0",
err: "must be a valid IP address",
},
} {
t.Run(tc.name, func(t *testing.T) {
errs := IsValidIP(field.NewPath(""), tc.in)
if tc.err == "" {
if len(errs) != 0 {
t.Errorf("expected %q to be valid but got: %v", tc.in, errs)
}
} else {
if len(errs) != 1 {
t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs)
} else if !strings.Contains(errs[0].Detail, tc.err) {
t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail)
}
}
errs = IsValidIPv4Address(field.NewPath(""), tc.in)
if tc.family == 4 {
if len(errs) != 0 {
t.Errorf("expected %q to pass IsValidIPv4Address but got: %v", tc.in, errs)
}
} else {
if len(errs) == 0 {
t.Errorf("expected %q to fail IsValidIPv4Address", tc.in)
}
}
errs = IsValidIPv6Address(field.NewPath(""), tc.in)
if tc.family == 6 {
if len(errs) != 0 {
t.Errorf("expected %q to pass IsValidIPv6Address but got: %v", tc.in, errs)
}
} else {
if len(errs) == 0 {
t.Errorf("expected %q to fail IsValidIPv6Address", tc.in)
}
}
})
}
}
func TestIsValidCIDR(t *testing.T) {
for _, tc := range []struct {
name string
in string
err string
}{
// GOOD VALUES
{
name: "ipv4",
in: "1.0.0.0/8",
},
{
name: "ipv4, all IPs",
in: "0.0.0.0/0",
},
{
name: "ipv4, single IP",
in: "1.1.1.1/32",
},
{
name: "ipv6",
in: "2001:4860:4860::/48",
},
{
name: "ipv6, all IPs",
in: "::/0",
},
{
name: "ipv6, single IP",
in: "::1/128",
},
// GOOD, THOUGH NON-CANONICAL, VALUES
{
name: "ipv6, extra 0s (non-canonical)",
in: "2a00:79e0:2:0::/64",
},
{
name: "ipv6, capital letters (non-canonical)",
in: "2001:DB8::/64",
},
// BAD VALUES WE CURRENTLY CONSIDER GOOD
{
name: "ipv4 with leading 0s",
in: "1.1.01.0/24",
},
{
name: "ipv4-in-ipv6 with ipv4-sized prefix",
in: "::ffff:1.1.1.0/24",
},
{
name: "ipv4-in-ipv6 with ipv6-sized prefix",
in: "::ffff:1.1.1.0/120",
},
{
name: "ipv4 with bits past prefix",
in: "1.2.3.4/24",
},
{
name: "ipv6 with bits past prefix",
in: "2001:db8::1/64",
},
{
name: "prefix length with leading 0s",
in: "192.168.0.0/016",
},
// BAD VALUES
{
name: "empty string",
in: "",
err: "must be a valid CIDR value",
},
{
name: "junk",
in: "aaaaaaa",
err: "must be a valid CIDR value",
},
{
name: "IP address",
in: "1.2.3.4",
err: "must be a valid CIDR value",
},
{
name: "partial URL",
in: "192.168.0.1/healthz",
err: "must be a valid CIDR value",
},
{
name: "partial URL 2",
in: "192.168.0.1/0/99",
err: "must be a valid CIDR value",
},
{
name: "negative prefix length",
in: "192.168.0.0/-16",
err: "must be a valid CIDR value",
},
{
name: "prefix length with sign",
in: "192.168.0.0/+16",
err: "must be a valid CIDR value",
},
} {
t.Run(tc.name, func(t *testing.T) {
errs := IsValidCIDR(field.NewPath(""), tc.in)
if tc.err == "" {
if len(errs) != 0 {
t.Errorf("expected %q to be valid but got: %v", tc.in, errs)
}
} else {
if len(errs) != 1 {
t.Errorf("expected %q to have 1 error but got: %v", tc.in, errs)
} else if !strings.Contains(errs[0].Detail, tc.err) {
t.Errorf("expected error for %q to contain %q but got: %q", tc.in, tc.err, errs[0].Detail)
}
}
})
}
}
func TestIsHTTPHeaderName(t *testing.T) {
goodValues := []string{
// Common ones