github.com/opentofu/opentofu@v1.7.1/internal/lang/funcs/cidr.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package funcs
     7  
     8  import (
     9  	"fmt"
    10  	"math/big"
    11  
    12  	"github.com/apparentlymart/go-cidr/cidr"
    13  	"github.com/opentofu/opentofu/internal/ipaddr"
    14  	"github.com/zclconf/go-cty/cty"
    15  	"github.com/zclconf/go-cty/cty/function"
    16  	"github.com/zclconf/go-cty/cty/gocty"
    17  )
    18  
    19  // CidrHostFunc contructs a function that calculates a full host IP address
    20  // within a given IP network address prefix.
    21  var CidrHostFunc = function.New(&function.Spec{
    22  	Params: []function.Parameter{
    23  		{
    24  			Name: "prefix",
    25  			Type: cty.String,
    26  		},
    27  		{
    28  			Name: "hostnum",
    29  			Type: cty.Number,
    30  		},
    31  	},
    32  	Type:         function.StaticReturnType(cty.String),
    33  	RefineResult: refineNotNull,
    34  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
    35  		var hostNum *big.Int
    36  		if err := gocty.FromCtyValue(args[1], &hostNum); err != nil {
    37  			return cty.UnknownVal(cty.String), err
    38  		}
    39  		_, network, err := ipaddr.ParseCIDR(args[0].AsString())
    40  		if err != nil {
    41  			return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %w", err)
    42  		}
    43  
    44  		ip, err := cidr.HostBig(network, hostNum)
    45  		if err != nil {
    46  			return cty.UnknownVal(cty.String), err
    47  		}
    48  
    49  		return cty.StringVal(ip.String()), nil
    50  	},
    51  })
    52  
    53  // CidrNetmaskFunc contructs a function that converts an IPv4 address prefix given
    54  // in CIDR notation into a subnet mask address.
    55  var CidrNetmaskFunc = function.New(&function.Spec{
    56  	Params: []function.Parameter{
    57  		{
    58  			Name: "prefix",
    59  			Type: cty.String,
    60  		},
    61  	},
    62  	Type:         function.StaticReturnType(cty.String),
    63  	RefineResult: refineNotNull,
    64  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
    65  		_, network, err := ipaddr.ParseCIDR(args[0].AsString())
    66  		if err != nil {
    67  			return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %w", err)
    68  		}
    69  
    70  		if network.IP.To4() == nil {
    71  			return cty.UnknownVal(cty.String), fmt.Errorf("IPv6 addresses cannot have a netmask: %s", args[0].AsString())
    72  		}
    73  
    74  		return cty.StringVal(ipaddr.IP(network.Mask).String()), nil
    75  	},
    76  })
    77  
    78  // CidrSubnetFunc contructs a function that calculates a subnet address within
    79  // a given IP network address prefix.
    80  var CidrSubnetFunc = function.New(&function.Spec{
    81  	Params: []function.Parameter{
    82  		{
    83  			Name: "prefix",
    84  			Type: cty.String,
    85  		},
    86  		{
    87  			Name: "newbits",
    88  			Type: cty.Number,
    89  		},
    90  		{
    91  			Name: "netnum",
    92  			Type: cty.Number,
    93  		},
    94  	},
    95  	Type:         function.StaticReturnType(cty.String),
    96  	RefineResult: refineNotNull,
    97  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
    98  		var newbits int
    99  		if err := gocty.FromCtyValue(args[1], &newbits); err != nil {
   100  			return cty.UnknownVal(cty.String), err
   101  		}
   102  		var netnum *big.Int
   103  		if err := gocty.FromCtyValue(args[2], &netnum); err != nil {
   104  			return cty.UnknownVal(cty.String), err
   105  		}
   106  
   107  		_, network, err := ipaddr.ParseCIDR(args[0].AsString())
   108  		if err != nil {
   109  			return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %w", err)
   110  		}
   111  
   112  		newNetwork, err := cidr.SubnetBig(network, newbits, netnum)
   113  		if err != nil {
   114  			return cty.UnknownVal(cty.String), err
   115  		}
   116  
   117  		return cty.StringVal(newNetwork.String()), nil
   118  	},
   119  })
   120  
   121  // CidrSubnetsFunc is similar to CidrSubnetFunc but calculates many consecutive
   122  // subnet addresses at once, rather than just a single subnet extension.
   123  var CidrSubnetsFunc = function.New(&function.Spec{
   124  	Params: []function.Parameter{
   125  		{
   126  			Name: "prefix",
   127  			Type: cty.String,
   128  		},
   129  	},
   130  	VarParam: &function.Parameter{
   131  		Name: "newbits",
   132  		Type: cty.Number,
   133  	},
   134  	Type:         function.StaticReturnType(cty.List(cty.String)),
   135  	RefineResult: refineNotNull,
   136  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
   137  		_, network, err := ipaddr.ParseCIDR(args[0].AsString())
   138  		if err != nil {
   139  			return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid CIDR expression: %s", err)
   140  		}
   141  		startPrefixLen, _ := network.Mask.Size()
   142  
   143  		prefixLengthArgs := args[1:]
   144  		if len(prefixLengthArgs) == 0 {
   145  			return cty.ListValEmpty(cty.String), nil
   146  		}
   147  
   148  		var firstLength int
   149  		if err := gocty.FromCtyValue(prefixLengthArgs[0], &firstLength); err != nil {
   150  			return cty.UnknownVal(cty.String), function.NewArgError(1, err)
   151  		}
   152  		firstLength += startPrefixLen
   153  
   154  		retVals := make([]cty.Value, len(prefixLengthArgs))
   155  
   156  		current, _ := cidr.PreviousSubnet(network, firstLength)
   157  		for i, lengthArg := range prefixLengthArgs {
   158  			var length int
   159  			if err := gocty.FromCtyValue(lengthArg, &length); err != nil {
   160  				return cty.UnknownVal(cty.String), function.NewArgError(i+1, err)
   161  			}
   162  
   163  			if length < 1 {
   164  				return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "must extend prefix by at least one bit")
   165  			}
   166  			// For portability with 32-bit systems where the subnet number
   167  			// will be a 32-bit int, we only allow extension of 32 bits in
   168  			// one call even if we're running on a 64-bit machine.
   169  			// (Of course, this is significant only for IPv6.)
   170  			if length > 32 {
   171  				return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "may not extend prefix by more than 32 bits")
   172  			}
   173  			length += startPrefixLen
   174  			if length > (len(network.IP) * 8) {
   175  				protocol := "IP"
   176  				switch len(network.IP) * 8 {
   177  				case 32:
   178  					protocol = "IPv4"
   179  				case 128:
   180  					protocol = "IPv6"
   181  				}
   182  				return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "would extend prefix to %d bits, which is too long for an %s address", length, protocol)
   183  			}
   184  
   185  			next, rollover := cidr.NextSubnet(current, length)
   186  			if rollover || !network.Contains(next.IP) {
   187  				// If we run out of suffix bits in the base CIDR prefix then
   188  				// NextSubnet will start incrementing the prefix bits, which
   189  				// we don't allow because it would then allocate addresses
   190  				// outside of the caller's given prefix.
   191  				return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "not enough remaining address space for a subnet with a prefix of %d bits after %s", length, current.String())
   192  			}
   193  
   194  			current = next
   195  			retVals[i] = cty.StringVal(current.String())
   196  		}
   197  
   198  		return cty.ListVal(retVals), nil
   199  	},
   200  })
   201  
   202  // CidrContainsFunc constructs a function that checks whether a given IP address
   203  // is within a given IP network address prefix.
   204  var CidrContainsFunc = function.New(&function.Spec{
   205  	Params: []function.Parameter{
   206  		{
   207  			Name: "containing_prefix",
   208  			Type: cty.String,
   209  		},
   210  		{
   211  			Name: "contained_ip_or_prefix",
   212  			Type: cty.String,
   213  		},
   214  	},
   215  	Type: function.StaticReturnType(cty.Bool),
   216  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
   217  		prefix := args[0].AsString()
   218  		addr := args[1].AsString()
   219  
   220  		// The first argument must be a CIDR prefix.
   221  		_, containing, err := ipaddr.ParseCIDR(prefix)
   222  		if err != nil {
   223  			return cty.UnknownVal(cty.Bool), err
   224  		}
   225  
   226  		// The second argument can be either an IP address or a CIDR prefix.
   227  		// We will try parsing it as an IP address first.
   228  		startIP := ipaddr.ParseIP(addr)
   229  		var endIP ipaddr.IP
   230  
   231  		// If the second argument did not parse as an IP, we will try parsing it
   232  		// as a CIDR prefix.
   233  		if startIP == nil {
   234  			_, contained, err := ipaddr.ParseCIDR(addr)
   235  
   236  			// If that also fails, we'll return an error.
   237  			if err != nil {
   238  				return cty.UnknownVal(cty.Bool), fmt.Errorf("invalid IP address or prefix: %s", addr)
   239  			}
   240  
   241  			// Otherwise, we will want to know the start and the end IP of the
   242  			// prefix, so that we can check whether both are contained in the
   243  			// containing prefix.
   244  			startIP, endIP = cidr.AddressRange(contained)
   245  		}
   246  
   247  		// We require that both addresses are of the same type, so that
   248  		// we can't accidentally compare an IPv4 address to an IPv6 prefix.
   249  		// The underlying Go function will always return false if this happens,
   250  		// but we want to return an error instead so that the caller can
   251  		// distinguish between a "legitimate" false result and an erroneous
   252  		// check.
   253  		if (startIP.To4() == nil) != (containing.IP.To4() == nil) {
   254  			return cty.UnknownVal(cty.Bool), fmt.Errorf("address family mismatch: %s vs. %s", prefix, addr)
   255  		}
   256  
   257  		// If the second argument was an IP address, we will check whether it
   258  		// is contained in the containing prefix, and that's our result.
   259  		result := containing.Contains(startIP)
   260  
   261  		// If the second argument was a CIDR prefix, we will also check whether
   262  		// the end IP of the prefix is contained in the containing prefix.
   263  		// Once CIDR is contained in another CIDR iff both the start and the
   264  		// end IP of the contained CIDR are contained in the containing CIDR.
   265  		if endIP != nil {
   266  			result = result && containing.Contains(endIP)
   267  		}
   268  
   269  		return cty.BoolVal(result), nil
   270  	},
   271  })
   272  
   273  // CidrHost calculates a full host IP address within a given IP network address prefix.
   274  func CidrHost(prefix, hostnum cty.Value) (cty.Value, error) {
   275  	return CidrHostFunc.Call([]cty.Value{prefix, hostnum})
   276  }
   277  
   278  // CidrNetmask converts an IPv4 address prefix given in CIDR notation into a subnet mask address.
   279  func CidrNetmask(prefix cty.Value) (cty.Value, error) {
   280  	return CidrNetmaskFunc.Call([]cty.Value{prefix})
   281  }
   282  
   283  // CidrSubnet calculates a subnet address within a given IP network address prefix.
   284  func CidrSubnet(prefix, newbits, netnum cty.Value) (cty.Value, error) {
   285  	return CidrSubnetFunc.Call([]cty.Value{prefix, newbits, netnum})
   286  }
   287  
   288  // CidrSubnets calculates a sequence of consecutive subnet prefixes that may
   289  // be of different prefix lengths under a common base prefix.
   290  func CidrSubnets(prefix cty.Value, newbits ...cty.Value) (cty.Value, error) {
   291  	args := make([]cty.Value, len(newbits)+1)
   292  	args[0] = prefix
   293  	copy(args[1:], newbits)
   294  	return CidrSubnetsFunc.Call(args)
   295  }
   296  
   297  // CidrContains checks whether a given IP address is within a given IP network address prefix.
   298  func CidrContains(prefix, address cty.Value) (cty.Value, error) {
   299  	return CidrContainsFunc.Call([]cty.Value{prefix, address})
   300  }