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  }