ip.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. /*
  2. Copyright 2023 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package validation
  14. import (
  15. "fmt"
  16. "net"
  17. "net/netip"
  18. "slices"
  19. "k8s.io/apimachinery/pkg/util/validation/field"
  20. "k8s.io/klog/v2"
  21. netutils "k8s.io/utils/net"
  22. )
  23. func parseIP(fldPath *field.Path, value string, strictValidation bool) (net.IP, field.ErrorList) {
  24. var allErrors field.ErrorList
  25. ip := netutils.ParseIPSloppy(value)
  26. if ip == nil {
  27. allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid IP address, (e.g. 10.9.8.7 or 2001:db8::ffff)"))
  28. return nil, allErrors
  29. }
  30. if strictValidation {
  31. addr, err := netip.ParseAddr(value)
  32. if err != nil {
  33. // If netutils.ParseIPSloppy parsed it, but netip.ParseAddr
  34. // doesn't, then it must have illegal leading 0s.
  35. allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have leading 0s"))
  36. }
  37. if addr.Is4In6() {
  38. allErrors = append(allErrors, field.Invalid(fldPath, value, "must not be an IPv4-mapped IPv6 address"))
  39. }
  40. }
  41. return ip, allErrors
  42. }
  43. // IsValidIPForLegacyField tests that the argument is a valid IP address for a "legacy"
  44. // API field that predates strict IP validation. In particular, this allows IPs that are
  45. // not in canonical form (e.g., "FE80:0:0:0:0:0:0:0abc" instead of "fe80::abc").
  46. //
  47. // If strictValidation is false, this also allows IPs in certain invalid or ambiguous
  48. // formats:
  49. //
  50. // 1. IPv4 IPs are allowed to have leading "0"s in octets (e.g. "010.002.003.004").
  51. // Historically, net.ParseIP (and later netutils.ParseIPSloppy) simply ignored leading
  52. // "0"s in IPv4 addresses, but most libc-based software treats 0-prefixed IPv4 octets
  53. // as octal, meaning different software might interpret the same string as two
  54. // different IPs, potentially leading to security issues. (Current net.ParseIP and
  55. // netip.ParseAddr simply reject inputs with leading "0"s.)
  56. //
  57. // 2. IPv4-mapped IPv6 IPs (e.g. "::ffff:1.2.3.4") are allowed. These can also lead to
  58. // different software interpreting the value in different ways, because they may be
  59. // treated as IPv4 by some software and IPv6 by other software. (net.ParseIP and
  60. // netip.ParseAddr both allow these, but there are no use cases for representing IPv4
  61. // addresses as IPv4-mapped IPv6 addresses in Kubernetes.)
  62. //
  63. // Alternatively, when validating an update to an existing field, you can pass a list of
  64. // IP values from the old object that should be accepted if they appear in the new object
  65. // even if they are not valid.
  66. //
  67. // This function should only be used to validate the existing fields that were
  68. // historically validated in this way, and strictValidation should be true unless the
  69. // StrictIPCIDRValidation feature gate is disabled. Use IsValidIP for parsing new fields.
  70. func IsValidIPForLegacyField(fldPath *field.Path, value string, strictValidation bool, validOldIPs []string) field.ErrorList {
  71. if slices.Contains(validOldIPs, value) {
  72. return nil
  73. }
  74. _, allErrors := parseIP(fldPath, value, strictValidation)
  75. return allErrors.WithOrigin("format=ip-sloppy")
  76. }
  77. // IsValidIP tests that the argument is a valid IP address, according to current
  78. // Kubernetes standards for IP address validation.
  79. func IsValidIP(fldPath *field.Path, value string) field.ErrorList {
  80. ip, allErrors := parseIP(fldPath, value, true)
  81. if len(allErrors) != 0 {
  82. return allErrors.WithOrigin("format=ip-strict")
  83. }
  84. if value != ip.String() {
  85. allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", ip.String())))
  86. }
  87. return allErrors.WithOrigin("format=ip-strict")
  88. }
  89. // GetWarningsForIP returns warnings for IP address values in non-standard forms. This
  90. // should only be used with fields that are validated with IsValidIPForLegacyField().
  91. func GetWarningsForIP(fldPath *field.Path, value string) []string {
  92. ip := netutils.ParseIPSloppy(value)
  93. if ip == nil {
  94. klog.ErrorS(nil, "GetWarningsForIP called on value that was not validated with IsValidIPForLegacyField", "field", fldPath, "value", value)
  95. return nil
  96. }
  97. addr, _ := netip.ParseAddr(value)
  98. if !addr.IsValid() || addr.Is4In6() {
  99. // This catches 2 cases: leading 0s (if ParseIPSloppy() accepted it but
  100. // ParseAddr() doesn't) or IPv4-mapped IPv6 (.Is4In6()). Either way,
  101. // re-stringifying the net.IP value will give the preferred form.
  102. return []string{
  103. fmt.Sprintf("%s: non-standard IP address %q will be considered invalid in a future Kubernetes release: use %q", fldPath, value, ip.String()),
  104. }
  105. }
  106. // If ParseIPSloppy() and ParseAddr() both accept it then it's fully valid, though
  107. // it may be non-canonical.
  108. if addr.Is6() && addr.String() != value {
  109. return []string{
  110. fmt.Sprintf("%s: IPv6 address %q should be in RFC 5952 canonical format (%q)", fldPath, value, addr.String()),
  111. }
  112. }
  113. return nil
  114. }
  115. func parseCIDR(fldPath *field.Path, value string, strictValidation bool) (*net.IPNet, field.ErrorList) {
  116. var allErrors field.ErrorList
  117. _, ipnet, err := netutils.ParseCIDRSloppy(value)
  118. if err != nil {
  119. allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid CIDR value, (e.g. 10.9.8.0/24 or 2001:db8::/64)"))
  120. return nil, allErrors
  121. }
  122. if strictValidation {
  123. prefix, err := netip.ParsePrefix(value)
  124. if err != nil {
  125. // If netutils.ParseCIDRSloppy parsed it, but netip.ParsePrefix
  126. // doesn't, then it must have illegal leading 0s (either in the
  127. // IP part or the prefix).
  128. allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have leading 0s in IP or prefix length"))
  129. } else if prefix.Addr().Is4In6() {
  130. allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have an IPv4-mapped IPv6 address"))
  131. } else if prefix.Addr() != prefix.Masked().Addr() {
  132. allErrors = append(allErrors, field.Invalid(fldPath, value, "must not have bits set beyond the prefix length"))
  133. }
  134. }
  135. return ipnet, allErrors
  136. }
  137. // IsValidCIDRForLegacyField tests that the argument is a valid CIDR value for a "legacy"
  138. // API field that predates strict IP validation. In particular, this allows IPs that are
  139. // not in canonical form (e.g., "FE80:0abc:0:0:0:0:0:0/64" instead of "fe80:abc::/64").
  140. //
  141. // If strictValidation is false, this also allows CIDR values in certain invalid or
  142. // ambiguous formats:
  143. //
  144. // 1. The IP part of the CIDR value is parsed as with IsValidIPForLegacyField with
  145. // strictValidation=false.
  146. //
  147. // 2. The CIDR value is allowed to be either a "subnet"/"mask" (with the lower bits after
  148. // the prefix length all being 0), or an "interface address" as with `ip addr` (with a
  149. // complete IP address and associated subnet length). With strict validation, the
  150. // value is required to be in "subnet"/"mask" form.
  151. //
  152. // 3. The prefix length is allowed to have leading 0s.
  153. //
  154. // Alternatively, when validating an update to an existing field, you can pass a list of
  155. // CIDR values from the old object that should be accepted if they appear in the new
  156. // object even if they are not valid.
  157. //
  158. // This function should only be used to validate the existing fields that were
  159. // historically validated in this way, and strictValidation should be true unless the
  160. // StrictIPCIDRValidation feature gate is disabled. Use IsValidCIDR or
  161. // IsValidInterfaceAddress for parsing new fields.
  162. func IsValidCIDRForLegacyField(fldPath *field.Path, value string, strictValidation bool, validOldCIDRs []string) field.ErrorList {
  163. if slices.Contains(validOldCIDRs, value) {
  164. return nil
  165. }
  166. _, allErrors := parseCIDR(fldPath, value, strictValidation)
  167. return allErrors
  168. }
  169. // IsValidCIDR tests that the argument is a valid CIDR value, according to current
  170. // Kubernetes standards for CIDR validation. This function is only for
  171. // "subnet"/"mask"-style CIDR values (e.g., "192.168.1.0/24", with no bits set beyond the
  172. // prefix length). Use IsValidInterfaceAddress for "ifaddr"-style CIDR values.
  173. func IsValidCIDR(fldPath *field.Path, value string) field.ErrorList {
  174. ipnet, allErrors := parseCIDR(fldPath, value, true)
  175. if len(allErrors) != 0 {
  176. return allErrors
  177. }
  178. if value != ipnet.String() {
  179. allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", ipnet.String())))
  180. }
  181. return allErrors
  182. }
  183. // GetWarningsForCIDR returns warnings for CIDR values in non-standard forms. This should
  184. // only be used with fields that are validated with IsValidCIDRForLegacyField().
  185. func GetWarningsForCIDR(fldPath *field.Path, value string) []string {
  186. ip, ipnet, err := netutils.ParseCIDRSloppy(value)
  187. if err != nil {
  188. klog.ErrorS(err, "GetWarningsForCIDR called on value that was not validated with IsValidCIDRForLegacyField", "field", fldPath, "value", value)
  189. return nil
  190. }
  191. var warnings []string
  192. // Check for bits set after prefix length
  193. if !ip.Equal(ipnet.IP) {
  194. _, addrlen := ipnet.Mask.Size()
  195. singleIPCIDR := fmt.Sprintf("%s/%d", ip.String(), addrlen)
  196. warnings = append(warnings,
  197. fmt.Sprintf("%s: CIDR value %q is ambiguous in this context (should be %q or %q?)", fldPath, value, ipnet.String(), singleIPCIDR),
  198. )
  199. }
  200. prefix, _ := netip.ParsePrefix(value)
  201. addr := prefix.Addr()
  202. if !prefix.IsValid() || addr.Is4In6() {
  203. // This catches 2 cases: leading 0s (if ParseCIDRSloppy() accepted it but
  204. // ParsePrefix() doesn't) or IPv4-mapped IPv6 (.Is4In6()). Either way,
  205. // re-stringifying the net.IPNet value will give the preferred form.
  206. warnings = append(warnings,
  207. fmt.Sprintf("%s: non-standard CIDR value %q will be considered invalid in a future Kubernetes release: use %q", fldPath, value, ipnet.String()),
  208. )
  209. }
  210. // If ParseCIDRSloppy() and ParsePrefix() both accept it then it's fully valid,
  211. // though it may be non-canonical. But only check this if there are no other
  212. // warnings, since either of the other warnings would also cause a round-trip
  213. // failure.
  214. if len(warnings) == 0 && addr.Is6() && prefix.String() != value {
  215. warnings = append(warnings,
  216. fmt.Sprintf("%s: IPv6 CIDR value %q should be in RFC 5952 canonical format (%q)", fldPath, value, prefix.String()),
  217. )
  218. }
  219. return warnings
  220. }
  221. // IsValidInterfaceAddress tests that the argument is a valid "ifaddr"-style CIDR value in
  222. // canonical form (e.g., "192.168.1.5/24", with a complete IP address and associated
  223. // subnet length). Use IsValidCIDR for "subnet"/"mask"-style CIDR values (e.g.,
  224. // "192.168.1.0/24").
  225. func IsValidInterfaceAddress(fldPath *field.Path, value string) field.ErrorList {
  226. var allErrors field.ErrorList
  227. ip, ipnet, err := netutils.ParseCIDRSloppy(value)
  228. if err != nil {
  229. allErrors = append(allErrors, field.Invalid(fldPath, value, "must be a valid address in CIDR form, (e.g. 10.9.8.7/24 or 2001:db8::1/64)"))
  230. return allErrors
  231. }
  232. // The canonical form of `value` is not `ipnet.String()`, because `ipnet` doesn't
  233. // include the bits after the prefix. We need to construct the canonical form
  234. // ourselves from `ip` and `ipnet.Mask`.
  235. maskSize, _ := ipnet.Mask.Size()
  236. if netutils.IsIPv4(ip) && maskSize > net.IPv4len*8 {
  237. // "::ffff:192.168.0.1/120" -> "192.168.0.1/24"
  238. maskSize -= (net.IPv6len - net.IPv4len) * 8
  239. }
  240. canonical := fmt.Sprintf("%s/%d", ip.String(), maskSize)
  241. if value != canonical {
  242. allErrors = append(allErrors, field.Invalid(fldPath, value, fmt.Sprintf("must be in canonical form (%q)", canonical)))
  243. }
  244. return allErrors
  245. }