k8s.io/apiserver@v0.31.1/pkg/cel/library/ip.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package library 18 19 import ( 20 "fmt" 21 "net/netip" 22 23 "github.com/google/cel-go/cel" 24 "github.com/google/cel-go/common/types" 25 "github.com/google/cel-go/common/types/ref" 26 27 apiservercel "k8s.io/apiserver/pkg/cel" 28 ) 29 30 // IP provides a CEL function library extension of IP address parsing functions. 31 // 32 // ip 33 // 34 // Converts a string to an IP address or results in an error if the string is not a valid IP address. 35 // The IP address must be an IPv4 or IPv6 address. 36 // IPv4-mapped IPv6 addresses (e.g. ::ffff:1.2.3.4) are not allowed. 37 // IP addresses with zones (e.g. fe80::1%eth0) are not allowed. 38 // Leading zeros in IPv4 address octets are not allowed. 39 // 40 // ip(<string>) <IPAddr> 41 // 42 // Examples: 43 // 44 // ip('127.0.0.1') // returns an IPv4 address 45 // ip('::1') // returns an IPv6 address 46 // ip('127.0.0.256') // error 47 // ip(':::1') // error 48 // 49 // isIP 50 // 51 // Returns true if a string is a valid IP address. 52 // The IP address must be an IPv4 or IPv6 address. 53 // IPv4-mapped IPv6 addresses (e.g. ::ffff:1.2.3.4) are not allowed. 54 // IP addresses with zones (e.g. fe80::1%eth0) are not allowed. 55 // Leading zeros in IPv4 address octets are not allowed. 56 // 57 // isIP(<string>) <bool> 58 // 59 // Examples: 60 // 61 // isIP('127.0.0.1') // returns true 62 // isIP('::1') // returns true 63 // isIP('127.0.0.256') // returns false 64 // isIP(':::1') // returns false 65 // 66 // ip.isCanonical 67 // 68 // Returns true if the IP address is in its canonical form. 69 // There is exactly one canonical form for every IP address, so fields containing 70 // IPs in canonical form can just be treated as strings when checking for equality or uniqueness. 71 // 72 // ip.isCanonical(<string>) <bool> 73 // 74 // Examples: 75 // 76 // ip.isCanonical('127.0.0.1') // returns true; all valid IPv4 addresses are canonical 77 // ip.isCanonical('2001:db8::abcd') // returns true 78 // ip.isCanonical('2001:DB8::ABCD') // returns false 79 // ip.isCanonical('2001:db8::0:0:0:abcd') // returns false 80 // 81 // family / isUnspecified / isLoopback / isLinkLocalMulticast / isLinkLocalUnicast / isGlobalUnicast 82 // 83 // - family: returns the IP addresses' family (IPv4 or IPv6) as an integer, either '4' or '6'. 84 // 85 // - isUnspecified: returns true if the IP address is the unspecified address. 86 // Either the IPv4 address "0.0.0.0" or the IPv6 address "::". 87 // 88 // - isLoopback: returns true if the IP address is the loopback address. 89 // Either an IPv4 address with a value of 127.x.x.x or an IPv6 address with a value of ::1. 90 // 91 // - isLinkLocalMulticast: returns true if the IP address is a link-local multicast address. 92 // Either an IPv4 address with a value of 224.0.0.x or an IPv6 address in the network ff00::/8. 93 // 94 // - isLinkLocalUnicast: returns true if the IP address is a link-local unicast address. 95 // Either an IPv4 address with a value of 169.254.x.x or an IPv6 address in the network fe80::/10. 96 // 97 // - isGlobalUnicast: returns true if the IP address is a global unicast address. 98 // Either an IPv4 address that is not zero or 255.255.255.255 or an IPv6 address that is not a link-local unicast, loopback or multicast address. 99 // 100 // Examples: 101 // 102 // ip('127.0.0.1').family() // returns '4” 103 // ip('::1').family() // returns '6' 104 // ip('127.0.0.1').family() == 4 // returns true 105 // ip('::1').family() == 6 // returns true 106 // ip('0.0.0.0').isUnspecified() // returns true 107 // ip('127.0.0.1').isUnspecified() // returns false 108 // ip('::').isUnspecified() // returns true 109 // ip('::1').isUnspecified() // returns false 110 // ip('127.0.0.1').isLoopback() // returns true 111 // ip('192.168.0.1').isLoopback() // returns false 112 // ip('::1').isLoopback() // returns true 113 // ip('2001:db8::abcd').isLoopback() // returns false 114 // ip('224.0.0.1').isLinkLocalMulticast() // returns true 115 // ip('224.0.1.1').isLinkLocalMulticast() // returns false 116 // ip('ff02::1').isLinkLocalMulticast() // returns true 117 // ip('fd00::1').isLinkLocalMulticast() // returns false 118 // ip('169.254.169.254').isLinkLocalUnicast() // returns true 119 // ip('192.168.0.1').isLinkLocalUnicast() // returns false 120 // ip('fe80::1').isLinkLocalUnicast() // returns true 121 // ip('fd80::1').isLinkLocalUnicast() // returns false 122 // ip('192.168.0.1').isGlobalUnicast() // returns true 123 // ip('255.255.255.255').isGlobalUnicast() // returns false 124 // ip('2001:db8::abcd').isGlobalUnicast() // returns true 125 // ip('ff00::1').isGlobalUnicast() // returns false 126 func IP() cel.EnvOption { 127 return cel.Lib(ipLib) 128 } 129 130 var ipLib = &ip{} 131 132 type ip struct{} 133 134 func (*ip) LibraryName() string { 135 return "net.ip" 136 } 137 138 var ipLibraryDecls = map[string][]cel.FunctionOpt{ 139 "ip": { 140 cel.Overload("string_to_ip", []*cel.Type{cel.StringType}, apiservercel.IPType, 141 cel.UnaryBinding(stringToIP)), 142 }, 143 "family": { 144 cel.MemberOverload("ip_family", []*cel.Type{apiservercel.IPType}, cel.IntType, 145 cel.UnaryBinding(family)), 146 }, 147 "ip.isCanonical": { 148 cel.Overload("ip_is_canonical", []*cel.Type{cel.StringType}, cel.BoolType, 149 cel.UnaryBinding(ipIsCanonical)), 150 }, 151 "isUnspecified": { 152 cel.MemberOverload("ip_is_unspecified", []*cel.Type{apiservercel.IPType}, cel.BoolType, 153 cel.UnaryBinding(isUnspecified)), 154 }, 155 "isLoopback": { 156 cel.MemberOverload("ip_is_loopback", []*cel.Type{apiservercel.IPType}, cel.BoolType, 157 cel.UnaryBinding(isLoopback)), 158 }, 159 "isLinkLocalMulticast": { 160 cel.MemberOverload("ip_is_link_local_multicast", []*cel.Type{apiservercel.IPType}, cel.BoolType, 161 cel.UnaryBinding(isLinkLocalMulticast)), 162 }, 163 "isLinkLocalUnicast": { 164 cel.MemberOverload("ip_is_link_local_unicast", []*cel.Type{apiservercel.IPType}, cel.BoolType, 165 cel.UnaryBinding(isLinkLocalUnicast)), 166 }, 167 "isGlobalUnicast": { 168 cel.MemberOverload("ip_is_global_unicast", []*cel.Type{apiservercel.IPType}, cel.BoolType, 169 cel.UnaryBinding(isGlobalUnicast)), 170 }, 171 "isIP": { 172 cel.Overload("is_ip", []*cel.Type{cel.StringType}, cel.BoolType, 173 cel.UnaryBinding(isIP)), 174 }, 175 "string": { 176 cel.Overload("ip_to_string", []*cel.Type{apiservercel.IPType}, cel.StringType, 177 cel.UnaryBinding(ipToString)), 178 }, 179 } 180 181 func (*ip) CompileOptions() []cel.EnvOption { 182 options := []cel.EnvOption{cel.Types(apiservercel.IPType), 183 cel.Variable(apiservercel.IPType.TypeName(), types.NewTypeTypeWithParam(apiservercel.IPType)), 184 } 185 for name, overloads := range ipLibraryDecls { 186 options = append(options, cel.Function(name, overloads...)) 187 } 188 return options 189 } 190 191 func (*ip) ProgramOptions() []cel.ProgramOption { 192 return []cel.ProgramOption{} 193 } 194 195 func stringToIP(arg ref.Val) ref.Val { 196 s, ok := arg.Value().(string) 197 if !ok { 198 return types.MaybeNoSuchOverloadErr(arg) 199 } 200 201 addr, err := parseIPAddr(s) 202 if err != nil { 203 // Don't add context, we control the error message already. 204 return types.NewErr("%v", err) 205 } 206 207 return apiservercel.IP{ 208 Addr: addr, 209 } 210 } 211 212 func ipToString(arg ref.Val) ref.Val { 213 ip, ok := arg.(apiservercel.IP) 214 if !ok { 215 return types.MaybeNoSuchOverloadErr(arg) 216 } 217 218 return types.String(ip.Addr.String()) 219 } 220 221 func family(arg ref.Val) ref.Val { 222 ip, ok := arg.(apiservercel.IP) 223 if !ok { 224 return types.MaybeNoSuchOverloadErr(arg) 225 } 226 227 switch { 228 case ip.Addr.Is4(): 229 return types.Int(4) 230 case ip.Addr.Is6(): 231 return types.Int(6) 232 default: 233 return types.NewErr("IP address %q is not an IPv4 or IPv6 address", ip.Addr.String()) 234 } 235 } 236 237 func ipIsCanonical(arg ref.Val) ref.Val { 238 s, ok := arg.Value().(string) 239 if !ok { 240 return types.MaybeNoSuchOverloadErr(arg) 241 } 242 243 addr, err := parseIPAddr(s) 244 if err != nil { 245 // Don't add context, we control the error message already. 246 return types.NewErr("%v", err) 247 } 248 249 // Addr.String() always returns the canonical form of the IP address. 250 // Therefore comparing this with the original string representation 251 // will tell us if the IP address is in its canonical form. 252 return types.Bool(addr.String() == s) 253 } 254 255 func isIP(arg ref.Val) ref.Val { 256 s, ok := arg.Value().(string) 257 if !ok { 258 return types.MaybeNoSuchOverloadErr(arg) 259 } 260 261 _, err := parseIPAddr(s) 262 return types.Bool(err == nil) 263 } 264 265 func isUnspecified(arg ref.Val) ref.Val { 266 ip, ok := arg.(apiservercel.IP) 267 if !ok { 268 return types.MaybeNoSuchOverloadErr(arg) 269 } 270 271 return types.Bool(ip.Addr.IsUnspecified()) 272 } 273 274 func isLoopback(arg ref.Val) ref.Val { 275 ip, ok := arg.(apiservercel.IP) 276 if !ok { 277 return types.MaybeNoSuchOverloadErr(arg) 278 } 279 280 return types.Bool(ip.Addr.IsLoopback()) 281 } 282 283 func isLinkLocalMulticast(arg ref.Val) ref.Val { 284 ip, ok := arg.(apiservercel.IP) 285 if !ok { 286 return types.MaybeNoSuchOverloadErr(arg) 287 } 288 289 return types.Bool(ip.Addr.IsLinkLocalMulticast()) 290 } 291 292 func isLinkLocalUnicast(arg ref.Val) ref.Val { 293 ip, ok := arg.(apiservercel.IP) 294 if !ok { 295 return types.MaybeNoSuchOverloadErr(arg) 296 } 297 298 return types.Bool(ip.Addr.IsLinkLocalUnicast()) 299 } 300 301 func isGlobalUnicast(arg ref.Val) ref.Val { 302 ip, ok := arg.(apiservercel.IP) 303 if !ok { 304 return types.MaybeNoSuchOverloadErr(arg) 305 } 306 307 return types.Bool(ip.Addr.IsGlobalUnicast()) 308 } 309 310 // parseIPAddr parses a string into an IP address. 311 // We use this function to parse IP addresses in the CEL library 312 // so that we can share the common logic of rejecting IP addresses 313 // that contain zones or are IPv4-mapped IPv6 addresses. 314 func parseIPAddr(raw string) (netip.Addr, error) { 315 addr, err := netip.ParseAddr(raw) 316 if err != nil { 317 return netip.Addr{}, fmt.Errorf("IP Address %q parse error during conversion from string: %v", raw, err) 318 } 319 320 if addr.Zone() != "" { 321 return netip.Addr{}, fmt.Errorf("IP address %q with zone value is not allowed", raw) 322 } 323 324 if addr.Is4In6() { 325 return netip.Addr{}, fmt.Errorf("IPv4-mapped IPv6 address %q is not allowed", raw) 326 } 327 328 return addr, nil 329 }