diff --git a/cmd/static/default-config.toml b/cmd/static/default-config.toml index 5d82a11..b815208 100644 --- a/cmd/static/default-config.toml +++ b/cmd/static/default-config.toml @@ -101,9 +101,9 @@ TargetMap = [] # ["IP:{{SOURCE_IP}}", "Proxy:lets-proxy", "Protocol:{{HTTP_PROTO}}" ] Headers = [ "X-Forwarded-Proto:{{HTTP_PROTO}}", "X-Forwarded-For:{{SOURCE_IP}}" ] - # A map with an IP key/mask and a value with an array of strings separated by a colon Header:Value # to add to a request with matching ip address for backend. +# You can use General.IncludeConfigs for load rules from dedicated rules config file. # Example: # [Proxy.HeadersByIP] #"192.168.0.0/24" = ["IPLocal:Test"] diff --git a/go.mod b/go.mod index 11a8e9a..d84ed1b 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( ) require ( + github.com/egorgasay/cidranger v1.0.1 github.com/hashicorp/golang-lru/v2 v2.0.1 github.com/jonboulle/clockwork v0.4.0 github.com/letsencrypt/pebble/v2 v2.4.0 @@ -43,6 +44,7 @@ require ( github.com/prometheus/common v0.34.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/valyala/fastrand v1.1.0 // indirect + github.com/yl2chen/cidranger v1.0.2 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect golang.org/x/mod v0.8.0 // indirect diff --git a/go.sum b/go.sum index ad6ed20..61f4d7b 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/egorgasay/cidranger v1.0.1 h1:hSU9Yihw8Tx1QqVgjKY4udAJfjC/hQquhZC+9OUDnec= +github.com/egorgasay/cidranger v1.0.1/go.mod h1:UHMmoDd2MvfRfOM+9lhqeakZW/pKy7APt49TztmcFjo= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -233,6 +235,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= +github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= +github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/proxy/config_test.go b/internal/proxy/config_test.go index 206058e..a094b52 100644 --- a/internal/proxy/config_test.go +++ b/internal/proxy/config_test.go @@ -1,7 +1,11 @@ package proxy import ( + "fmt" + "github.com/egorgasay/cidranger" "net" + "strconv" + "strings" "testing" "github.com/rekby/lets-proxy2/internal/th" @@ -295,18 +299,15 @@ func TestConfig_Apply(t *testing.T) { func TestConfig_getHeadersByIPDirector(t *testing.T) { ctx, flush := th.TestContext(t) defer flush() - td := testdeep.NewT(t) tests := []struct { name string c Config - want DirectorSetHeadersByIP wantErr bool }{ { name: "empty", c: Config{}, - want: nil, }, { name: "oneNetwork", @@ -319,16 +320,6 @@ func TestConfig_getHeadersByIPDirector(t *testing.T) { }, }, }, - want: DirectorSetHeadersByIP{ - NetHeaders{ - IPNet: net.IPNet{IP: net.ParseIP("192.168.1.0"), Mask: net.CIDRMask(24, 32)}, - Headers: HTTPHeaders{ - {Name: "User-Agent", Value: "PostmanRuntime/7.29.2"}, - {Name: "Accept", Value: "*/*"}, - {Name: "Accept-Encoding", Value: "gzip, deflate, br"}, - }, - }, - }, }, { name: "configError1", @@ -341,19 +332,18 @@ func TestConfig_getHeadersByIPDirector(t *testing.T) { }, }, }, - want: DirectorSetHeadersByIP{}, wantErr: true, }, { name: "5Networks", c: Config{ HeadersByIP: map[string][]string{ - "10.0.0.0/8": { + "11.0.0.0/8": { "User-Agent:PostmanRuntime/7.29.2", "Accept:*/*", "Accept-Encoding:gzip, deflate, br", }, - "10.0.0.0/24": { + "10.55.0.0/24": { "Connection:Keep-Alive", "Upgrade-Insecure-Requests:1", "Cache-Control:no-cache", @@ -375,47 +365,6 @@ func TestConfig_getHeadersByIPDirector(t *testing.T) { }, }, }, - want: DirectorSetHeadersByIP{ - NetHeaders{ - IPNet: net.IPNet{IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(8, 32)}, - Headers: HTTPHeaders{ - {Name: "User-Agent", Value: "PostmanRuntime/7.29.2"}, - {Name: "Accept", Value: "*/*"}, - {Name: "Accept-Encoding", Value: "gzip, deflate, br"}, - }, - }, - NetHeaders{ - IPNet: net.IPNet{IP: net.ParseIP("10.0.0.0"), Mask: net.CIDRMask(24, 32)}, - Headers: HTTPHeaders{ - {Name: "Connection", Value: "Keep-Alive"}, - {Name: "Upgrade-Insecure-Requests", Value: "1"}, - {Name: "Cache-Control", Value: "no-cache"}, - }, - }, - NetHeaders{ - IPNet: net.IPNet{IP: net.ParseIP("10.0.1.0"), Mask: net.CIDRMask(24, 32)}, - Headers: HTTPHeaders{ - {Name: "Origin", Value: "https://example.com"}, - {Name: "Content-Type", Value: "application/json"}, - {Name: "Content-Length", Value: "123"}, - }, - }, - NetHeaders{ - IPNet: net.IPNet{IP: net.ParseIP("10.2.0.0"), Mask: net.CIDRMask(24, 32)}, - Headers: HTTPHeaders{ - {Name: "Accept-Encoding", Value: "gzip, deflate, br"}, - {Name: "Accept-Language", Value: "en-US,en;q=0.9"}, - }, - }, - NetHeaders{ - IPNet: net.IPNet{IP: net.ParseIP("fe80:0000:0000:0000::"), Mask: net.CIDRMask(64, 128)}, - Headers: HTTPHeaders{ - {Name: "Accept", Value: "*/*"}, - {Name: "Accept-Encoding", Value: "gzip, deflate, br"}, - {Name: "Accept-Language", Value: "en-US,en;q=0.9"}, - }, - }, - }, }, } for _, tt := range tests { @@ -427,39 +376,89 @@ func TestConfig_getHeadersByIPDirector(t *testing.T) { if tt.wantErr { return + } else if got == nil { + return } - getMapDir := func(d DirectorSetHeadersByIP) map[string]map[string]string { - var mp = make(map[string]map[string]string) - for _, netHeaders := range d { - if netHeaders.Headers == nil { - continue - } - if _, ok := mp[netHeaders.IPNet.String()]; !ok { - mp[netHeaders.IPNet.String()] = make(map[string]string) - } - for _, header := range netHeaders.Headers { - mp[netHeaders.IPNet.String()][header.Name] = header.Value - } + ranger := got.(cidranger.Ranger[HTTPHeaders]) + for network, headers := range tt.c.HeadersByIP { + _, ipnet, err := net.ParseCIDR(network) + if err != nil { + t.Fatalf("ParseCIDR error %v", err) } - t.Logf("getMapDir() got = %v", mp) - return mp - } + ip, err := netToIP(ipnet) + if err != nil { + t.Fatalf("netToIP error %v", err) + } - if got == nil && tt.want == nil { - return - } + if ok, err := ranger.Contains(ip); err != nil { + t.Fatalf("contains error %v", err) + } else if !ok { + t.Fatalf("network %s not found", network) + } + + gotHeaders := make([]string, 0, len(headers)) + + err = ranger.IterByIncomingNetworks(ip, func(network net.IPNet, h HTTPHeaders) error { + if headers == nil { + return nil + } + + for _, header := range h { + gotHeaders = append(gotHeaders, fmt.Sprintf("%s:%s", header.Name, header.Value)) + } + return nil + }) + if err != nil { + t.Fatalf("IterByIncomingNetworks error %v", err) + } - if gotDir, ok := got.(DirectorSetHeadersByIP); !ok { - t.Fatalf("can't lead to the type %v", got) - } else { - gotMap := getMapDir(gotDir) - wantMap := getMapDir(tt.want) - if !td.CmpDeeply(gotMap, wantMap) { - t.Fatalf("getHeadersByIPDirector() got = %v, want %v", gotMap, wantMap) + if !isTheSameArray(headers, gotHeaders) { + t.Fatalf("want \n%v \ngot \n%v", headers, gotHeaders) } } }) } } + +func isTheSameArray[T comparable](a, b []T) bool { + if len(a) != len(b) { + return false + } + + tmp := make(map[T]struct{}) + for _, el := range a { + tmp[el] = struct{}{} + } + for _, el := range b { + if _, ok := tmp[el]; !ok { + return false + } + } + return true +} + +func netToIP(ipnet *net.IPNet) (net.IP, error) { + ip := ipnet.IP.String() + + sep := ":" + if ipnet.IP.To4() != nil { + sep = "." + } + + split := strings.Split(ip, sep) + + if sep == ":" && split[len(split)-1] == "" { + return net.ParseIP(strings.Join(split, sep) + "1"), nil + } + num, err := strconv.Atoi(split[len(split)-1]) + if err != nil { + return nil, err + } + + num++ + split[3] = strconv.Itoa(num) + + return net.ParseIP(strings.Join(split, sep)), nil +} diff --git a/internal/proxy/directors.go b/internal/proxy/directors.go index f1df1f3..e64ac34 100644 --- a/internal/proxy/directors.go +++ b/internal/proxy/directors.go @@ -1,21 +1,20 @@ package proxy import ( - "bytes" "fmt" + "github.com/rekby/lets-proxy2/internal/contextlabel" "net" "net/http" "net/url" - "sort" "strconv" - "github.com/rekby/lets-proxy2/internal/contextlabel" - "github.com/rekby/lets-proxy2/internal/log" zc "github.com/rekby/zapcontext" "go.uber.org/zap" + + "github.com/egorgasay/cidranger" ) const ( @@ -185,51 +184,45 @@ type HTTPHeader struct { Value string } type HTTPHeaders []HTTPHeader -type NetHeaders struct { - IPNet net.IPNet - Headers HTTPHeaders + +type Net struct { + net.IPNet +} + +func (n Net) Network() net.IPNet { return n.IPNet } + +type DirectorSetHeadersByIP struct { + allHeaders []string + cidranger.Ranger[HTTPHeaders] } -type DirectorSetHeadersByIP []NetHeaders func NewDirectorSetHeadersByIP(m map[string]HTTPHeaders) (DirectorSetHeadersByIP, error) { - res := make(DirectorSetHeadersByIP, 0, len(m)) + allHeadersSet := make(map[string]struct{}) + + ranger := cidranger.NewPCTrieRanger[HTTPHeaders]() for k, v := range m { _, subnet, err := net.ParseCIDR(k) if err != nil { - return nil, fmt.Errorf("can't parse CIDR: %v %w", k, err) + return DirectorSetHeadersByIP{}, fmt.Errorf("can't parse CIDR: %v %w", k, err) + } + + err = ranger.Insert(&Net{IPNet: *subnet}, v) + if err != nil { + return DirectorSetHeadersByIP{}, fmt.Errorf("can't insert into cidranger %w", err) } - res = append(res, NetHeaders{ - IPNet: *subnet, - Headers: v, - }) + for _, header := range v { + allHeadersSet[http.CanonicalHeaderKey(header.Name)] = struct{}{} + } } - sortByIPNet(res) - return res, nil -} -func sortByIPNet(d DirectorSetHeadersByIP) { - sort.Slice(d, func(i, j int) bool { - left, right := d[i], d[j] + allHeaders := make([]string, 0, len(allHeadersSet)) - maskOnes := func(m net.IPMask) int { - ones, _ := m.Size() - return ones - } + for k := range allHeadersSet { + allHeaders = append(allHeaders, k) + } - switch { - case len(left.IPNet.IP) < len(right.IPNet.IP): - return true - case len(left.IPNet.IP) > len(right.IPNet.IP): - return false - case maskOnes(left.IPNet.Mask) < maskOnes(right.IPNet.Mask): - return true - case maskOnes(left.IPNet.Mask) > maskOnes(right.IPNet.Mask): - return false - default: - return bytes.Compare(left.IPNet.IP, right.IPNet.IP) < 0 - } - }) + return DirectorSetHeadersByIP{Ranger: ranger, allHeaders: allHeaders}, nil } func (h DirectorSetHeadersByIP) Director(request *http.Request) error { @@ -246,18 +239,23 @@ func (h DirectorSetHeadersByIP) Director(request *http.Request) error { ip := net.ParseIP(host) - for _, ipHeaders := range h { - if !ipHeaders.IPNet.Contains(ip) { - continue - } + for _, headerName := range h.allHeaders { + delete(request.Header, headerName) + } + err = h.IterByIncomingNetworks(ip, func(network net.IPNet, value HTTPHeaders) error { if request.Header == nil { request.Header = make(http.Header) } - for _, header := range ipHeaders.Headers { - request.Header.Set(header.Name, header.Value) + for _, header := range value { + request.Header[header.Name] = []string{header.Value} } + + return nil + }) + if err != nil { + return fmt.Errorf("can't iterate cidranger %w", err) } return nil diff --git a/internal/proxy/directors_test.go b/internal/proxy/directors_test.go index ca8978f..827c4c0 100644 --- a/internal/proxy/directors_test.go +++ b/internal/proxy/directors_test.go @@ -156,6 +156,17 @@ func TestDirectorSetHeadersByIP(t *testing.T) { "fe80:0000:0000:0000::/64": { {Name: "TestHeader5", Value: "TestHeaderValue5"}, }, + "8.0.0.0/8": { + {Name: "X", Value: "1"}, + {Name: "Y", Value: "2"}, + }, + "8.1.0.0/16": { + {Name: "X", Value: "4"}, + {Name: "Z", Value: "3"}, + }, + "8.1.2.0/24": { + {Name: "O", Value: "443"}, + }, } td := testdeep.NewT(t) @@ -166,6 +177,7 @@ func TestDirectorSetHeadersByIP(t *testing.T) { name string args args shouldModify bool + want HTTPHeaders wantErr bool }{ { @@ -173,6 +185,69 @@ func TestDirectorSetHeadersByIP(t *testing.T) { args: args{ request: &http.Request{RemoteAddr: "192.168.0.19:897"}, }, + want: HTTPHeaders{ + {Name: "TestHeader1", Value: "TestHeaderValue1"}, + {Name: "TestHeader2", Value: "TestHeaderValue2"}, + {Name: "TestHeader3", Value: "TestHeaderValue3"}, + {Name: "TestHeader4", Value: "TestHeaderValue4"}, + }, + shouldModify: true, + }, + { + name: "okIPv4_2", + args: args{ + request: &http.Request{RemoteAddr: "8.1.2.19:897"}, + }, + want: HTTPHeaders{ + {Name: "O", Value: "443"}, + {Name: "X", Value: "4"}, + {Name: "Y", Value: "2"}, + {Name: "Z", Value: "3"}, + }, + shouldModify: true, + }, + { + name: "okIPv4_RemoveTestHeader1_IterOverReqHeaders", + args: args{ + request: &http.Request{RemoteAddr: "8.1.2.19:897", Header: http.Header{ + "TestHeader1": []string{""}, + "SHOULD_KEEP": []string{"_THIS"}, + }}, + }, + want: HTTPHeaders{ + {Name: "O", Value: "443"}, + {Name: "X", Value: "4"}, + {Name: "Y", Value: "2"}, + {Name: "Z", Value: "3"}, + {Name: "SHOULD_KEEP", Value: "_THIS"}, + }, + shouldModify: true, + }, + { + name: "okIPv4_RemoveTestHeader1_IterOverRules", + args: args{ + request: &http.Request{RemoteAddr: "89.19.92.199:897", Header: http.Header{ + "TestHeader1": []string{""}, + "TestHeader2": []string{""}, + "TestHeader3": []string{""}, + "TestHeader4": []string{""}, + "TestHeader5": []string{""}, + "SHOULD_KEEP1": []string{""}, + "SHOULD_KEEP2": []string{""}, + "SHOULD_KEEP3": []string{""}, + "TestHeader6": []string{"SHOULD_KEEP4"}, + "SHOULD_KEEP5": []string{""}, + "SHOULD_KEEP6": []string{""}, + }}, + }, + want: HTTPHeaders{ + {Name: "SHOULD_KEEP1", Value: ""}, + {Name: "SHOULD_KEEP2", Value: ""}, + {Name: "SHOULD_KEEP3", Value: ""}, + {Name: "TestHeader6", Value: "SHOULD_KEEP4"}, + {Name: "SHOULD_KEEP5", Value: ""}, + {Name: "SHOULD_KEEP6", Value: ""}, + }, shouldModify: true, }, { @@ -180,6 +255,9 @@ func TestDirectorSetHeadersByIP(t *testing.T) { args: args{ request: &http.Request{RemoteAddr: "[fe80::28ca:829b:2d2e:a908]:897"}, }, + want: HTTPHeaders{ + {Name: "TestHeader5", Value: "TestHeaderValue5"}, + }, shouldModify: true, }, { @@ -193,9 +271,10 @@ func TestDirectorSetHeadersByIP(t *testing.T) { { name: "wrongAddrIPv4", args: args{ - request: &http.Request{RemoteAddr: "172.168.0.1:897"}, + request: &http.Request{RemoteAddr: "172.168.0:897"}, }, shouldModify: false, + wantErr: true, }, { name: "wrongAddrIPv6", @@ -203,6 +282,7 @@ func TestDirectorSetHeadersByIP(t *testing.T) { request: &http.Request{RemoteAddr: "[ae80:28ca:27ca:829b:2d2e:a908]:897"}, }, shouldModify: false, + wantErr: true, }, { name: "noPortIPv4", @@ -210,6 +290,7 @@ func TestDirectorSetHeadersByIP(t *testing.T) { request: &http.Request{RemoteAddr: "172.168.0.1"}, }, shouldModify: false, + wantErr: true, }, { name: "noPortIPv6", @@ -217,6 +298,7 @@ func TestDirectorSetHeadersByIP(t *testing.T) { request: &http.Request{RemoteAddr: "[ae80:28ca:27ca:829b:2d2e:a908]"}, }, shouldModify: false, + wantErr: true, }, } @@ -234,24 +316,35 @@ func TestDirectorSetHeadersByIP(t *testing.T) { } var found bool - for _, netHeaders := range d { + for network := range m { + _, cidr, err := net.ParseCIDR(network) + if err != nil { + t.Errorf("ParseCIDR: %v", err) + } + split := strings.Split(tt.args.request.RemoteAddr, ":") - ip := tt.args.request.RemoteAddr + addr := tt.args.request.RemoteAddr if len(split) > 1 { - ip = strings.Trim(strings.Join(split[:len(split)-1], ":"), "[]") + addr = strings.Trim(strings.Join(split[:len(split)-1], ":"), "[]") } else if len(split) == 0 { t.Errorf("Director() RemoteAddr error") continue } - if !netHeaders.IPNet.Contains(net.ParseIP(ip)) { + ip := net.ParseIP(addr) + + if (ip.To4() != nil && cidr.IP.To4() == nil) || (ip.To4() == nil && cidr.IP.To4() != nil) { continue } found = true - for _, header := range netHeaders.Headers { - td.CmpDeeply(tt.args.request.Header.Get(header.Name), header.Value) + for _, header := range tt.want { + v, exists := tt.args.request.Header[header.Name] + if !exists { + t.Errorf("Director() header not found: %v", header.Name) + } + td.CmpDeeply(v, []string{header.Value}) } } @@ -275,7 +368,7 @@ func TestNewDirectorSetHeadersByIP(t *testing.T) { tests := []struct { name string args args - want DirectorSetHeadersByIP + ips []net.IP wantErr bool }{ { @@ -289,28 +382,12 @@ func TestNewDirectorSetHeadersByIP(t *testing.T) { {Name: "TestHeader3", Value: "TestHeaderValue3"}, {Name: "TestHeader4", Value: "TestHeaderValue4"}, }, - "fe80:0000:0000:0000::/64": { - {Name: "TestHeader5", Value: "TestHeaderValue5"}, - }, - }, - }, - want: DirectorSetHeadersByIP{ - { - IPNet: net.IPNet{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(24, 32)}, - Headers: HTTPHeaders{ - {Name: "TestHeader1", Value: "TestHeaderValue1"}, - {Name: "TestHeader2", Value: "TestHeaderValue2"}, - {Name: "TestHeader3", Value: "TestHeaderValue3"}, - {Name: "TestHeader4", Value: "TestHeaderValue4"}, - }, - }, - { - IPNet: net.IPNet{IP: net.ParseIP("fe80::"), Mask: net.CIDRMask(64, 128)}, - Headers: HTTPHeaders{ + "fe80::/64": { {Name: "TestHeader5", Value: "TestHeaderValue5"}, }, }, }, + ips: []net.IP{net.ParseIP("192.168.0.1"), net.ParseIP("fe80:0000:0000:0000::1")}, }, { name: "wrongFormat", @@ -323,12 +400,11 @@ func TestNewDirectorSetHeadersByIP(t *testing.T) { {Name: "TestHeader3", Value: "TestHeaderValue3"}, {Name: "TestHeader4", Value: "TestHeaderValue4"}, }, - "fe80:0000:0000:0000::/64": { + "fe80::/64": { {Name: "TestHeader5", Value: "TestHeaderValue5"}, }, }, }, - want: DirectorSetHeadersByIP{}, wantErr: true, }, } @@ -343,98 +419,18 @@ func TestNewDirectorSetHeadersByIP(t *testing.T) { return } - found := false - for _, gotNetHeaders := range got { - for _, wantNetHeaders := range tt.want { - if gotNetHeaders.IPNet.String() != wantNetHeaders.IPNet.String() { - continue - } - found = true - td.CmpDeeply(gotNetHeaders.Headers, wantNetHeaders.Headers) - } - } - if !found { - t.Errorf("NewDirectorSetHeadersByIP() headers not found") + for _, ip := range tt.ips { + err = got.IterByIncomingNetworks(ip, func(network net.IPNet, value HTTPHeaders) error { + cidr := network.String() + td.CmpDeeply(value, tt.args.m[cidr]) + delete(tt.args.m, cidr) + return nil + }) + td.CmpNoError(err) } - }) - } -} - -func Test_sortByIPNet(t *testing.T) { - _, flush := th.TestContext(t) - defer flush() - td := testdeep.NewT(t) - - type args struct { - d DirectorSetHeadersByIP - } - tests := []struct { - name string - args args - want DirectorSetHeadersByIP - }{ - { - name: "IPv4Only", - args: args{ - d: DirectorSetHeadersByIP{ - {IPNet: net.IPNet{IP: net.ParseIP("192.168.88.0"), Mask: net.CIDRMask(24, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.0.0.0"), Mask: net.CIDRMask(8, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(16, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.99.0"), Mask: net.CIDRMask(24, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("172.0.0.0"), Mask: net.CIDRMask(8, 32)}}, - }, - }, - want: DirectorSetHeadersByIP{ - {IPNet: net.IPNet{IP: net.ParseIP("172.0.0.0"), Mask: net.CIDRMask(8, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.0.0.0"), Mask: net.CIDRMask(8, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("172.16.0.0"), Mask: net.CIDRMask(16, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.88.0"), Mask: net.CIDRMask(24, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.99.0"), Mask: net.CIDRMask(24, 32)}}, - }, - }, - { - name: "IPv6Only", - args: args{ - d: DirectorSetHeadersByIP{ - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678::"), Mask: net.CIDRMask(64, 128)}}, - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234::"), Mask: net.CIDRMask(48, 128)}}, - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678:abcd::"), Mask: net.CIDRMask(80, 128)}}, - }, - }, - want: DirectorSetHeadersByIP{ - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234::"), Mask: net.CIDRMask(48, 128)}}, - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678::"), Mask: net.CIDRMask(64, 128)}}, - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678:abcd::"), Mask: net.CIDRMask(80, 128)}}, - }, - }, - { - name: "IPv6AndIPv4", - args: args{ - d: DirectorSetHeadersByIP{ - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678:abcd::"), Mask: net.CIDRMask(80, 128)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.88.0"), Mask: net.CIDRMask(24, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.0.0.0"), Mask: net.CIDRMask(8, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678::"), Mask: net.CIDRMask(64, 128)}}, - }, - }, - want: DirectorSetHeadersByIP{ - {IPNet: net.IPNet{IP: net.ParseIP("192.0.0.0"), Mask: net.CIDRMask(8, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.0.0"), Mask: net.CIDRMask(16, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("192.168.88.0"), Mask: net.CIDRMask(24, 32)}}, - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678::"), Mask: net.CIDRMask(64, 128)}}, - {IPNet: net.IPNet{IP: net.ParseIP("2001:db8:1234:5678:abcd::"), Mask: net.CIDRMask(80, 128)}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sortByIPNet(tt.args.d) - if !td.CmpDeeply(tt.args.d, tt.want) { - t.Errorf("sortByIPNet() = %v, want %v", tt.args.d, tt.want) + if len(tt.args.m) > 0 { + t.Fatalf("not all networks found, %v", tt.args.m) } }) } diff --git a/vendor/github.com/egorgasay/cidranger/Gopkg.lock b/vendor/github.com/egorgasay/cidranger/Gopkg.lock new file mode 100644 index 0000000..507a497 --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/Gopkg.lock @@ -0,0 +1,33 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" + name = "github.com/davecgh/go-spew" + packages = ["spew"] + pruneopts = "UT" + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + pruneopts = "UT" + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + digest = "1:f85e109eda8f6080877185d1c39e98dd8795e1780c08beca28304b87fd855a1c" + name = "github.com/stretchr/testify" + packages = ["assert"] + pruneopts = "UT" + revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" + version = "v1.2.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + input-imports = ["github.com/stretchr/testify/assert"] + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/github.com/egorgasay/cidranger/Gopkg.toml b/vendor/github.com/egorgasay/cidranger/Gopkg.toml new file mode 100644 index 0000000..55dbd3b --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/Gopkg.toml @@ -0,0 +1,34 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + name = "github.com/stretchr/testify" + version = "1.2.1" + +[prune] + go-tests = true + unused-packages = true diff --git a/vendor/github.com/egorgasay/cidranger/LICENSE b/vendor/github.com/egorgasay/cidranger/LICENSE new file mode 100644 index 0000000..c41c622 --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Yulin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/egorgasay/cidranger/README.md b/vendor/github.com/egorgasay/cidranger/README.md new file mode 100644 index 0000000..b2de0a1 --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/README.md @@ -0,0 +1,107 @@ +# cidranger +Fast IP to CIDR block(s) lookup using trie in Golang, inspired by [IPv4 route lookup linux](https://vincent.bernat.im/en/blog/2017-ipv4-route-lookup-linux). Possible use cases include detecting if a IP address is from published cloud provider CIDR blocks (e.g. 52.95.110.1 is contained in published AWS Route53 CIDR 52.95.110.0/24), IP routing rules, etc. + +[![GoDoc Reference](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/yl2chen/cidranger) +[![Build Status](https://img.shields.io/travis/yl2chen/cidranger.svg?branch=master&style=flat-square)](https://travis-ci.org/yl2chen/cidranger) +[![Coverage Status](https://img.shields.io/coveralls/yl2chen/cidranger.svg?branch=master&style=flat-square)](https://coveralls.io/github/yl2chen/cidranger?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/yl2chen/cidranger?&style=flat-square)](https://goreportcard.com/report/github.com/yl2chen/cidranger) + +This is visualization of a trie storing CIDR blocks `128.0.0.0/2` `192.0.0.0/2` `200.0.0.0/5` without path compression, the 0/1 number on the path indicates the bit value of the IP address at specified bit position, hence the path from root node to a child node represents a CIDR block that contains all IP ranges of its children, and children's children. +
+ +Visualization of trie storing same CIDR blocks with path compression, improving both lookup speed and memory footprint. + + +## Getting Started +Configure imports. +```go +import ( + "net" + + "github.com/yl2chen/cidranger" +) +``` +Create a new ranger implemented using Path-Compressed prefix trie. +```go +ranger := NewPCTrieRanger() +``` +Inserts CIDR blocks. +```go +_, network1, _ := net.ParseCIDR("192.168.1.0/24") +_, network2, _ := net.ParseCIDR("128.168.1.0/24") +ranger.Insert(NewBasicRangerEntry(*network1)) +ranger.Insert(NewBasicRangerEntry(*network2)) +``` +To attach any additional value(s) to the entry, simply create custom struct +storing the desired value(s) that implements the RangerEntry interface: +```go +type RangerEntry interface { + Network() net.IPNet +} +``` +The prefix trie can be visualized as: +``` +0.0.0.0/0 (target_pos:31:has_entry:false) +| 1--> 128.0.0.0/1 (target_pos:30:has_entry:false) +| | 0--> 128.168.1.0/24 (target_pos:7:has_entry:true) +| | 1--> 192.168.1.0/24 (target_pos:7:has_entry:true) +``` +To test if given IP is contained in constructed ranger, +```go +contains, err = ranger.Contains(net.ParseIP("128.168.1.0")) // returns true, nil +contains, err = ranger.Contains(net.ParseIP("192.168.2.0")) // returns false, nil +``` +To get all the networks given is contained in, +```go +containingNetworks, err = ranger.ContainingNetworks(net.ParseIP("128.168.1.0")) +``` +To get all networks in ranger, +```go +entries, err := ranger.CoveredNetworks(*AllIPv4) // for IPv4 +entries, err := ranger.CoveredNetworks(*AllIPv6) // for IPv6 +``` + +## Benchmark +Compare hit/miss case for IPv4/IPv6 using PC trie vs brute force implementation, Ranger is initialized with published AWS ip ranges (889 IPv4 CIDR blocks and 360 IPv6) +```go +// Ipv4 lookup hit scenario +BenchmarkPCTrieHitIPv4UsingAWSRanges-4 5000000 353 ns/op +BenchmarkBruteRangerHitIPv4UsingAWSRanges-4 100000 13719 ns/op + +// Ipv6 lookup hit scenario, counter-intuitively faster then IPv4 due to less IPv6 CIDR +// blocks in the AWS dataset, hence the constructed trie has less path splits and depth. +BenchmarkPCTrieHitIPv6UsingAWSRanges-4 10000000 143 ns/op +BenchmarkBruteRangerHitIPv6UsingAWSRanges-4 300000 5178 ns/op + +// Ipv4 lookup miss scenario +BenchmarkPCTrieMissIPv4UsingAWSRanges-4 20000000 96.5 ns/op +BenchmarkBruteRangerMissIPv4UsingAWSRanges-4 50000 24781 ns/op + +// Ipv6 lookup miss scenario +BenchmarkPCTrieHMissIPv6UsingAWSRanges-4 10000000 115 ns/op +BenchmarkBruteRangerMissIPv6UsingAWSRanges-4 100000 10824 ns/op +``` + +## Example of IPv6 trie: +``` +::/0 (target_pos:127:has_entry:false) +| 0--> 2400::/14 (target_pos:113:has_entry:false) +| | 0--> 2400:6400::/22 (target_pos:105:has_entry:false) +| | | 0--> 2400:6500::/32 (target_pos:95:has_entry:false) +| | | | 0--> 2400:6500::/39 (target_pos:88:has_entry:false) +| | | | | 0--> 2400:6500:0:7000::/53 (target_pos:74:has_entry:false) +| | | | | | 0--> 2400:6500:0:7000::/54 (target_pos:73:has_entry:false) +| | | | | | | 0--> 2400:6500:0:7000::/55 (target_pos:72:has_entry:false) +| | | | | | | | 0--> 2400:6500:0:7000::/56 (target_pos:71:has_entry:true) +| | | | | | | | 1--> 2400:6500:0:7100::/56 (target_pos:71:has_entry:true) +| | | | | | | 1--> 2400:6500:0:7200::/56 (target_pos:71:has_entry:true) +| | | | | | 1--> 2400:6500:0:7400::/55 (target_pos:72:has_entry:false) +| | | | | | | 0--> 2400:6500:0:7400::/56 (target_pos:71:has_entry:true) +| | | | | | | 1--> 2400:6500:0:7500::/56 (target_pos:71:has_entry:true) +| | | | | 1--> 2400:6500:100:7000::/54 (target_pos:73:has_entry:false) +| | | | | | 0--> 2400:6500:100:7100::/56 (target_pos:71:has_entry:true) +| | | | | | 1--> 2400:6500:100:7200::/56 (target_pos:71:has_entry:true) +| | | | 1--> 2400:6500:ff00::/64 (target_pos:63:has_entry:true) +| | | 1--> 2400:6700:ff00::/64 (target_pos:63:has_entry:true) +| | 1--> 2403:b300:ff00::/64 (target_pos:63:has_entry:true) +``` diff --git a/vendor/github.com/egorgasay/cidranger/brute.go b/vendor/github.com/egorgasay/cidranger/brute.go new file mode 100644 index 0000000..dde2010 --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/brute.go @@ -0,0 +1,130 @@ +package cidranger + +import ( + "net" + + rnet "github.com/yl2chen/cidranger/net" +) + +// bruteRanger is a brute force implementation of Ranger. Insertion and +// deletion of networks is performed on an internal storage in the form of +// map[string]net.IPNet (constant time operations). However, inclusion tests are +// always performed linearly at no guaranteed traversal order of recorded networks, +// so one can assume a worst case performance of O(N). The performance can be +// boosted many ways, e.g. changing usage of net.IPNet.Contains() to using masked +// bits equality checking, but the main purpose of this implementation is for +// testing because the correctness of this implementation can be easily guaranteed, +// and used as the ground truth when running a wider range of 'random' tests on +// other more sophisticated implementations. +type bruteRanger[V any] struct { + ipV4Entries map[string]RangerEntry + ipV6Entries map[string]RangerEntry +} + +// newBruteRanger returns a new Ranger. +func newBruteRanger[V any]() Ranger[V] { + return &bruteRanger[V]{ + ipV4Entries: make(map[string]RangerEntry), + ipV6Entries: make(map[string]RangerEntry), + } +} + +// Insert inserts a RangerEntry into ranger. +func (b *bruteRanger[V]) Insert(entry RangerEntry, value ...V) error { + network := entry.Network() + key := network.String() + if _, found := b.ipV4Entries[key]; !found { + entries, err := b.getEntriesByVersion(entry.Network().IP) + if err != nil { + return err + } + entries[key] = entry + } + return nil +} + +// Remove removes a RangerEntry identified by given network from ranger. +func (b *bruteRanger[V]) Remove(network net.IPNet) (RangerEntry, error) { + networks, err := b.getEntriesByVersion(network.IP) + if err != nil { + return nil, err + } + key := network.String() + if networkToDelete, found := networks[key]; found { + delete(networks, key) + return networkToDelete, nil + } + return nil, nil +} + +// Contains returns bool indicating whether given ip is contained by any +// network in ranger. +func (b *bruteRanger[V]) Contains(ip net.IP) (bool, error) { + entries, err := b.getEntriesByVersion(ip) + if err != nil { + return false, err + } + for _, entry := range entries { + network := entry.Network() + if network.Contains(ip) { + return true, nil + } + } + return false, nil +} + +// ContainingNetworks returns all RangerEntry(s) that given ip contained in. +func (b *bruteRanger[V]) ContainingNetworks(ip net.IP) ([]RangerEntry, error) { + entries, err := b.getEntriesByVersion(ip) + if err != nil { + return nil, err + } + results := []RangerEntry{} + for _, entry := range entries { + network := entry.Network() + if network.Contains(ip) { + results = append(results, entry) + } + } + return results, nil +} + +func (b *bruteRanger[V]) IterByIncomingNetworks(ip net.IP, fn func(network net.IPNet, value V) error) error { + // does not need to be implemented + // this Ranger is used for testing only + panic("implement me") +} + +// CoveredNetworks returns the list of RangerEntry(s) the given ipnet +// covers. That is, the networks that are completely subsumed by the +// specified network. +func (b *bruteRanger[V]) CoveredNetworks(network net.IPNet) ([]RangerEntry, error) { + entries, err := b.getEntriesByVersion(network.IP) + if err != nil { + return nil, err + } + var results []RangerEntry + testNetwork := rnet.NewNetwork(network) + for _, entry := range entries { + entryNetwork := rnet.NewNetwork(entry.Network()) + if testNetwork.Covers(entryNetwork) { + results = append(results, entry) + } + } + return results, nil +} + +// Len returns number of networks in ranger. +func (b *bruteRanger[V]) Len() int { + return len(b.ipV4Entries) + len(b.ipV6Entries) +} + +func (b *bruteRanger[V]) getEntriesByVersion(ip net.IP) (map[string]RangerEntry, error) { + if ip.To4() != nil { + return b.ipV4Entries, nil + } + if ip.To16() != nil { + return b.ipV6Entries, nil + } + return nil, ErrInvalidNetworkInput +} diff --git a/vendor/github.com/egorgasay/cidranger/cidranger.go b/vendor/github.com/egorgasay/cidranger/cidranger.go new file mode 100644 index 0000000..22ae67f --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/cidranger.go @@ -0,0 +1,103 @@ +/* +Package cidranger provides utility to store CIDR blocks and perform ip +inclusion tests against it. + +To create a new instance of the path-compressed trie: + + ranger := NewPCTrieRanger() + +To insert or remove an entry (any object that satisfies the RangerEntry +interface): + + _, network, _ := net.ParseCIDR("192.168.0.0/24") + ranger.Insert(NewBasicRangerEntry(*network)) + ranger.Remove(network) + +If you desire for any value to be attached to the entry, simply +create custom struct that satisfies the RangerEntry interface: + + type RangerEntry interface { + Network() net.IPNet + } + +To test whether an IP is contained in the constructed networks ranger: + + // returns bool, error + containsBool, err := ranger.Contains(net.ParseIP("192.168.0.1")) + +To get a list of CIDR blocks in constructed ranger that contains IP: + + // returns []RangerEntry, error + entries, err := ranger.ContainingNetworks(net.ParseIP("192.168.0.1")) + +To get a list of all IPv4/IPv6 rangers respectively: + + // returns []RangerEntry, error + entries, err := ranger.CoveredNetworks(*AllIPv4) + entries, err := ranger.CoveredNetworks(*AllIPv6) +*/ +package cidranger + +import ( + "fmt" + "net" +) + +// ErrInvalidNetworkInput is returned upon invalid network input. +var ErrInvalidNetworkInput = fmt.Errorf("Invalid network input") + +// ErrInvalidNetworkNumberInput is returned upon invalid network input. +var ErrInvalidNetworkNumberInput = fmt.Errorf("Invalid network number input") + +// AllIPv4 is a IPv4 CIDR that contains all networks +var AllIPv4 = parseCIDRUnsafe("0.0.0.0/0") + +// AllIPv6 is a IPv6 CIDR that contains all networks +var AllIPv6 = parseCIDRUnsafe("0::0/0") + +func parseCIDRUnsafe(s string) *net.IPNet { + _, cidr, _ := net.ParseCIDR(s) + return cidr +} + +// RangerEntry is an interface for insertable entry into a Ranger. +type RangerEntry interface { + Network() net.IPNet +} + +type basicRangerEntry struct { + ipNet net.IPNet +} + +func (b *basicRangerEntry) Network() net.IPNet { + return b.ipNet +} + +// NewBasicRangerEntry returns a basic RangerEntry that only stores the network +// itself. +func NewBasicRangerEntry(ipNet net.IPNet) RangerEntry { + return &basicRangerEntry{ + ipNet: ipNet, + } +} + +// Ranger is an interface for cidr block containment lookups. +type Ranger[V any] interface { + Insert(entry RangerEntry, value ...V) error + Remove(network net.IPNet) (RangerEntry, error) + Contains(ip net.IP) (bool, error) + ContainingNetworks(ip net.IP) ([]RangerEntry, error) + CoveredNetworks(network net.IPNet) ([]RangerEntry, error) + Len() int + IterByIncomingNetworks(ip net.IP, fn func(network net.IPNet, value V) error) error +} + +// NewPCTrieRanger returns a versionedRanger that supports both IPv4 and IPv6 +// using the path compressed trie implemention. +func NewPCTrieRanger[V any](defaultValue ...V) Ranger[V] { + var val V + if len(defaultValue) > 0 { + val = defaultValue[0] + } + return NewVersionedRanger[V](newPrefixTree[V], val) +} diff --git a/vendor/github.com/egorgasay/cidranger/trie.go b/vendor/github.com/egorgasay/cidranger/trie.go new file mode 100644 index 0000000..f4bc093 --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/trie.go @@ -0,0 +1,447 @@ +package cidranger + +import ( + "fmt" + "net" + "strings" + + rnet "github.com/yl2chen/cidranger/net" +) + +// prefixTrie is a path-compressed (PC) trie implementation of the +// ranger interface inspired by this blog post: +// https://vincent.bernat.im/en/blog/2017-ipv4-route-lookup-linux +// +// CIDR blocks are stored using a prefix tree structure where each node has its +// parent as prefix, and the path from the root node represents current CIDR +// block. +// +// For IPv4, the trie structure guarantees max depth of 32 as IPv4 addresses are +// 32 bits long and each bit represents a prefix tree starting at that bit. This +// property also guarantees constant lookup time in Big-O notation. +// +// Path compression compresses a string of node with only 1 child into a single +// node, decrease the amount of lookups necessary during containment tests. +// +// Level compression dictates the amount of direct children of a node by +// allowing it to handle multiple bits in the path. The heuristic (based on +// children population) to decide when the compression and decompression happens +// is outlined in the prior linked blog, and will be experimented with in more +// depth in this project in the future. +// +// Note: Can not insert both IPv4 and IPv6 network addresses into the same +// prefix trie, use versionedRanger wrapper instead. +// +// TODO: Implement level-compressed component of the LPC trie. +type prefixTrie[V any] struct { + parent *prefixTrie[V] + children []*prefixTrie[V] + + numBitsSkipped uint + numBitsHandled uint + + network rnet.Network + entry RangerEntry + value V + + size int // This is only maintained in the root trie. +} + +// newPrefixTree creates a new prefixTrie. +func newPrefixTree[V any](version rnet.IPVersion, defaultValue ...V) Ranger[V] { + _, rootNet, _ := net.ParseCIDR("0.0.0.0/0") + if version == rnet.IPv6 { + _, rootNet, _ = net.ParseCIDR("0::0/0") + } + + var value V + if len(defaultValue) > 0 { + value = defaultValue[0] + } + return &prefixTrie[V]{ + children: make([]*prefixTrie[V], 2, 2), + numBitsSkipped: 0, + numBitsHandled: 1, + network: rnet.NewNetwork(*rootNet), + value: value, + } +} + +func newPathprefixTrie[V any](network rnet.Network, numBitsSkipped uint, value V) *prefixTrie[V] { + path := &prefixTrie[V]{ + children: make([]*prefixTrie[V], 2, 2), + numBitsSkipped: numBitsSkipped, + numBitsHandled: 1, + network: network.Masked(int(numBitsSkipped)), + value: value, + } + return path +} + +func newEntryTrie[V any](network rnet.Network, entry RangerEntry, value V) *prefixTrie[V] { + ones, _ := network.IPNet.Mask.Size() + leaf := newPathprefixTrie(network, uint(ones), value) + leaf.entry = entry + return leaf +} + +// Insert inserts a RangerEntry into prefix trie. +func (p *prefixTrie[V]) Insert(entry RangerEntry, value ...V) error { + network := entry.Network() + var val V + if len(value) > 0 { + val = value[0] + } + sizeIncreased, err := p.insert(rnet.NewNetwork(network), entry, val) + if sizeIncreased { + p.size++ + } + return err +} + +// Remove removes RangerEntry identified by given network from trie. +func (p *prefixTrie[V]) Remove(network net.IPNet) (RangerEntry, error) { + entry, err := p.remove(rnet.NewNetwork(network)) + if entry != nil { + p.size-- + } + return entry, err +} + +// Contains returns boolean indicating whether given ip is contained in any +// of the inserted networks. +func (p *prefixTrie[V]) Contains(ip net.IP) (bool, error) { + nn := rnet.NewNetworkNumber(ip) + if nn == nil { + return false, ErrInvalidNetworkNumberInput + } + return p.contains(nn) +} + +// ContainingNetworks returns the list of RangerEntry(s) the given ip is +// contained in in ascending prefix order. +func (p *prefixTrie[V]) ContainingNetworks(ip net.IP) ([]RangerEntry, error) { + nn := rnet.NewNetworkNumber(ip) + if nn == nil { + return nil, ErrInvalidNetworkNumberInput + } + return p.containingNetworks(nn) +} + +// IterByIncomingNetworks iterates over all networks that the transmitted IP is included in. +func (p *prefixTrie[V]) IterByIncomingNetworks(ip net.IP, f func(network net.IPNet, value V) error) error { + if err := f(p.network.IPNet, p.value); err != nil { + return err + } + nn := rnet.NewNetworkNumber(ip) + if nn == nil { + return ErrInvalidNetworkNumberInput + } + return p.iterByIncomingNetworks(nn, f) +} + +// CoveredNetworks returns the list of RangerEntry(s) the given ipnet +// covers. That is, the networks that are completely subsumed by the +// specified network. +func (p *prefixTrie[V]) CoveredNetworks(network net.IPNet) ([]RangerEntry, error) { + net := rnet.NewNetwork(network) + return p.coveredNetworks(net) +} + +// Len returns number of networks in ranger. +func (p *prefixTrie[V]) Len() int { + return p.size +} + +// String returns string representation of trie, mainly for visualization and +// debugging. +func (p *prefixTrie[V]) String() string { + children := []string{} + padding := strings.Repeat("| ", p.level()+1) + for bits, child := range p.children { + if child == nil { + continue + } + childStr := fmt.Sprintf("\n%s%d--> %s", padding, bits, child.String()) + children = append(children, childStr) + } + return fmt.Sprintf("%s (target_pos:%d:has_entry:%t)%s", p.network, + p.targetBitPosition(), p.hasEntry(), strings.Join(children, "")) +} + +func (p *prefixTrie[V]) contains(number rnet.NetworkNumber) (bool, error) { + if !p.network.Contains(number) { + return false, nil + } + if p.hasEntry() { + return true, nil + } + if p.targetBitPosition() < 0 { + return false, nil + } + bit, err := p.targetBitFromIP(number) + if err != nil { + return false, err + } + child := p.children[bit] + if child != nil { + return child.contains(number) + } + return false, nil +} + +func (p *prefixTrie[V]) containingNetworks(number rnet.NetworkNumber) ([]RangerEntry, error) { + results := []RangerEntry{} + if !p.network.Contains(number) { + return results, nil + } + if p.hasEntry() { + results = []RangerEntry{p.entry} + } + if p.targetBitPosition() < 0 { + return results, nil + } + bit, err := p.targetBitFromIP(number) + if err != nil { + return nil, err + } + child := p.children[bit] + if child != nil { + ranges, err := child.containingNetworks(number) + if err != nil { + return nil, err + } + if len(ranges) > 0 { + if len(results) > 0 { + results = append(results, ranges...) + } else { + results = ranges + } + } + } + return results, nil +} + +func (p *prefixTrie[V]) iterByIncomingNetworks(number rnet.NetworkNumber, + f func(network net.IPNet, value V) error) error { + if !p.network.Contains(number) { + return nil + } + + if p.hasEntry() { + if err := f(p.network.IPNet, p.value); err != nil { + return err + } + } + if p.targetBitPosition() < 0 { + return nil + } + bit, err := p.targetBitFromIP(number) + if err != nil { + return err + } + child := p.children[bit] + if child != nil { + err = child.iterByIncomingNetworks(number, f) + if err != nil { + return err + } + } + return nil +} + +func (p *prefixTrie[V]) coveredNetworks(network rnet.Network) ([]RangerEntry, error) { + var results []RangerEntry + if network.Covers(p.network) { + for entry := range p.walkDepth() { + results = append(results, entry) + } + } else if p.targetBitPosition() >= 0 { + bit, err := p.targetBitFromIP(network.Number) + if err != nil { + return results, err + } + child := p.children[bit] + if child != nil { + return child.coveredNetworks(network) + } + } + return results, nil +} + +func (p *prefixTrie[V]) insert(network rnet.Network, entry RangerEntry, value V) (bool, error) { + if p.network.Equal(network) { + sizeIncreased := p.entry == nil + p.entry = entry + return sizeIncreased, nil + } + + bit, err := p.targetBitFromIP(network.Number) + if err != nil { + return false, err + } + existingChild := p.children[bit] + + // No existing child, insert new leaf trie. + if existingChild == nil { + p.appendTrie(bit, newEntryTrie(network, entry, value)) + return true, nil + } + + // Check whether it is necessary to insert additional path prefix between current trie and existing child, + // in the case that inserted network diverges on its path to existing child. + lcb, err := network.LeastCommonBitPosition(existingChild.network) + divergingBitPos := int(lcb) - 1 + if divergingBitPos > existingChild.targetBitPosition() { + pathPrefix := newPathprefixTrie(network, p.totalNumberOfBits()-lcb, value) + err := p.insertPrefix(bit, pathPrefix, existingChild) + if err != nil { + return false, err + } + // Update new child + existingChild = pathPrefix + } + return existingChild.insert(network, entry, value) +} + +func (p *prefixTrie[V]) appendTrie(bit uint32, prefix *prefixTrie[V]) { + p.children[bit] = prefix + prefix.parent = p +} + +func (p *prefixTrie[V]) insertPrefix(bit uint32, pathPrefix, child *prefixTrie[V]) error { + // Set parent/child relationship between current trie and inserted pathPrefix + p.children[bit] = pathPrefix + pathPrefix.parent = p + + // Set parent/child relationship between inserted pathPrefix and original child + pathPrefixBit, err := pathPrefix.targetBitFromIP(child.network.Number) + if err != nil { + return err + } + pathPrefix.children[pathPrefixBit] = child + child.parent = pathPrefix + return nil +} + +func (p *prefixTrie[V]) remove(network rnet.Network) (RangerEntry, error) { + if p.hasEntry() && p.network.Equal(network) { + entry := p.entry + p.entry = nil + + err := p.compressPathIfPossible() + if err != nil { + return nil, err + } + return entry, nil + } + if p.targetBitPosition() < 0 { + return nil, nil + } + bit, err := p.targetBitFromIP(network.Number) + if err != nil { + return nil, err + } + child := p.children[bit] + if child != nil { + return child.remove(network) + } + return nil, nil +} + +func (p *prefixTrie[V]) qualifiesForPathCompression() bool { + // Current prefix trie can be path compressed if it meets all following. + // 1. records no CIDR entry + // 2. has single or no child + // 3. is not root trie + return !p.hasEntry() && p.childrenCount() <= 1 && p.parent != nil +} + +func (p *prefixTrie[V]) compressPathIfPossible() error { + if !p.qualifiesForPathCompression() { + // Does not qualify to be compressed + return nil + } + + // Find lone child. + var loneChild *prefixTrie[V] + for _, child := range p.children { + if child != nil { + loneChild = child + break + } + } + + // Find root of currnt single child lineage. + parent := p.parent + for ; parent.qualifiesForPathCompression(); parent = parent.parent { + } + parentBit, err := parent.targetBitFromIP(p.network.Number) + if err != nil { + return err + } + parent.children[parentBit] = loneChild + + // Attempts to furthur apply path compression at current lineage parent, in case current lineage + // compressed into parent. + return parent.compressPathIfPossible() +} + +func (p *prefixTrie[V]) childrenCount() int { + count := 0 + for _, child := range p.children { + if child != nil { + count++ + } + } + return count +} + +func (p *prefixTrie[V]) totalNumberOfBits() uint { + return rnet.BitsPerUint32 * uint(len(p.network.Number)) +} + +func (p *prefixTrie[V]) targetBitPosition() int { + return int(p.totalNumberOfBits()-p.numBitsSkipped) - 1 +} + +func (p *prefixTrie[V]) targetBitFromIP(n rnet.NetworkNumber) (uint32, error) { + // This is a safe uint boxing of int since we should never attempt to get + // target bit at a negative position. + return n.Bit(uint(p.targetBitPosition())) +} + +func (p *prefixTrie[V]) hasEntry() bool { + return p.entry != nil +} + +func (p *prefixTrie[V]) level() int { + if p.parent == nil { + return 0 + } + return p.parent.level() + 1 +} + +// walkDepth walks the trie in depth order, for unit testing. +func (p *prefixTrie[V]) walkDepth() <-chan RangerEntry { + entries := make(chan RangerEntry) + go func() { + if p.hasEntry() { + entries <- p.entry + } + childEntriesList := []<-chan RangerEntry{} + for _, trie := range p.children { + if trie == nil { + continue + } + childEntriesList = append(childEntriesList, trie.walkDepth()) + } + for _, childEntries := range childEntriesList { + for entry := range childEntries { + entries <- entry + } + } + close(entries) + }() + return entries +} diff --git a/vendor/github.com/egorgasay/cidranger/version.go b/vendor/github.com/egorgasay/cidranger/version.go new file mode 100644 index 0000000..99595f5 --- /dev/null +++ b/vendor/github.com/egorgasay/cidranger/version.go @@ -0,0 +1,90 @@ +package cidranger + +import ( + "net" + + rnet "github.com/yl2chen/cidranger/net" +) + +type rangerFactory[V any] func(v rnet.IPVersion, value ...V) Ranger[V] + +type versionedRanger[V any] struct { + ipV4Ranger Ranger[V] + ipV6Ranger Ranger[V] +} + +func NewVersionedRanger[V any](factory rangerFactory[V], defaultValue V) Ranger[V] { + return &versionedRanger[V]{ + ipV4Ranger: factory(rnet.IPv4, defaultValue), + ipV6Ranger: factory(rnet.IPv6, defaultValue), + } +} + +func (v *versionedRanger[V]) Insert(entry RangerEntry, value ...V) error { + var val V + if len(value) > 0 { + val = value[0] + } + network := entry.Network() + ranger, err := v.getRangerForIP(network.IP) + if err != nil { + return err + } + return ranger.Insert(entry, val) +} + +func (v *versionedRanger[V]) Remove(network net.IPNet) (RangerEntry, error) { + ranger, err := v.getRangerForIP(network.IP) + if err != nil { + return nil, err + } + return ranger.Remove(network) +} + +func (v *versionedRanger[V]) Contains(ip net.IP) (bool, error) { + ranger, err := v.getRangerForIP(ip) + if err != nil { + return false, err + } + return ranger.Contains(ip) +} + +func (v *versionedRanger[V]) ContainingNetworks(ip net.IP) ([]RangerEntry, error) { + ranger, err := v.getRangerForIP(ip) + if err != nil { + return nil, err + } + return ranger.ContainingNetworks(ip) +} + +func (v *versionedRanger[V]) IterByIncomingNetworks(ip net.IP, fn func(network net.IPNet, value V) error) error { + ranger, err := v.getRangerForIP(ip) + if err != nil { + return err + } + + return ranger.IterByIncomingNetworks(ip, fn) +} + +func (v *versionedRanger[V]) CoveredNetworks(network net.IPNet) ([]RangerEntry, error) { + ranger, err := v.getRangerForIP(network.IP) + if err != nil { + return nil, err + } + return ranger.CoveredNetworks(network) +} + +// Len returns number of networks in ranger. +func (v *versionedRanger[V]) Len() int { + return v.ipV4Ranger.Len() + v.ipV6Ranger.Len() +} + +func (v *versionedRanger[V]) getRangerForIP(ip net.IP) (Ranger[V], error) { + if ip.To4() != nil { + return v.ipV4Ranger, nil + } + if ip.To16() != nil { + return v.ipV6Ranger, nil + } + return nil, ErrInvalidNetworkNumberInput +} diff --git a/vendor/github.com/yl2chen/cidranger/LICENSE b/vendor/github.com/yl2chen/cidranger/LICENSE new file mode 100644 index 0000000..c41c622 --- /dev/null +++ b/vendor/github.com/yl2chen/cidranger/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Yulin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/yl2chen/cidranger/net/ip.go b/vendor/github.com/yl2chen/cidranger/net/ip.go new file mode 100644 index 0000000..1ce30e8 --- /dev/null +++ b/vendor/github.com/yl2chen/cidranger/net/ip.go @@ -0,0 +1,272 @@ +/* +Package net provides utility functions for working with IPs (net.IP). +*/ +package net + +import ( + "encoding/binary" + "fmt" + "math" + "net" +) + +// IPVersion is version of IP address. +type IPVersion string + +// Helper constants. +const ( + IPv4Uint32Count = 1 + IPv6Uint32Count = 4 + + BitsPerUint32 = 32 + BytePerUint32 = 4 + + IPv4 IPVersion = "IPv4" + IPv6 IPVersion = "IPv6" +) + +// ErrInvalidBitPosition is returned when bits requested is not valid. +var ErrInvalidBitPosition = fmt.Errorf("bit position not valid") + +// ErrVersionMismatch is returned upon mismatch in network input versions. +var ErrVersionMismatch = fmt.Errorf("Network input version mismatch") + +// ErrNoGreatestCommonBit is an error returned when no greatest common bit +// exists for the cidr ranges. +var ErrNoGreatestCommonBit = fmt.Errorf("No greatest common bit") + +// NetworkNumber represents an IP address using uint32 as internal storage. +// IPv4 usings 1 uint32, while IPv6 uses 4 uint32. +type NetworkNumber []uint32 + +// NewNetworkNumber returns a equivalent NetworkNumber to given IP address, +// return nil if ip is neither IPv4 nor IPv6. +func NewNetworkNumber(ip net.IP) NetworkNumber { + if ip == nil { + return nil + } + coercedIP := ip.To4() + parts := 1 + if coercedIP == nil { + coercedIP = ip.To16() + parts = 4 + } + if coercedIP == nil { + return nil + } + nn := make(NetworkNumber, parts) + for i := 0; i < parts; i++ { + idx := i * net.IPv4len + nn[i] = binary.BigEndian.Uint32(coercedIP[idx : idx+net.IPv4len]) + } + return nn +} + +// ToV4 returns ip address if ip is IPv4, returns nil otherwise. +func (n NetworkNumber) ToV4() NetworkNumber { + if len(n) != IPv4Uint32Count { + return nil + } + return n +} + +// ToV6 returns ip address if ip is IPv6, returns nil otherwise. +func (n NetworkNumber) ToV6() NetworkNumber { + if len(n) != IPv6Uint32Count { + return nil + } + return n +} + +// ToIP returns equivalent net.IP. +func (n NetworkNumber) ToIP() net.IP { + ip := make(net.IP, len(n)*BytePerUint32) + for i := 0; i < len(n); i++ { + idx := i * net.IPv4len + binary.BigEndian.PutUint32(ip[idx:idx+net.IPv4len], n[i]) + } + if len(ip) == net.IPv4len { + ip = net.IPv4(ip[0], ip[1], ip[2], ip[3]) + } + return ip +} + +// Equal is the equality test for 2 network numbers. +func (n NetworkNumber) Equal(n1 NetworkNumber) bool { + if len(n) != len(n1) { + return false + } + if n[0] != n1[0] { + return false + } + if len(n) == IPv6Uint32Count { + return n[1] == n1[1] && n[2] == n1[2] && n[3] == n1[3] + } + return true +} + +// Next returns the next logical network number. +func (n NetworkNumber) Next() NetworkNumber { + newIP := make(NetworkNumber, len(n)) + copy(newIP, n) + for i := len(newIP) - 1; i >= 0; i-- { + newIP[i]++ + if newIP[i] > 0 { + break + } + } + return newIP +} + +// Previous returns the previous logical network number. +func (n NetworkNumber) Previous() NetworkNumber { + newIP := make(NetworkNumber, len(n)) + copy(newIP, n) + for i := len(newIP) - 1; i >= 0; i-- { + newIP[i]-- + if newIP[i] < math.MaxUint32 { + break + } + } + return newIP +} + +// Bit returns uint32 representing the bit value at given position, e.g., +// "128.0.0.0" has bit value of 1 at position 31, and 0 for positions 30 to 0. +func (n NetworkNumber) Bit(position uint) (uint32, error) { + if int(position) > len(n)*BitsPerUint32-1 { + return 0, ErrInvalidBitPosition + } + idx := len(n) - 1 - int(position/BitsPerUint32) + // Mod 31 to get array index. + rShift := position & (BitsPerUint32 - 1) + return (n[idx] >> rShift) & 1, nil +} + +// LeastCommonBitPosition returns the smallest position of the preceding common +// bits of the 2 network numbers, and returns an error ErrNoGreatestCommonBit +// if the two network number diverges from the first bit. +// e.g., if the network number diverges after the 1st bit, it returns 131 for +// IPv6 and 31 for IPv4 . +func (n NetworkNumber) LeastCommonBitPosition(n1 NetworkNumber) (uint, error) { + if len(n) != len(n1) { + return 0, ErrVersionMismatch + } + for i := 0; i < len(n); i++ { + mask := uint32(1) << 31 + pos := uint(31) + for ; mask > 0; mask >>= 1 { + if n[i]&mask != n1[i]&mask { + if i == 0 && pos == 31 { + return 0, ErrNoGreatestCommonBit + } + return (pos + 1) + uint(BitsPerUint32)*uint(len(n)-i-1), nil + } + pos-- + } + } + return 0, nil +} + +// Network represents a block of network numbers, also known as CIDR. +type Network struct { + net.IPNet + Number NetworkNumber + Mask NetworkNumberMask +} + +// NewNetwork returns Network built using given net.IPNet. +func NewNetwork(ipNet net.IPNet) Network { + return Network{ + IPNet: ipNet, + Number: NewNetworkNumber(ipNet.IP), + Mask: NetworkNumberMask(NewNetworkNumber(net.IP(ipNet.Mask))), + } +} + +// Masked returns a new network conforming to new mask. +func (n Network) Masked(ones int) Network { + mask := net.CIDRMask(ones, len(n.Number)*BitsPerUint32) + return NewNetwork(net.IPNet{ + IP: n.IP.Mask(mask), + Mask: mask, + }) +} + +// Contains returns true if NetworkNumber is in range of Network, false +// otherwise. +func (n Network) Contains(nn NetworkNumber) bool { + if len(n.Mask) != len(nn) { + return false + } + if nn[0]&n.Mask[0] != n.Number[0] { + return false + } + if len(nn) == IPv6Uint32Count { + return nn[1]&n.Mask[1] == n.Number[1] && nn[2]&n.Mask[2] == n.Number[2] && nn[3]&n.Mask[3] == n.Number[3] + } + return true +} + +// Contains returns true if Network covers o, false otherwise +func (n Network) Covers(o Network) bool { + if len(n.Number) != len(o.Number) { + return false + } + nMaskSize, _ := n.IPNet.Mask.Size() + oMaskSize, _ := o.IPNet.Mask.Size() + return n.Contains(o.Number) && nMaskSize <= oMaskSize +} + +// LeastCommonBitPosition returns the smallest position of the preceding common +// bits of the 2 networks, and returns an error ErrNoGreatestCommonBit +// if the two network number diverges from the first bit. +func (n Network) LeastCommonBitPosition(n1 Network) (uint, error) { + maskSize, _ := n.IPNet.Mask.Size() + if maskSize1, _ := n1.IPNet.Mask.Size(); maskSize1 < maskSize { + maskSize = maskSize1 + } + maskPosition := len(n1.Number)*BitsPerUint32 - maskSize + lcb, err := n.Number.LeastCommonBitPosition(n1.Number) + if err != nil { + return 0, err + } + return uint(math.Max(float64(maskPosition), float64(lcb))), nil +} + +// Equal is the equality test for 2 networks. +func (n Network) Equal(n1 Network) bool { + return n.String() == n1.String() +} + +func (n Network) String() string { + return n.IPNet.String() +} + +// NetworkNumberMask is an IP address. +type NetworkNumberMask NetworkNumber + +// Mask returns a new masked NetworkNumber from given NetworkNumber. +func (m NetworkNumberMask) Mask(n NetworkNumber) (NetworkNumber, error) { + if len(m) != len(n) { + return nil, ErrVersionMismatch + } + result := make(NetworkNumber, len(m)) + result[0] = m[0] & n[0] + if len(m) == IPv6Uint32Count { + result[1] = m[1] & n[1] + result[2] = m[2] & n[2] + result[3] = m[3] & n[3] + } + return result, nil +} + +// NextIP returns the next sequential ip. +func NextIP(ip net.IP) net.IP { + return NewNetworkNumber(ip).Next().ToIP() +} + +// PreviousIP returns the previous sequential ip. +func PreviousIP(ip net.IP) net.IP { + return NewNetworkNumber(ip).Previous().ToIP() +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 73f36a1..cb9753a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -56,6 +56,9 @@ github.com/cespare/xxhash/v2 # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew +# github.com/egorgasay/cidranger v1.0.1 +## explicit; go 1.18 +github.com/egorgasay/cidranger # github.com/gojuno/minimock/v3 v3.0.5 ## explicit github.com/gojuno/minimock/v3 @@ -145,6 +148,9 @@ github.com/sirupsen/logrus # github.com/valyala/fastrand v1.1.0 ## explicit github.com/valyala/fastrand +# github.com/yl2chen/cidranger v1.0.2 +## explicit; go 1.13 +github.com/yl2chen/cidranger/net # go.uber.org/atomic v1.9.0 ## explicit; go 1.13 go.uber.org/atomic