diff --git a/pkg/net/ip.go b/pkg/net/ip.go index 89c587be167..5b7fc738ac4 100644 --- a/pkg/net/ip.go +++ b/pkg/net/ip.go @@ -16,7 +16,9 @@ package net import ( + "errors" "fmt" + "math/big" "net/netip" "cuelang.org/go/cue" @@ -242,3 +244,63 @@ func IPString(ip cue.Value) (string, error) { } return ipdata.String(), nil } + +func netIPAdd(addr netip.Addr, offset *big.Int) (netip.Addr, error) { + i := big.NewInt(0).SetBytes(addr.AsSlice()) + i = i.Add(i, offset) + + if i.Sign() < 0 { + return netip.Addr{}, errors.New("IP address arithmetic resulted in out-of-range address (underflow)") + } + + b := i.Bytes() + size := addr.BitLen() / 8 + + if len(b) > size { + return netip.Addr{}, errors.New("IP address arithmetic resulted in out-of-range address (overflow)") + } + + if len(b) < size { + b = append(make([]byte, size-len(b), size), b...) + } + addr, _ = netip.AddrFromSlice(b) + return addr, nil +} + +// AddIP adds a numerical offset to a given IP address. +// The address can be provided as a string, byte array, or CIDR subnet notation. +// It returns the resulting IP address or CIDR subnet notation as a string. +func AddIP(ip cue.Value, offset *big.Int) (string, error) { + prefix, err := netGetIPCIDR(ip) + if err == nil { + addr, err := netIPAdd(prefix.Addr(), offset) + if err != nil { + return "", err + } + return netip.PrefixFrom(addr, prefix.Bits()).String(), nil + } + ipdata := netGetIP(ip) + if !ipdata.IsValid() { + return "", fmt.Errorf("invalid IP %q", ip) + } + addr, err := netIPAdd(ipdata, offset) + if err != nil { + return "", err + } + return addr.String(), nil +} + +// AddIPCIDR adds a numerical offset to a given CIDR subnet +// string, returning a CIDR string. +func AddIPCIDR(ip cue.Value, offset *big.Int) (string, error) { + prefix, err := netGetIPCIDR(ip) + if err != nil { + return "", err + } + shifted := big.NewInt(0).Lsh(offset, (uint)(prefix.Addr().BitLen()-prefix.Bits())) + addr, err := netIPAdd(prefix.Addr(), shifted) + if err != nil { + return "", err + } + return netip.PrefixFrom(addr, prefix.Bits()).String(), nil +} diff --git a/pkg/net/pkg.go b/pkg/net/pkg.go index 568236636be..5599a702d47 100644 --- a/pkg/net/pkg.go +++ b/pkg/net/pkg.go @@ -237,6 +237,32 @@ var p = &pkg.Package{ c.Ret, c.Err = IPString(ip) } }, + }, { + Name: "AddIP", + Params: []pkg.Param{ + {Kind: adt.TopKind}, + {Kind: adt.IntKind}, + }, + Result: adt.StringKind, + Func: func(c *pkg.CallCtxt) { + ip, offset := c.Value(0), c.BigInt(1) + if c.Do() { + c.Ret, c.Err = AddIP(ip, offset) + } + }, + }, { + Name: "AddIPCIDR", + Params: []pkg.Param{ + {Kind: adt.TopKind}, + {Kind: adt.IntKind}, + }, + Result: adt.StringKind, + Func: func(c *pkg.CallCtxt) { + ip, offset := c.Value(0), c.BigInt(1) + if c.Do() { + c.Ret, c.Err = AddIPCIDR(ip, offset) + } + }, }, { Name: "PathEscape", Params: []pkg.Param{ diff --git a/pkg/net/testdata/gen.txtar b/pkg/net/testdata/gen.txtar index 28023c57a9b..2804bfb34e5 100644 --- a/pkg/net/testdata/gen.txtar +++ b/pkg/net/testdata/gen.txtar @@ -37,6 +37,19 @@ t31: net.URL & "https://foo.com/bar" t32: net.AbsURL & "/foo/bar" t33: net.AbsURL & "https://foo.com/bar" t34: net.AbsURL & "%" +t35: net.AddIP("127.0.0.1", 1) +t36: net.AddIP("127.0.0.1/8", 2) +t37: net.AddIP("2001:db8::", 1) +t38: net.AddIP("2001:db8::/64", 2) +t39: net.AddIP("invalid ip", 1) +t40: net.AddIP("0.0.0.0", -1) +t41: net.AddIP("255.255.255.255", 1) +t42: net.AddIP("::ffff:127.0.0.1", 1) +t43: net.AddIPCIDR("192.168.0.0/23", 1) +t44: net.AddIPCIDR("2001:db8:1111:2222::/64", 1) +t45: net.AddIPCIDR("192.168.0.0", 1) +t46: net.AddIPCIDR("10.0.0.0/8", -11) +t47: net.AddIPCIDR("255.0.0.0/8", 1) -- out/net -- Errors: t25: invalid value "2001:db8::1234567" (does not satisfy net.IPv6): @@ -64,6 +77,18 @@ t20: error in call to net.IPCIDR: netip.ParsePrefix("172.16.12.3"): no '/': t27: invalid value "23.23.23.23" (does not satisfy net.IPv6): ./in.cue:29:6 ./in.cue:29:19 +t39: error in call to net.AddIP: invalid IP "invalid ip": + ./in.cue:41:6 +t40: error in call to net.AddIP: IP address arithmetic resulted in out-of-range address (underflow): + ./in.cue:42:6 +t41: error in call to net.AddIP: IP address arithmetic resulted in out-of-range address (overflow): + ./in.cue:43:6 +t45: error in call to net.AddIPCIDR: netip.ParsePrefix("192.168.0.0"): no '/': + ./in.cue:47:6 +t46: error in call to net.AddIPCIDR: IP address arithmetic resulted in out-of-range address (underflow): + ./in.cue:48:6 +t47: error in call to net.AddIPCIDR: IP address arithmetic resulted in out-of-range address (overflow): + ./in.cue:49:6 Result: t1: "foo.bar." @@ -100,3 +125,16 @@ t31: "https://foo.com/bar" t32: _|_ // t32: invalid value "/foo/bar" (does not satisfy net.AbsURL): t32: error in call to net.AbsURL: URL is not absolute t33: "https://foo.com/bar" t34: _|_ // t34: invalid value "%" (does not satisfy net.AbsURL): t34: error in call to net.AbsURL: parse "%": invalid URL escape "%" +t35: "127.0.0.2" +t36: "127.0.0.3/8" +t37: "2001:db8::1" +t38: "2001:db8::2/64" +t39: _|_ // t39: error in call to net.AddIP: invalid IP "invalid ip" +t40: _|_ // t40: error in call to net.AddIP: IP address arithmetic resulted in out-of-range address (underflow) +t41: _|_ // t41: error in call to net.AddIP: IP address arithmetic resulted in out-of-range address (overflow) +t42: "::ffff:127.0.0.2" +t43: "192.168.2.0/23" +t44: "2001:db8:1111:2223::/64" +t45: _|_ // t45: error in call to net.AddIPCIDR: netip.ParsePrefix("192.168.0.0"): no '/' +t46: _|_ // t46: error in call to net.AddIPCIDR: IP address arithmetic resulted in out-of-range address (underflow) +t47: _|_ // t47: error in call to net.AddIPCIDR: IP address arithmetic resulted in out-of-range address (overflow)