diff --git a/internal/ipaddr/LICENSE b/internal/ipaddr/LICENSE new file mode 100644 index 000000000..744875676 --- /dev/null +++ b/internal/ipaddr/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/internal/ipaddr/PATENTS b/internal/ipaddr/PATENTS new file mode 100644 index 000000000..733099041 --- /dev/null +++ b/internal/ipaddr/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/internal/ipaddr/README.md b/internal/ipaddr/README.md new file mode 100644 index 000000000..f0d54988f --- /dev/null +++ b/internal/ipaddr/README.md @@ -0,0 +1,34 @@ +# Forked IP address parsing functions + +This directory contains a subset of code from the Go project's `net` package +as of Go 1.16, used under the Go project license which we've included here +in [`LICENSE`](LICENSE) and [`PATENTS`](PATENTS), which are also copied from +the Go project. + +Terraform has its own fork of these functions because Go 1.17 included a +breaking change to reject IPv4 address octets written with leading zeros. + +The Go project rationale for that change was that Go historically interpreted +leading-zero octets inconsistently with many other implementations, trimming +off the zeros and still treating the rest as decimal rather than treating the +octet as octal. + +The Go team made the reasonable observation that having a function that +interprets a non-normalized form in a manner inconsistent with other +implementations may cause naive validation or policy checks to produce +incorrect results, and thus it's a potential security concern. For more +information, see [Go issue #30999](https://golang.org/issue/30999). + +After careful consideration, the Terraform team has concluded that Terraform's +use of these functions as part of the implementation of the `cidrhost`, +`cidrsubnet`, `cidrsubnets`, and `cidrnetmask` functions has a more limited +impact than the general availability of these functions in the Go standard +library, and so we can't justify a similar exception to our Terraform 1.0 +compatibility promises as the Go team made to their Go 1.0 compatibility +promises. + +If you're considering using this package for new functionality _other than_ the +built-in functions mentioned above, please do so only if consistency with the +behavior of those functions is important. Otherwise, new features are not +burdened by the same compatibility constraints and so should typically prefer +to use the stricter interpretation of the upstream parsing functions. diff --git a/internal/ipaddr/doc.go b/internal/ipaddr/doc.go new file mode 100644 index 000000000..68d79c3c2 --- /dev/null +++ b/internal/ipaddr/doc.go @@ -0,0 +1,6 @@ +// Package ipaddr is a fork of a subset of the Go standard "net" package which +// retains parsing behaviors from Go 1.16 or earlier. +// +// Don't use this for any new code without careful consideration. See the +// README.md in the package directory for more information. +package ipaddr diff --git a/internal/ipaddr/ip.go b/internal/ipaddr/ip.go new file mode 100644 index 000000000..6d1c75d56 --- /dev/null +++ b/internal/ipaddr/ip.go @@ -0,0 +1,226 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// IP address manipulations +// +// IPv4 addresses are 4 bytes; IPv6 addresses are 16 bytes. +// An IPv4 address can be converted to an IPv6 address by +// adding a canonical prefix (10 zeros, 2 0xFFs). +// This library accepts either size of byte slice but always +// returns 16-byte addresses. + +package ipaddr + +import ( + stdnet "net" +) + +// +// Lean on the standard net lib as much as possible. +// + +type IP = stdnet.IP +type IPNet = stdnet.IPNet +type ParseError = stdnet.ParseError + +const IPv4len = stdnet.IPv4len +const IPv6len = stdnet.IPv6len + +var CIDRMask = stdnet.CIDRMask +var IPv4 = stdnet.IPv4 + +// Parse IPv4 address (d.d.d.d). +func parseIPv4(s string) IP { + var p [IPv4len]byte + for i := 0; i < IPv4len; i++ { + if len(s) == 0 { + // Missing octets. + return nil + } + if i > 0 { + if s[0] != '.' { + return nil + } + s = s[1:] + } + n, c, ok := dtoi(s) + if !ok || n > 0xFF { + return nil + } + // + // NOTE: This correct check was added for go-1.17, but is a + // backwards-incompatible change for Terraform users, who might have + // already written modules with leading zeroes. + // + //if c > 1 && s[0] == '0' { + // // Reject non-zero components with leading zeroes. + // return nil + //} + s = s[c:] + p[i] = byte(n) + } + if len(s) != 0 { + return nil + } + return IPv4(p[0], p[1], p[2], p[3]) +} + +// parseIPv6 parses s as a literal IPv6 address described in RFC 4291 +// and RFC 5952. +func parseIPv6(s string) (ip IP) { + ip = make(IP, IPv6len) + ellipsis := -1 // position of ellipsis in ip + + // Might have leading ellipsis + if len(s) >= 2 && s[0] == ':' && s[1] == ':' { + ellipsis = 0 + s = s[2:] + // Might be only ellipsis + if len(s) == 0 { + return ip + } + } + + // Loop, parsing hex numbers followed by colon. + i := 0 + for i < IPv6len { + // Hex number. + n, c, ok := xtoi(s) + if !ok || n > 0xFFFF { + return nil + } + + // If followed by dot, might be in trailing IPv4. + if c < len(s) && s[c] == '.' { + if ellipsis < 0 && i != IPv6len-IPv4len { + // Not the right place. + return nil + } + if i+IPv4len > IPv6len { + // Not enough room. + return nil + } + ip4 := parseIPv4(s) + if ip4 == nil { + return nil + } + ip[i] = ip4[12] + ip[i+1] = ip4[13] + ip[i+2] = ip4[14] + ip[i+3] = ip4[15] + s = "" + i += IPv4len + break + } + + // Save this 16-bit chunk. + ip[i] = byte(n >> 8) + ip[i+1] = byte(n) + i += 2 + + // Stop at end of string. + s = s[c:] + if len(s) == 0 { + break + } + + // Otherwise must be followed by colon and more. + if s[0] != ':' || len(s) == 1 { + return nil + } + s = s[1:] + + // Look for ellipsis. + if s[0] == ':' { + if ellipsis >= 0 { // already have one + return nil + } + ellipsis = i + s = s[1:] + if len(s) == 0 { // can be at end + break + } + } + } + + // Must have used entire string. + if len(s) != 0 { + return nil + } + + // If didn't parse enough, expand ellipsis. + if i < IPv6len { + if ellipsis < 0 { + return nil + } + n := IPv6len - i + for j := i - 1; j >= ellipsis; j-- { + ip[j+n] = ip[j] + } + for j := ellipsis + n - 1; j >= ellipsis; j-- { + ip[j] = 0 + } + } else if ellipsis >= 0 { + // Ellipsis must represent at least one 0 group. + return nil + } + return ip +} + +// ParseIP parses s as an IP address, returning the result. +// The string s can be in IPv4 dotted decimal ("192.0.2.1"), IPv6 +// ("2001:db8::68"), or IPv4-mapped IPv6 ("::ffff:192.0.2.1") form. +// If s is not a valid textual representation of an IP address, +// ParseIP returns nil. +func ParseIP(s string) IP { + for i := 0; i < len(s); i++ { + switch s[i] { + case '.': + return parseIPv4(s) + case ':': + return parseIPv6(s) + } + } + return nil +} + +// ParseCIDR parses s as a CIDR notation IP address and prefix length, +// like "192.0.2.0/24" or "2001:db8::/32", as defined in +// RFC 4632 and RFC 4291. +// +// It returns the IP address and the network implied by the IP and +// prefix length. +// For example, ParseCIDR("192.0.2.1/24") returns the IP address +// 192.0.2.1 and the network 192.0.2.0/24. +func ParseCIDR(s string) (IP, *IPNet, error) { + i := indexByteString(s, '/') + if i < 0 { + return nil, nil, &ParseError{Type: "CIDR address", Text: s} + } + addr, mask := s[:i], s[i+1:] + iplen := IPv4len + ip := parseIPv4(addr) + if ip == nil { + iplen = IPv6len + ip = parseIPv6(addr) + } + n, i, ok := dtoi(mask) + if ip == nil || !ok || i != len(mask) || n < 0 || n > 8*iplen { + return nil, nil, &ParseError{Type: "CIDR address", Text: s} + } + m := CIDRMask(n, 8*iplen) + return ip, &IPNet{IP: ip.Mask(m), Mask: m}, nil +} + +// This is copied from go/src/internal/bytealg, which includes versions +// optimized for various platforms. Those optimizations are elided here so we +// don't have to maintain them. +func indexByteString(s string, c byte) int { + for i := 0; i < len(s); i++ { + if s[i] == c { + return i + } + } + return -1 +} diff --git a/internal/ipaddr/ip_test.go b/internal/ipaddr/ip_test.go new file mode 100644 index 000000000..ddd95aa53 --- /dev/null +++ b/internal/ipaddr/ip_test.go @@ -0,0 +1,126 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ipaddr + +import ( + stdnet "net" + "reflect" + "testing" +) + +// +// Lean on the standard net lib as much as possible. +// +type IPMask = stdnet.IPMask + +var IPv4Mask = stdnet.IPv4Mask + +var parseIPTests = []struct { + in string + out IP +}{ + {"127.0.1.2", IPv4(127, 0, 1, 2)}, + {"127.0.0.1", IPv4(127, 0, 0, 1)}, + {"127.001.002.003", IPv4(127, 1, 2, 3)}, + {"127.007.008.009", IPv4(127, 7, 8, 9)}, + {"127.010.020.030", IPv4(127, 10, 20, 30)}, + {"::ffff:127.1.2.3", IPv4(127, 1, 2, 3)}, + {"::ffff:127.001.002.003", IPv4(127, 1, 2, 3)}, + {"::ffff:127.007.008.009", IPv4(127, 7, 8, 9)}, + {"::ffff:127.010.020.030", IPv4(127, 10, 20, 30)}, + {"::ffff:7f01:0203", IPv4(127, 1, 2, 3)}, + {"0:0:0:0:0000:ffff:127.1.2.3", IPv4(127, 1, 2, 3)}, + {"0:0:0:0:000000:ffff:127.1.2.3", IPv4(127, 1, 2, 3)}, + {"0:0:0:0::ffff:127.1.2.3", IPv4(127, 1, 2, 3)}, + + {"2001:4860:0:2001::68", IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}}, + {"2001:4860:0000:2001:0000:0000:0000:0068", IP{0x20, 0x01, 0x48, 0x60, 0, 0, 0x20, 0x01, 0, 0, 0, 0, 0, 0, 0x00, 0x68}}, + + {"-0.0.0.0", nil}, + {"0.-1.0.0", nil}, + {"0.0.-2.0", nil}, + {"0.0.0.-3", nil}, + {"127.0.0.256", nil}, + {"abc", nil}, + {"123:", nil}, + {"fe80::1%lo0", nil}, + {"fe80::1%911", nil}, + {"", nil}, + {"a1:a2:a3:a4::b1:b2:b3:b4", nil}, // Issue 6628 + // + // NOTE: These correct failures were added for go-1.17, but are a + // backwards-incompatible change for Terraform users, who might have + // already written modules using leading zeroes. + // + //{"127.001.002.003", nil}, + //{"::ffff:127.001.002.003", nil}, + //{"123.000.000.000", nil}, + //{"1.2..4", nil}, + //{"0123.0.0.1", nil}, +} + +func TestParseIP(t *testing.T) { + for _, tt := range parseIPTests { + if out := ParseIP(tt.in); !reflect.DeepEqual(out, tt.out) { + t.Errorf("ParseIP(%q) = %v, want %v", tt.in, out, tt.out) + } + } +} + +var parseCIDRTests = []struct { + in string + ip IP + net *IPNet + err error +}{ + {"135.104.0.0/32", IPv4(135, 104, 0, 0), &IPNet{IP: IPv4(135, 104, 0, 0), Mask: IPv4Mask(255, 255, 255, 255)}, nil}, + {"0.0.0.0/24", IPv4(0, 0, 0, 0), &IPNet{IP: IPv4(0, 0, 0, 0), Mask: IPv4Mask(255, 255, 255, 0)}, nil}, + {"135.104.0.0/24", IPv4(135, 104, 0, 0), &IPNet{IP: IPv4(135, 104, 0, 0), Mask: IPv4Mask(255, 255, 255, 0)}, nil}, + {"135.104.0.1/32", IPv4(135, 104, 0, 1), &IPNet{IP: IPv4(135, 104, 0, 1), Mask: IPv4Mask(255, 255, 255, 255)}, nil}, + {"135.104.0.1/24", IPv4(135, 104, 0, 1), &IPNet{IP: IPv4(135, 104, 0, 0), Mask: IPv4Mask(255, 255, 255, 0)}, nil}, + {"127.000.000.001/32", IPv4(127, 0, 0, 1), &IPNet{IP: IPv4(127, 0, 0, 1), Mask: IPv4Mask(255, 255, 255, 255)}, nil}, + {"127.007.008.009/32", IPv4(127, 7, 8, 9), &IPNet{IP: IPv4(127, 7, 8, 9), Mask: IPv4Mask(255, 255, 255, 255)}, nil}, + {"127.010.020.030/32", IPv4(127, 10, 20, 30), &IPNet{IP: IPv4(127, 10, 20, 30), Mask: IPv4Mask(255, 255, 255, 255)}, nil}, + {"::1/128", ParseIP("::1"), &IPNet{IP: ParseIP("::1"), Mask: IPMask(ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"))}, nil}, + {"abcd:2345::/127", ParseIP("abcd:2345::"), &IPNet{IP: ParseIP("abcd:2345::"), Mask: IPMask(ParseIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"))}, nil}, + {"abcd:2345::/65", ParseIP("abcd:2345::"), &IPNet{IP: ParseIP("abcd:2345::"), Mask: IPMask(ParseIP("ffff:ffff:ffff:ffff:8000::"))}, nil}, + {"abcd:2345::/64", ParseIP("abcd:2345::"), &IPNet{IP: ParseIP("abcd:2345::"), Mask: IPMask(ParseIP("ffff:ffff:ffff:ffff::"))}, nil}, + {"abcd:2345::/63", ParseIP("abcd:2345::"), &IPNet{IP: ParseIP("abcd:2345::"), Mask: IPMask(ParseIP("ffff:ffff:ffff:fffe::"))}, nil}, + {"abcd:2345::/33", ParseIP("abcd:2345::"), &IPNet{IP: ParseIP("abcd:2345::"), Mask: IPMask(ParseIP("ffff:ffff:8000::"))}, nil}, + {"abcd:2345::/32", ParseIP("abcd:2345::"), &IPNet{IP: ParseIP("abcd:2345::"), Mask: IPMask(ParseIP("ffff:ffff::"))}, nil}, + {"abcd:2344::/31", ParseIP("abcd:2344::"), &IPNet{IP: ParseIP("abcd:2344::"), Mask: IPMask(ParseIP("ffff:fffe::"))}, nil}, + {"abcd:2300::/24", ParseIP("abcd:2300::"), &IPNet{IP: ParseIP("abcd:2300::"), Mask: IPMask(ParseIP("ffff:ff00::"))}, nil}, + {"abcd:2345::/24", ParseIP("abcd:2345::"), &IPNet{IP: ParseIP("abcd:2300::"), Mask: IPMask(ParseIP("ffff:ff00::"))}, nil}, + {"2001:DB8::/48", ParseIP("2001:DB8::"), &IPNet{IP: ParseIP("2001:DB8::"), Mask: IPMask(ParseIP("ffff:ffff:ffff::"))}, nil}, + {"2001:DB8::1/48", ParseIP("2001:DB8::1"), &IPNet{IP: ParseIP("2001:DB8::"), Mask: IPMask(ParseIP("ffff:ffff:ffff::"))}, nil}, + {"192.168.1.1/255.255.255.0", nil, nil, &ParseError{Type: "CIDR address", Text: "192.168.1.1/255.255.255.0"}}, + {"192.168.1.1/35", nil, nil, &ParseError{Type: "CIDR address", Text: "192.168.1.1/35"}}, + {"2001:db8::1/-1", nil, nil, &ParseError{Type: "CIDR address", Text: "2001:db8::1/-1"}}, + {"2001:db8::1/-0", nil, nil, &ParseError{Type: "CIDR address", Text: "2001:db8::1/-0"}}, + {"-0.0.0.0/32", nil, nil, &ParseError{Type: "CIDR address", Text: "-0.0.0.0/32"}}, + {"0.-1.0.0/32", nil, nil, &ParseError{Type: "CIDR address", Text: "0.-1.0.0/32"}}, + {"0.0.-2.0/32", nil, nil, &ParseError{Type: "CIDR address", Text: "0.0.-2.0/32"}}, + {"0.0.0.-3/32", nil, nil, &ParseError{Type: "CIDR address", Text: "0.0.0.-3/32"}}, + {"0.0.0.0/-0", nil, nil, &ParseError{Type: "CIDR address", Text: "0.0.0.0/-0"}}, + // + // NOTE: Theis correct failure was added for go-1.17, but is a + // backwards-incompatible change for Terraform users, who might have + // already written modules using leading zeroes. + // + //{"127.000.000.001/32", nil, nil, &ParseError{Type: "CIDR address", Text: "127.000.000.001/32"}}, + {"", nil, nil, &ParseError{Type: "CIDR address", Text: ""}}, +} + +func TestParseCIDR(t *testing.T) { + for _, tt := range parseCIDRTests { + ip, net, err := ParseCIDR(tt.in) + if !reflect.DeepEqual(err, tt.err) { + t.Errorf("ParseCIDR(%q) = %v, %v; want %v, %v", tt.in, ip, net, tt.ip, tt.net) + } + if err == nil && (!tt.ip.Equal(ip) || !tt.net.IP.Equal(net.IP) || !reflect.DeepEqual(net.Mask, tt.net.Mask)) { + t.Errorf("ParseCIDR(%q) = %v, {%v, %v}; want %v, {%v, %v}", tt.in, ip, net.IP, net.Mask, tt.ip, tt.net.IP, tt.net.Mask) + } + } +} diff --git a/internal/ipaddr/parse.go b/internal/ipaddr/parse.go new file mode 100644 index 000000000..07d6eece4 --- /dev/null +++ b/internal/ipaddr/parse.go @@ -0,0 +1,54 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Simple file i/o and string manipulation, to avoid +// depending on strconv and bufio and strings. + +package ipaddr + +// Bigger than we need, not too big to worry about overflow +const big = 0xFFFFFF + +// Decimal to integer. +// Returns number, characters consumed, success. +func dtoi(s string) (n int, i int, ok bool) { + n = 0 + for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { + n = n*10 + int(s[i]-'0') + if n >= big { + return big, i, false + } + } + if i == 0 { + return 0, 0, false + } + return n, i, true +} + +// Hexadecimal to integer. +// Returns number, characters consumed, success. +func xtoi(s string) (n int, i int, ok bool) { + n = 0 + for i = 0; i < len(s); i++ { + if '0' <= s[i] && s[i] <= '9' { + n *= 16 + n += int(s[i] - '0') + } else if 'a' <= s[i] && s[i] <= 'f' { + n *= 16 + n += int(s[i]-'a') + 10 + } else if 'A' <= s[i] && s[i] <= 'F' { + n *= 16 + n += int(s[i]-'A') + 10 + } else { + break + } + if n >= big { + return 0, i, false + } + } + if i == 0 { + return 0, i, false + } + return n, i, true +} diff --git a/internal/lang/funcs/cidr.go b/internal/lang/funcs/cidr.go index 3a20ec282..c6f42c4d7 100644 --- a/internal/lang/funcs/cidr.go +++ b/internal/lang/funcs/cidr.go @@ -3,9 +3,9 @@ package funcs import ( "fmt" "math/big" - "net" "github.com/apparentlymart/go-cidr/cidr" + "github.com/hashicorp/terraform/internal/ipaddr" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/gocty" @@ -30,7 +30,7 @@ var CidrHostFunc = function.New(&function.Spec{ if err := gocty.FromCtyValue(args[1], &hostNum); err != nil { return cty.UnknownVal(cty.String), err } - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ipaddr.ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) } @@ -55,12 +55,12 @@ var CidrNetmaskFunc = function.New(&function.Spec{ }, Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ipaddr.ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) } - return cty.StringVal(net.IP(network.Mask).String()), nil + return cty.StringVal(ipaddr.IP(network.Mask).String()), nil }, }) @@ -92,7 +92,7 @@ var CidrSubnetFunc = function.New(&function.Spec{ return cty.UnknownVal(cty.String), err } - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ipaddr.ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) } @@ -121,7 +121,7 @@ var CidrSubnetsFunc = function.New(&function.Spec{ }, Type: function.StaticReturnType(cty.List(cty.String)), Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - _, network, err := net.ParseCIDR(args[0].AsString()) + _, network, err := ipaddr.ParseCIDR(args[0].AsString()) if err != nil { return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid CIDR expression: %s", err) } diff --git a/internal/lang/funcs/cidr_test.go b/internal/lang/funcs/cidr_test.go index 4bed530cb..524653111 100644 --- a/internal/lang/funcs/cidr_test.go +++ b/internal/lang/funcs/cidr_test.go @@ -32,6 +32,19 @@ func TestCidrHost(t *testing.T) { cty.StringVal("192.168.1.0"), false, }, + { + // We inadvertently inherited a pre-Go1.17 standard library quirk + // if parsing zero-prefix parts as decimal rather than octal. + // Go 1.17 resolved that quirk by making zero-prefix invalid, but + // we've preserved our existing behavior for backward compatibility, + // on the grounds that these functions are for generating addresses + // rather than validating or processing them. We do always generate + // a canonical result regardless of the input, though. + cty.StringVal("010.001.0.0/24"), + cty.NumberIntVal(6), + cty.StringVal("10.1.0.6"), + false, + }, { cty.StringVal("192.168.1.0/30"), cty.NumberIntVal(255), @@ -110,6 +123,17 @@ func TestCidrNetmask(t *testing.T) { cty.StringVal("ffff:ffff:ffff:ffff::"), false, }, + { + // We inadvertently inherited a pre-Go1.17 standard library quirk + // if parsing zero-prefix parts as decimal rather than octal. + // Go 1.17 resolved that quirk by making zero-prefix invalid, but + // we've preserved our existing behavior for backward compatibility, + // on the grounds that these functions are for generating addresses + // rather than validating or processing them. + cty.StringVal("010.001.0.0/24"), + cty.StringVal("255.255.255.0"), + false, + }, { cty.StringVal("not-a-cidr"), cty.UnknownVal(cty.String), @@ -178,6 +202,20 @@ func TestCidrSubnet(t *testing.T) { cty.StringVal("fe80::3:0:0:0/81"), false, }, + { + // We inadvertently inherited a pre-Go1.17 standard library quirk + // if parsing zero-prefix parts as decimal rather than octal. + // Go 1.17 resolved that quirk by making zero-prefix invalid, but + // we've preserved our existing behavior for backward compatibility, + // on the grounds that these functions are for generating addresses + // rather than validating or processing them. We do always generate + // a canonical result regardless of the input, though. + cty.StringVal("010.001.0.0/24"), + cty.NumberIntVal(4), + cty.NumberIntVal(1), + cty.StringVal("10.1.0.16/28"), + false, + }, { // not enough bits left cty.StringVal("192.168.0.0/30"), cty.NumberIntVal(4), @@ -267,6 +305,23 @@ func TestCidrSubnets(t *testing.T) { }), ``, }, + { + // We inadvertently inherited a pre-Go1.17 standard library quirk + // if parsing zero-prefix parts as decimal rather than octal. + // Go 1.17 resolved that quirk by making zero-prefix invalid, but + // we've preserved our existing behavior for backward compatibility, + // on the grounds that these functions are for generating addresses + // rather than validating or processing them. We do always generate + // a canonical result regardless of the input, though. + cty.StringVal("010.0.0.0/21"), + []cty.Value{ + cty.NumberIntVal(3), + }, + cty.ListVal([]cty.Value{ + cty.StringVal("10.0.0.0/24"), + }), + ``, + }, { cty.StringVal("10.0.0.0/30"), []cty.Value{ diff --git a/website/docs/language/functions/cidrhost.html.md b/website/docs/language/functions/cidrhost.html.md index e43540362..af43c4034 100644 --- a/website/docs/language/functions/cidrhost.html.md +++ b/website/docs/language/functions/cidrhost.html.md @@ -34,6 +34,11 @@ situations, such as point-to-point links. This function accepts both IPv6 and IPv4 prefixes, and the result always uses the same addressing scheme as the given prefix. +-> **Note:** As a historical accident, this function interprets IPv4 address +octets that have leading zeros as decimal numbers, which is contrary to some +other systems which interpret them as octal. We have preserved this behavior +for backward compatibility, but recommend against relying on this behavior. + ## Examples ``` diff --git a/website/docs/language/functions/cidrnetmask.html.md b/website/docs/language/functions/cidrnetmask.html.md index bb3de4f7b..a19140ebd 100644 --- a/website/docs/language/functions/cidrnetmask.html.md +++ b/website/docs/language/functions/cidrnetmask.html.md @@ -25,6 +25,11 @@ IPv4 address syntax, as expected by some software. CIDR notation is the only valid notation for IPv6 addresses, so `cidrnetmask` produces an error if given an IPv6 address. +-> **Note:** As a historical accident, this function interprets IPv4 address +octets that have leading zeros as decimal numbers, which is contrary to some +other systems which interpret them as octal. We have preserved this behavior +for backward compatibility, but recommend against relying on this behavior. + ## Examples ``` diff --git a/website/docs/language/functions/cidrsubnet.html.md b/website/docs/language/functions/cidrsubnet.html.md index e64c894fb..5b27aec36 100644 --- a/website/docs/language/functions/cidrsubnet.html.md +++ b/website/docs/language/functions/cidrsubnet.html.md @@ -34,6 +34,11 @@ allows you to give a specific network number to use. `cidrsubnets` can allocate multiple network addresses at once, but numbers them automatically starting with zero. +-> **Note:** As a historical accident, this function interprets IPv4 address +octets that have leading zeros as decimal numbers, which is contrary to some +other systems which interpret them as octal. We have preserved this behavior +for backward compatibility, but recommend against relying on this behavior. + ## Examples ``` diff --git a/website/docs/language/functions/cidrsubnets.html.md b/website/docs/language/functions/cidrsubnets.html.md index 4f43635c7..d1c604ee3 100644 --- a/website/docs/language/functions/cidrsubnets.html.md +++ b/website/docs/language/functions/cidrsubnets.html.md @@ -39,6 +39,11 @@ existing calls safely, as long as there is sufficient address space available. This function accepts both IPv6 and IPv4 prefixes, and the result always uses the same addressing scheme as the given prefix. +-> **Note:** As a historical accident, this function interprets IPv4 address +octets that have leading zeros as decimal numbers, which is contrary to some +other systems which interpret them as octal. We have preserved this behavior +for backward compatibility, but recommend against relying on this behavior. + -> **Note:** [The Terraform module `hashicorp/subnets/cidr`](https://registry.terraform.io/modules/hashicorp/subnets/cidr) wraps `cidrsubnets` to provide additional functionality for assigning symbolic names to your networks and skipping prefixes for obsolete allocations. Its