From d3767c5e98c2ba04bca3e244c50bf84ceca53d1b Mon Sep 17 00:00:00 2001 From: Denis Tingaikin Date: Mon, 21 Mar 2022 03:03:57 +0300 Subject: [PATCH] add vl3 chain elements and ipam service Signed-off-by: Denis Tingaikin --- pkg/ipam/vl3ipam/server.go | 127 +++++++++++ pkg/ipam/vl3ipam/server_test.go | 191 ++++++++++++++++ pkg/networkservice/chains/client/options.go | 5 +- pkg/networkservice/common/cidr/cidr_test.go | 159 ------------- pkg/networkservice/common/cidr/client.go | 85 ------- pkg/networkservice/common/cidr/doc.go | 18 -- pkg/networkservice/common/cidr/metadata.go | 38 ---- pkg/networkservice/common/cidr/server.go | 134 ----------- .../connectioncontext/ipcontext/vl3/client.go | 147 ++++++++++++ .../ipcontext/vl3/client_test.go | 209 ++++++++++++++++++ .../connectioncontext/ipcontext/vl3/ipam.go | 143 ++++++++++++ .../ipcontext/vl3/metdata.go | 53 +++++ .../connectioncontext/ipcontext/vl3/server.go | 127 +++++++++++ .../ipcontext/vl3/server_test.go | 175 +++++++++++++++ pkg/tools/ippool/ippool.go | 39 +++- pkg/tools/ippool/ippool_test.go | 66 +++++- pkg/tools/ippool/tools.go | 4 +- pkg/tools/ippool/types.go | 6 +- 18 files changed, 1285 insertions(+), 441 deletions(-) create mode 100644 pkg/ipam/vl3ipam/server.go create mode 100644 pkg/ipam/vl3ipam/server_test.go delete mode 100644 pkg/networkservice/common/cidr/cidr_test.go delete mode 100644 pkg/networkservice/common/cidr/client.go delete mode 100644 pkg/networkservice/common/cidr/doc.go delete mode 100644 pkg/networkservice/common/cidr/metadata.go delete mode 100644 pkg/networkservice/common/cidr/server.go create mode 100644 pkg/networkservice/connectioncontext/ipcontext/vl3/client.go create mode 100644 pkg/networkservice/connectioncontext/ipcontext/vl3/client_test.go create mode 100644 pkg/networkservice/connectioncontext/ipcontext/vl3/ipam.go create mode 100644 pkg/networkservice/connectioncontext/ipcontext/vl3/metdata.go create mode 100644 pkg/networkservice/connectioncontext/ipcontext/vl3/server.go create mode 100644 pkg/networkservice/connectioncontext/ipcontext/vl3/server_test.go diff --git a/pkg/ipam/vl3ipam/server.go b/pkg/ipam/vl3ipam/server.go new file mode 100644 index 0000000000..d8bcb946b5 --- /dev/null +++ b/pkg/ipam/vl3ipam/server.go @@ -0,0 +1,127 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package vl3ipam provides implementation of api/pkg/api/ipam.IPAMServer for vL3 scenario. +package vl3ipam + +import ( + "net" + "sync" + + "github.com/networkservicemesh/api/pkg/api/ipam" + "github.com/pkg/errors" + + "github.com/networkservicemesh/sdk/pkg/tools/ippool" +) + +// ErrUndefined means that operation is not supported +var ErrUndefined = errors.New("request type is undefined") + +// ErrOutOfRange means that ip pool of IPAM is empty +var ErrOutOfRange = errors.New("prefix is out of range or already in use") + +type vl3IPAMServer struct { + pool *ippool.IPPool + excludedPrefixes []string + poolMutex sync.Mutex + initalSize uint8 +} + +// NewIPAMServer creates a new ipam.IPAMServer handler for grpc.Server +func NewIPAMServer(prefix string, initialNSEPrefixSize uint8) ipam.IPAMServer { + return &vl3IPAMServer{ + pool: ippool.NewWithNetString(prefix), + initalSize: initialNSEPrefixSize, + } +} + +var _ ipam.IPAMServer = (*vl3IPAMServer)(nil) + +func (s *vl3IPAMServer) ManagePrefixes(prefixServer ipam.IPAM_ManagePrefixesServer) error { + var pool = s.pool + var mutex = &s.poolMutex + var clientsPrefixes []string + var err error + + for err == nil { + var r *ipam.PrefixRequest + + r, err = prefixServer.Recv() + if err != nil { + break + } + + switch r.Type { + case ipam.Type_UNDEFINED: + return ErrUndefined + + case ipam.Type_ALLOCATE: + var resp ipam.PrefixResponse + mutex.Lock() + for _, excludePrefix := range r.ExcludePrefixes { + pool.ExcludeString(excludePrefix) + } + resp.Prefix = r.Prefix + if resp.Prefix == "" || !pool.ContainsNetString(resp.Prefix) { + var ip net.IP + ip, err = pool.Pull() + if err != nil { + mutex.Unlock() + break + } + ipNet := &net.IPNet{ + IP: ip, + Mask: net.CIDRMask( + int(s.initalSize), + len(ip)*8, + ), + } + resp.Prefix = ipNet.String() + } + s.excludedPrefixes = append(s.excludedPrefixes, r.Prefix) + clientsPrefixes = append(clientsPrefixes, resp.Prefix) + pool.ExcludeString(resp.Prefix) + mutex.Unlock() + resp.ExcludePrefixes = r.ExcludePrefixes + resp.ExcludePrefixes = append(resp.ExcludePrefixes, s.excludedPrefixes...) + err = prefixServer.Send(&resp) + + case ipam.Type_DELETE: + for i, p := range clientsPrefixes { + if p != r.Prefix { + continue + } + mutex.Lock() + pool.AddNetString(p) + mutex.Unlock() + clientsPrefixes = append(clientsPrefixes[:i], clientsPrefixes[i+1:]...) + break + } + } + } + + s.poolMutex.Lock() + for _, prefix := range clientsPrefixes { + pool.AddNetString(prefix) + } + s.poolMutex.Unlock() + + if prefixServer.Context().Err() != nil { + return nil + } + + return err +} diff --git a/pkg/ipam/vl3ipam/server_test.go b/pkg/ipam/vl3ipam/server_test.go new file mode 100644 index 0000000000..7c67b436c5 --- /dev/null +++ b/pkg/ipam/vl3ipam/server_test.go @@ -0,0 +1,191 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vl3ipam_test + +import ( + "context" + "fmt" + "net/url" + "testing" + "time" + + "github.com/networkservicemesh/api/pkg/api/ipam" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + "google.golang.org/grpc" + + "github.com/networkservicemesh/sdk/pkg/ipam/vl3ipam" + "github.com/networkservicemesh/sdk/pkg/tools/grpcutils" +) + +func newVL3IPAMServer(ctx context.Context, t *testing.T, prefix string, initialSize uint8) url.URL { + var s = grpc.NewServer() + ipam.RegisterIPAMServer(s, vl3ipam.NewIPAMServer(prefix, initialSize)) + + var serverAddr url.URL + + require.Len(t, grpcutils.ListenAndServe(ctx, &serverAddr, s), 0) + + return serverAddr +} + +func newVL3IPAMClient(ctx context.Context, t *testing.T, connectTO *url.URL) ipam.IPAMClient { + var cc, err = grpc.DialContext(ctx, grpcutils.URLToTarget(connectTO), grpc.WithInsecure()) + require.NoError(t, err) + + go func() { + <-ctx.Done() + _ = cc.Close() + }() + + return ipam.NewIPAMClient(cc) +} + +func Test_vl3_IPAM_Allocate(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + var ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + connectTO := newVL3IPAMServer(ctx, t, "172.16.0.0/16", 24) + + for i := 0; i < 10; i++ { + c := newVL3IPAMClient(ctx, t, &connectTO) + + var stream, err = c.ManagePrefixes(ctx) + + require.NoError(t, err, i) + + err = stream.Send(&ipam.PrefixRequest{ + Type: ipam.Type_ALLOCATE, + }) + + require.NoError(t, err) + + resp, err := stream.Recv() + require.NoError(t, err) + + require.Equal(t, fmt.Sprintf("172.16.%v.0/24", i), resp.Prefix, i) + require.NotEmpty(t, resp.ExcludePrefixes) + } +} + +func Test_vl3_IPAM_Allocat2(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + var ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + connectTO := newVL3IPAMServer(ctx, t, "173.16.0.0/16", 24) + + for i := 0; i < 10; i++ { + clientCTX, cancel := context.WithCancel(ctx) + c := newVL3IPAMClient(clientCTX, t, &connectTO) + + var stream, err = c.ManagePrefixes(clientCTX) + require.NoError(t, err, i) + + err = stream.Send(&ipam.PrefixRequest{ + Type: ipam.Type_ALLOCATE, + }) + + require.NoError(t, err) + + resp, err := stream.Recv() + require.NoError(t, err) + + require.Equal(t, "173.16.0.0/24", resp.Prefix, i) + require.NotEmpty(t, resp.ExcludePrefixes, i) + cancel() + time.Sleep(time.Millisecond * 50) + } +} + +func Test_vl3_IPAM_Allocat3(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + var ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + connectTO := newVL3IPAMServer(ctx, t, "172.16.0.0/16", 24) + + for i := 0; i < 10; i++ { + clientCTX, cancel := context.WithCancel(ctx) + c := newVL3IPAMClient(clientCTX, t, &connectTO) + + var stream, err = c.ManagePrefixes(clientCTX) + require.NoError(t, err, i) + + err = stream.Send(&ipam.PrefixRequest{ + Type: ipam.Type_ALLOCATE, + Prefix: "172.16.0.0/30", + }) + + require.NoError(t, err) + + resp, err := stream.Recv() + require.NoError(t, err) + + require.Equal(t, "172.16.0.0/30", resp.Prefix, i) + require.NotEmpty(t, resp.ExcludePrefixes, i) + cancel() + time.Sleep(time.Millisecond * 50) + } +} + +func Test_vl3_IPAM_Allocat4(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + var ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + connectTO := newVL3IPAMServer(ctx, t, "172.16.0.0/16", 40) + + for i := 0; i < 10; i++ { + c := newVL3IPAMClient(ctx, t, &connectTO) + + var stream, err = c.ManagePrefixes(ctx) + require.NoError(t, err, i) + + err = stream.Send(&ipam.PrefixRequest{ + Type: ipam.Type_ALLOCATE, + Prefix: "172.16.0.0/30", + }) + + require.NoError(t, err) + + resp, err := stream.Recv() + require.NoError(t, err) + + require.Equal(t, "172.16.0.0/30", resp.Prefix, i) + require.NotEmpty(t, resp.ExcludePrefixes, i) + + err = stream.Send(&ipam.PrefixRequest{ + Type: ipam.Type_DELETE, + Prefix: "172.16.0.0/30", + }) + + require.NoError(t, err) + } +} diff --git a/pkg/networkservice/chains/client/options.go b/pkg/networkservice/chains/client/options.go index 1bea592449..dc4ab9ce5e 100644 --- a/pkg/networkservice/chains/client/options.go +++ b/pkg/networkservice/chains/client/options.go @@ -23,17 +23,18 @@ import ( "github.com/networkservicemesh/api/pkg/api/networkservice" "google.golang.org/grpc" + "github.com/networkservicemesh/sdk/pkg/networkservice/common/clienturl" "github.com/networkservicemesh/sdk/pkg/networkservice/common/null" ) type clientOptions struct { name string - clientURL *url.URL cc grpc.ClientConnInterface additionalFunctionality []networkservice.NetworkServiceClient authorizeClient networkservice.NetworkServiceClient refreshClient networkservice.NetworkServiceClient healClient networkservice.NetworkServiceClient + clientUrlClient networkservice.NetworkServiceClient dialOptions []grpc.DialOption dialTimeout time.Duration } @@ -51,7 +52,7 @@ func WithName(name string) Option { // WithClientURL sets name for the client. func WithClientURL(clientURL *url.URL) Option { return Option(func(c *clientOptions) { - c.clientURL = clientURL + c.clientUrlClient = clienturl.NewClient(clientURL) }) } diff --git a/pkg/networkservice/common/cidr/cidr_test.go b/pkg/networkservice/common/cidr/cidr_test.go deleted file mode 100644 index a7c6f3b517..0000000000 --- a/pkg/networkservice/common/cidr/cidr_test.go +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at: -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cidr_test - -import ( - "context" - "testing" - - "github.com/networkservicemesh/api/pkg/api/networkservice" - - "github.com/networkservicemesh/sdk/pkg/networkservice/common/cidr" - "github.com/networkservicemesh/sdk/pkg/networkservice/common/updatepath" - "github.com/networkservicemesh/sdk/pkg/networkservice/core/adapters" - "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" - "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" - - "github.com/stretchr/testify/require" -) - -func newRequest() *networkservice.NetworkServiceRequest { - return &networkservice.NetworkServiceRequest{ - Connection: &networkservice.Connection{ - Context: &networkservice.ConnectionContext{ - IpContext: new(networkservice.IPContext), - }, - }, - } -} - -func newServer(prefixes, excludePrefixes []string) networkservice.NetworkServiceServer { - return next.NewNetworkServiceServer( - updatepath.NewServer("cidr-server"), - metadata.NewServer(), - cidr.NewServer(prefixes, excludePrefixes), - ) -} - -func newClient(prefixLen uint32, family networkservice.IpFamily_Family, server networkservice.NetworkServiceServer) networkservice.NetworkServiceClient { - return next.NewNetworkServiceClient( - updatepath.NewClient("cidr-client"), - metadata.NewClient(), - cidr.NewClient(prefixLen, family), - /* Server part */ - adapters.NewServerToClient( - server, - ), - ) -} - -func validateConn(t *testing.T, conn *networkservice.Connection, prefix, route string) { - require.Equal(t, conn.Context.IpContext.ExtraPrefixes[0], prefix) - require.Equal(t, conn.Context.IpContext.SrcRoutes, []*networkservice.Route{ - { - Prefix: route, - }, - }) -} - -func TestIPFamilies(t *testing.T) { - var samples = []struct { - name string - family networkservice.IpFamily_Family - prefixLen uint32 - prefix string - cidr0 string - cidr1 string - cidr2 string - }{ - { - name: "IPv4", - family: networkservice.IpFamily_IPV4, - prefix: "192.168.0.0/16", - prefixLen: 24, - cidr0: "192.168.0.0/24", - cidr1: "192.168.1.0/24", - cidr2: "192.168.2.0/24", - }, - { - name: "IPv6", - family: networkservice.IpFamily_IPV6, - prefix: "2001:db8::/96", - prefixLen: 112, - cidr0: "2001:db8::/112", - cidr1: "2001:db8::1:0/112", - cidr2: "2001:db8::2:0/112", - }, - } - - for _, sample := range samples { - t.Run(sample.name, func(t *testing.T) { - // nolint:scopelint - testIPFamilies(t, sample.family, sample.prefixLen, sample.prefix, sample.cidr0, sample.cidr1, sample.cidr2) - }) - } -} - -func testIPFamilies(t *testing.T, family networkservice.IpFamily_Family, prefixLen uint32, prefix, cidr0, cidr1, cidr2 string) { - prefixes := []string{prefix} - server := newServer(prefixes, nil) - - request := newRequest() - client1 := newClient(prefixLen, family, server) - conn1, err := client1.Request(context.Background(), request) - require.NoError(t, err) - validateConn(t, conn1, cidr0, prefix) - - // refresh - conn1, err = client1.Request(context.Background(), request) - require.NoError(t, err) - validateConn(t, conn1, cidr0, prefix) - - client2 := newClient(prefixLen, family, server) - conn2, err := client2.Request(context.Background(), newRequest()) - require.NoError(t, err) - validateConn(t, conn2, cidr1, prefix) - - _, err = client1.Close(context.Background(), conn1) - require.NoError(t, err) - - client3 := newClient(prefixLen, family, server) - conn3, err := client3.Request(context.Background(), newRequest()) - require.NoError(t, err) - validateConn(t, conn3, cidr0, prefix) - - client4 := newClient(prefixLen, family, server) - conn4, err := client4.Request(context.Background(), newRequest()) - require.NoError(t, err) - validateConn(t, conn4, cidr2, prefix) -} - -func TestIPv4Exclude(t *testing.T) { - prefixes := []string{"192.168.0.0/16"} - excludePrefixes := []string{"192.168.0.0/24"} - server := newServer(prefixes, excludePrefixes) - - client1 := newClient(24, networkservice.IpFamily_IPV4, server) - conn1, err := client1.Request(context.Background(), newRequest()) - require.NoError(t, err) - require.NotEqual(t, "192.168.0.0/24", conn1.Context.IpContext.ExtraPrefixes[0]) - require.Equal(t, conn1.Context.IpContext.SrcRoutes, []*networkservice.Route{ - { - Prefix: "192.168.0.0/16", - }, - }) -} diff --git a/pkg/networkservice/common/cidr/client.go b/pkg/networkservice/common/cidr/client.go deleted file mode 100644 index bddd0eff29..0000000000 --- a/pkg/networkservice/common/cidr/client.go +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at: -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cidr - -import ( - "context" - - "github.com/golang/protobuf/ptypes/empty" - "github.com/networkservicemesh/api/pkg/api/networkservice" - "google.golang.org/grpc" - - "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" - "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" -) - -type cidrClient struct { - prefixLen uint32 - family networkservice.IpFamily_Family -} - -// NewClient creates a NetworkServiceClient chain element that requests ExtraPrefix -func NewClient(prefixLen uint32, family networkservice.IpFamily_Family) networkservice.NetworkServiceClient { - return &cidrClient{ - prefixLen: prefixLen, - family: family, - } -} - -func (c *cidrClient) Request(ctx context.Context, request *networkservice.NetworkServiceRequest, opts ...grpc.CallOption) (*networkservice.Connection, error) { - conn := request.GetConnection() - if conn.GetContext() == nil { - conn.Context = &networkservice.ConnectionContext{} - } - if conn.GetContext().GetIpContext() == nil { - conn.Context.IpContext = &networkservice.IPContext{} - } - ipContext := conn.GetContext().GetIpContext() - - // Add ExtraPrefixRequest if there is no extra prefix - if ipContext.GetExtraPrefixes() == nil { - ipContext.ExtraPrefixRequest = append(ipContext.ExtraPrefixRequest, &networkservice.ExtraPrefixRequest{ - AddrFamily: &networkservice.IpFamily{Family: c.family}, - PrefixLen: c.prefixLen, - RequiredNumber: 1, - RequestedNumber: 1, - }) - } - - // Add ExtraPrefix to the route for the remote side - loaded := load(ctx, metadata.IsClient(c)) - if !loaded && ipContext.GetExtraPrefixes() != nil { - for _, item := range ipContext.ExtraPrefixes { - ipContext.DstRoutes = append(ipContext.DstRoutes, &networkservice.Route{ - Prefix: item, - }) - } - store(ctx, metadata.IsClient(c)) - } - - conn, err := next.Client(ctx).Request(ctx, request, opts...) - if err != nil && !loaded { - delete(ctx, metadata.IsClient(c)) - } - - return conn, err -} - -func (c *cidrClient) Close(ctx context.Context, conn *networkservice.Connection, opts ...grpc.CallOption) (*empty.Empty, error) { - delete(ctx, metadata.IsClient(c)) - return next.Client(ctx).Close(ctx, conn, opts...) -} diff --git a/pkg/networkservice/common/cidr/doc.go b/pkg/networkservice/common/cidr/doc.go deleted file mode 100644 index 15d2a76614..0000000000 --- a/pkg/networkservice/common/cidr/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at: -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Package cidr provides networkservice.NetworkService chain elements for creating loopback interface -package cidr diff --git a/pkg/networkservice/common/cidr/metadata.go b/pkg/networkservice/common/cidr/metadata.go deleted file mode 100644 index d8a14d8e66..0000000000 --- a/pkg/networkservice/common/cidr/metadata.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at: -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cidr - -import ( - "context" - - "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" -) - -type key struct{} - -func store(ctx context.Context, isClient bool) { - metadata.Map(ctx, isClient).Store(key{}, struct{}{}) -} - -func delete(ctx context.Context, isClient bool) { - metadata.Map(ctx, isClient).Delete(key{}) -} - -func load(ctx context.Context, isClient bool) (ok bool) { - _, ok = metadata.Map(ctx, isClient).Load(key{}) - return ok -} diff --git a/pkg/networkservice/common/cidr/server.go b/pkg/networkservice/common/cidr/server.go deleted file mode 100644 index d5ca59af5f..0000000000 --- a/pkg/networkservice/common/cidr/server.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. -// -// SPDX-License-Identifier: Apache-2.0 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at: -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package cidr - -import ( - "context" - "sync" - - "github.com/golang/protobuf/ptypes/empty" - "github.com/pkg/errors" - - "github.com/networkservicemesh/api/pkg/api/networkservice" - - "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" - "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" - "github.com/networkservicemesh/sdk/pkg/tools/prefixpool" -) - -type cidrServer struct { - pool *prefixpool.PrefixPool - - prefixes []string - excludePrefixes []string - once sync.Once - initErr error -} - -// NewServer creates a NetworkServiceServer chain element that allocates CIDR from some global prefix -// and saves it in ExtraPrefix -func NewServer(prefixes, excludePrefixes []string) networkservice.NetworkServiceServer { - return &cidrServer{ - prefixes: prefixes, - excludePrefixes: excludePrefixes, - } -} - -func (c *cidrServer) init() { - if len(c.prefixes) == 0 { - c.initErr = errors.New("required one or more prefixes") - return - } - var err error - if c.pool, err = prefixpool.New(c.prefixes...); err != nil { - c.initErr = errors.New("required one or more prefixes") - return - } - if _, err = c.pool.ExcludePrefixes(c.excludePrefixes); err != nil { - c.initErr = errors.New("unable to exclude additional prefixes") - return - } -} - -func (c *cidrServer) Request(ctx context.Context, request *networkservice.NetworkServiceRequest) (*networkservice.Connection, error) { - c.once.Do(c.init) - if c.initErr != nil { - return nil, c.initErr - } - - conn := request.GetConnection() - if conn.GetContext() == nil { - conn.Context = &networkservice.ConnectionContext{} - } - if conn.GetContext().GetIpContext() == nil { - conn.Context.IpContext = &networkservice.IPContext{} - } - ipContext := conn.GetContext().GetIpContext() - - /* If we already have extra prefixes, exclude them from the next CIDR allocations */ - if ipContext.GetExtraPrefixes() != nil { - if _, err := c.pool.ExcludePrefixes(ipContext.GetExtraPrefixes()); err != nil { - return nil, err - } - } else if ipContext.GetExtraPrefixRequest() != nil { - /* Else, extract a new one if there is ExtraPrefixRequest */ - requested, err := c.pool.ExtractPrefixes(request.GetConnection().GetId(), ipContext.GetExtraPrefixRequest()...) - if err != nil { - return nil, err - } - ipContext.ExtraPrefixes = append(ipContext.ExtraPrefixes, requested...) - ipContext.ExtraPrefixRequest = nil - - for i := 0; i < len(requested); i++ { - ipContext.DstRoutes = append(ipContext.DstRoutes, &networkservice.Route{ - Prefix: requested[i], - }) - } - } - - conn, err := next.Server(ctx).Request(ctx, request) - if err != nil { - extraPrefixes := request.GetConnection().GetContext().GetIpContext().GetExtraPrefixes() - if len(extraPrefixes) != 0 { - _ = c.pool.ReleaseExcludedPrefixes(extraPrefixes) - } - return conn, err - } - - ipContext = conn.GetContext().GetIpContext() - if ok := load(ctx, metadata.IsClient(c)); !ok { - /* Set srcRoutes = prefixes because these hosts should be available through this element */ - for i := 0; i < len(c.prefixes); i++ { - ipContext.SrcRoutes = append(ipContext.SrcRoutes, &networkservice.Route{ - Prefix: c.prefixes[i], - }) - } - store(ctx, metadata.IsClient(c)) - } - - return conn, err -} - -func (c *cidrServer) Close(ctx context.Context, conn *networkservice.Connection) (*empty.Empty, error) { - extraPrefixes := conn.GetContext().GetIpContext().GetExtraPrefixes() - if len(extraPrefixes) != 0 { - _ = c.pool.ReleaseExcludedPrefixes(extraPrefixes) - } - delete(ctx, metadata.IsClient(c)) - - return next.Server(ctx).Close(ctx, conn) -} diff --git a/pkg/networkservice/connectioncontext/ipcontext/vl3/client.go b/pkg/networkservice/connectioncontext/ipcontext/vl3/client.go new file mode 100644 index 0000000000..52be2bd99b --- /dev/null +++ b/pkg/networkservice/connectioncontext/ipcontext/vl3/client.go @@ -0,0 +1,147 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package vl3 provides chain elements that manage ipcontext of request for vL3 networks. +// Depends on `begin`, `metadata` chain elements. +package vl3 + +import ( + "context" + "errors" + + "github.com/edwarnicke/serialize" + "github.com/golang/protobuf/ptypes/empty" + "github.com/networkservicemesh/api/pkg/api/ipam" + "github.com/networkservicemesh/api/pkg/api/networkservice" + "google.golang.org/grpc" + + "github.com/networkservicemesh/sdk/pkg/networkservice/common/begin" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" +) + +type vl3Client struct { + pool vl3IPAM + chainContext context.Context + executor serialize.Executor + subscriptions []chan struct{} +} + +// NewClient - returns a new vL3 client instance that manages connection.context.ipcontext for vL3 scenario. +// Produces refresh on prefix update. +// Requires begin and metdata chain elements. +func NewClient(chainContext context.Context, prefixCh <-chan *ipam.PrefixResponse) networkservice.NetworkServiceClient { + if chainContext == nil { + panic("chainContext can not be nil") + } + if prefixCh == nil { + panic("prefixCh can not be nil") + } + var r = &vl3Client{ + chainContext: chainContext, + } + + go func() { + for update := range prefixCh { + prefixResp := update + r.executor.AsyncExec(func() { + r.pool.reset(chainContext, prefixResp.GetPrefix(), prefixResp.GetExcludePrefixes()) + for _, sub := range r.subscriptions { + sub <- struct{}{} + } + }) + } + }() + + return r +} + +func (n *vl3Client) Request(ctx context.Context, request *networkservice.NetworkServiceRequest, opts ...grpc.CallOption) (*networkservice.Connection, error) { + if !n.pool.isInitialized() { + return nil, errors.New("prefix pool is initializing") + } + eventFactory := begin.FromContext(ctx) + if eventFactory == nil { + return nil, errors.New("begin is required. Please add begin.NewClient() into chain") + } + cancelCtx, cancel := context.WithCancel(n.chainContext) + + if oldCancel, loaded := loadAndDeleteCancel(ctx); loaded { + oldCancel() + } + + storeCancel(ctx, cancel) + + notifyCh := make(chan struct{}) + + n.executor.AsyncExec(func() { + n.subscriptions = append(n.subscriptions, notifyCh) + }) + + go func() { + defer func() { + n.executor.AsyncExec(func() { + for i, sub := range n.subscriptions { + if sub == notifyCh { + n.subscriptions = append(n.subscriptions[:i], n.subscriptions[i+1:]...) + close(notifyCh) + return + } + } + }) + }() + + select { + case <-n.chainContext.Done(): + return + case <-cancelCtx.Done(): + return + case <-notifyCh: + eventFactory.Request(begin.CancelContext(cancelCtx)) + } + }() + + if request.Connection == nil { + request.Connection = new(networkservice.Connection) + } + var conn = request.GetConnection() + if conn.GetContext() == nil { + conn.Context = new(networkservice.ConnectionContext) + } + if conn.GetContext().GetIpContext() == nil { + conn.GetContext().IpContext = new(networkservice.IPContext) + } + + var address, prefix = n.pool.selfAddress().String(), n.pool.selfPrefix().String() + + conn.GetContext().GetIpContext().SrcIpAddrs = []string{address} + conn.GetContext().GetIpContext().DstRoutes = []*networkservice.Route{ + { + Prefix: address, + }, + { + Prefix: prefix, + }, + } + + return next.Client(ctx).Request(ctx, request, opts...) +} + +func (n *vl3Client) Close(ctx context.Context, conn *networkservice.Connection, opts ...grpc.CallOption) (*empty.Empty, error) { + if oldCancel, loaded := loadAndDeleteCancel(ctx); loaded { + oldCancel() + } + return next.Client(ctx).Close(ctx, conn, opts...) +} diff --git a/pkg/networkservice/connectioncontext/ipcontext/vl3/client_test.go b/pkg/networkservice/connectioncontext/ipcontext/vl3/client_test.go new file mode 100644 index 0000000000..5ab4840453 --- /dev/null +++ b/pkg/networkservice/connectioncontext/ipcontext/vl3/client_test.go @@ -0,0 +1,209 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package vl3_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/networkservicemesh/api/pkg/api/ipam" + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/networkservicemesh/sdk/pkg/networkservice/common/begin" + "github.com/networkservicemesh/sdk/pkg/networkservice/connectioncontext/ipcontext/vl3" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/adapters" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" + "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" +) + +func Test_VL3NSE_ConnectsToVl3NSE(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + var serverPrefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(serverPrefixCh) + + serverPrefixCh <- &ipam.PrefixResponse{Prefix: "10.0.0.1/24"} + + var clientPrefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(clientPrefixCh) + + clientPrefixCh <- &ipam.PrefixResponse{Prefix: "10.0.1.0/24"} + + var server = next.NewNetworkServiceServer( + adapters.NewClientToServer( + next.NewNetworkServiceClient( + begin.NewClient(), + metadata.NewClient(), + vl3.NewClient(ctx, clientPrefixCh), + ), + ), + metadata.NewServer(), + vl3.NewServer(ctx, serverPrefixCh), + ) + + require.Eventually(t, func() bool { return len(serverPrefixCh) == 0 && len(clientPrefixCh) == 0 }, time.Second, time.Millisecond*100) + + resp, err := server.Request(ctx, &networkservice.NetworkServiceRequest{Connection: &networkservice.Connection{Id: t.Name()}}) + + require.NoError(t, err) + + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.1.0/24", resp.GetContext().GetIpContext().GetDstRoutes()[1].GetPrefix()) + + // refresh + resp, err = server.Request(ctx, &networkservice.NetworkServiceRequest{Connection: resp}) + + require.NoError(t, err) + + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.1.0/24", resp.GetContext().GetIpContext().GetDstRoutes()[1].GetPrefix()) +} + +func Test_VL3NSE_ConnectsToVl3NSE_ChangePrefix(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + var serverPrefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(serverPrefixCh) + + serverPrefixCh <- &ipam.PrefixResponse{Prefix: "10.0.0.1/24"} + + var clientPrefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(clientPrefixCh) + + clientPrefixCh <- &ipam.PrefixResponse{Prefix: "10.0.1.0/24"} + + var server = next.NewNetworkServiceServer( + adapters.NewClientToServer( + next.NewNetworkServiceClient( + begin.NewClient(), + metadata.NewClient(), + vl3.NewClient(ctx, clientPrefixCh), + ), + ), + metadata.NewServer(), + vl3.NewServer(ctx, serverPrefixCh), + ) + + require.Eventually(t, func() bool { return len(serverPrefixCh) == 0 && len(clientPrefixCh) == 0 }, time.Second, time.Millisecond*100) + + resp, err := server.Request(ctx, &networkservice.NetworkServiceRequest{Connection: &networkservice.Connection{Id: t.Name()}}) + + require.NoError(t, err) + + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.1.0/24", resp.GetContext().GetIpContext().GetDstRoutes()[1].GetPrefix()) + + clientPrefixCh <- &ipam.PrefixResponse{Prefix: "10.0.5.0/24"} + require.Eventually(t, func() bool { return len(serverPrefixCh) == 0 && len(clientPrefixCh) == 0 }, time.Second, time.Millisecond*100) + + // refresh + for i := 0; i < 10; i++ { + resp, err = server.Request(ctx, &networkservice.NetworkServiceRequest{Connection: resp}) + + require.NoError(t, err) + + require.Equal(t, "10.0.5.0/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "10.0.5.0/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.5.0/24", resp.GetContext().GetIpContext().GetDstRoutes()[1].GetPrefix()) + } +} + +func Test_VL3NSE_ConnectsToVl3NSE_Close(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + var serverPrefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(serverPrefixCh) + + serverPrefixCh <- &ipam.PrefixResponse{Prefix: "10.0.0.1/24"} + + var clientPrefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(clientPrefixCh) + + clientPrefixCh <- &ipam.PrefixResponse{Prefix: "10.0.1.0/24"} + + var server = next.NewNetworkServiceServer( + adapters.NewClientToServer( + next.NewNetworkServiceClient( + begin.NewClient(), + metadata.NewClient(), + vl3.NewClient(ctx, clientPrefixCh), + ), + ), + metadata.NewServer(), + vl3.NewServer(ctx, serverPrefixCh), + ) + + require.Eventually(t, func() bool { return len(serverPrefixCh) == 0 && len(clientPrefixCh) == 0 }, time.Second, time.Millisecond*100) + + resp, err := server.Request(ctx, &networkservice.NetworkServiceRequest{Connection: &networkservice.Connection{Id: uuid.New().String()}}) + + require.NoError(t, err) + + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "10.0.1.0/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.1.0/24", resp.GetContext().GetIpContext().GetDstRoutes()[1].GetPrefix()) + + _, err = server.Close(ctx, resp) + + require.NoError(t, err) +} diff --git a/pkg/networkservice/connectioncontext/ipcontext/vl3/ipam.go b/pkg/networkservice/connectioncontext/ipcontext/vl3/ipam.go new file mode 100644 index 0000000000..85f6823a35 --- /dev/null +++ b/pkg/networkservice/connectioncontext/ipcontext/vl3/ipam.go @@ -0,0 +1,143 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vl3 + +import ( + "context" + "net" + "sync" + + "github.com/networkservicemesh/sdk/pkg/tools/ippool" + "github.com/networkservicemesh/sdk/pkg/tools/log" +) + +type vl3IPAM struct { + sync.Mutex + self net.IPNet + ipPool *ippool.IPPool + excludedPrefixes map[string]struct{} + clientMask uint8 +} + +func (p *vl3IPAM) isInitialized() bool { + p.Lock() + defer p.Unlock() + + return p.ipPool != nil +} + +func (p *vl3IPAM) selfAddress() *net.IPNet { + p.Lock() + defer p.Unlock() + return &net.IPNet{ + IP: p.self.IP, + Mask: net.CIDRMask( + int(p.clientMask), + int(p.clientMask), + ), + } +} + +func (p *vl3IPAM) selfPrefix() *net.IPNet { + p.Lock() + defer p.Unlock() + r := p.self + return &r +} +func (p *vl3IPAM) globalIPNet() *net.IPNet { + p.Lock() + defer p.Unlock() + return &net.IPNet{ + IP: p.self.IP, + Mask: net.CIDRMask( + int(p.clientMask)/2, + int(p.clientMask), + ), + } +} + +func (p *vl3IPAM) allocate() (*net.IPNet, error) { + p.Lock() + defer p.Unlock() + + ip, err := p.ipPool.Pull() + if err != nil { + return nil, err + } + + r := &net.IPNet{ + IP: ip, + Mask: net.CIDRMask( + int(p.clientMask), + int(p.clientMask), + ), + } + + p.excludedPrefixes[r.String()] = struct{}{} + return r, nil +} + +func (p *vl3IPAM) freeIfAllocated(ipNet string) { + p.Lock() + defer p.Unlock() + + if _, ok := p.excludedPrefixes[ipNet]; ok { + delete(p.excludedPrefixes, ipNet) + p.ipPool.AddNetString(ipNet) + } +} + +func (p *vl3IPAM) isExcluded(ipNet string) bool { + p.Lock() + defer p.Unlock() + + _, r := p.excludedPrefixes[ipNet] + return r +} + +func (p *vl3IPAM) reset(ctx context.Context, prefix string, excludePrefies []string) { + p.Lock() + defer p.Unlock() + + _, ipNet, err := net.ParseCIDR(prefix) + if err != nil { + log.FromContext(ctx).Error(err.Error()) + return + } + + p.self = *ipNet + p.ipPool = ippool.NewWithNet(ipNet) + p.excludedPrefixes = make(map[string]struct{}) + p.clientMask = net.IPv6len * 8 + if len(p.self.IP) == net.IPv4len { + p.clientMask = net.IPv4len * 8 + } + selfAddress := &net.IPNet{ + IP: p.self.IP, + Mask: net.CIDRMask( + int(p.clientMask), + int(p.clientMask), + ), + } + p.excludedPrefixes[selfAddress.String()] = struct{}{} + p.ipPool.Exclude(selfAddress) + + for _, excludePrefix := range excludePrefies { + p.ipPool.ExcludeString(excludePrefix) + p.excludedPrefixes[excludePrefix] = struct{}{} + } +} diff --git a/pkg/networkservice/connectioncontext/ipcontext/vl3/metdata.go b/pkg/networkservice/connectioncontext/ipcontext/vl3/metdata.go new file mode 100644 index 0000000000..c227ab8db5 --- /dev/null +++ b/pkg/networkservice/connectioncontext/ipcontext/vl3/metdata.go @@ -0,0 +1,53 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vl3 + +import ( + "context" + + "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" +) + +type addressKey struct{} + +func loadAddress(ctx context.Context) (string, bool) { + v, ok := metadata.Map(ctx, false).Load(addressKey{}) + if ok { + return v.(string), true + } + + return "", false +} + +func storeAddress(ctx context.Context, address string) { + metadata.Map(ctx, false).Store(addressKey{}, address) +} + +type cancelKey struct{} + +func storeCancel(ctx context.Context, cancel context.CancelFunc) { + metadata.Map(ctx, true).Store(cancelKey{}, cancel) +} + +func loadAndDeleteCancel(ctx context.Context) (value context.CancelFunc, ok bool) { + rawValue, ok := metadata.Map(ctx, true).LoadAndDelete(cancelKey{}) + if !ok { + return + } + value, ok = rawValue.(context.CancelFunc) + return value, ok +} diff --git a/pkg/networkservice/connectioncontext/ipcontext/vl3/server.go b/pkg/networkservice/connectioncontext/ipcontext/vl3/server.go new file mode 100644 index 0000000000..92b9fd8b42 --- /dev/null +++ b/pkg/networkservice/connectioncontext/ipcontext/vl3/server.go @@ -0,0 +1,127 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vl3 + +import ( + "context" + "errors" + + "github.com/golang/protobuf/ptypes/empty" + "github.com/networkservicemesh/api/pkg/api/ipam" + "github.com/networkservicemesh/api/pkg/api/networkservice" + + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" +) + +type vl3Server struct { + pool vl3IPAM +} + +// NewServer - returns a new vL3 server instance that manages connection.context.ipcontext for vL3 scenario. +// Produces refresh on prefix update. +// Requires begin and metdata chain elements. +func NewServer(ctx context.Context, prefixCh <-chan *ipam.PrefixResponse) networkservice.NetworkServiceServer { + var result = new(vl3Server) + + go func() { + for resp := range prefixCh { + result.pool.reset(ctx, resp.GetPrefix(), resp.GetExcludePrefixes()) + } + }() + + return result +} + +func (v *vl3Server) Request(ctx context.Context, request *networkservice.NetworkServiceRequest) (*networkservice.Connection, error) { + if !v.pool.isInitialized() { + return nil, errors.New("prefix pool is initializing") + } + if request.Connection == nil { + request.Connection = new(networkservice.Connection) + } + var conn = request.GetConnection() + if conn.GetContext() == nil { + conn.Context = new(networkservice.ConnectionContext) + } + if conn.GetContext().GetIpContext() == nil { + conn.GetContext().IpContext = new(networkservice.IPContext) + } + + var ipContext = &networkservice.IPContext{ + SrcIpAddrs: request.GetConnection().Context.GetIpContext().GetSrcIpAddrs(), + DstRoutes: request.GetConnection().Context.GetIpContext().GetDstRoutes(), + ExcludedPrefixes: request.GetConnection().Context.GetIpContext().GetExcludedPrefixes(), + } + + shouldAllocate := len(ipContext.SrcIpAddrs) == 0 + + if prevAddress, ok := loadAddress(ctx); ok && !shouldAllocate { + shouldAllocate = !v.pool.isExcluded(prevAddress) + } + + if shouldAllocate { + srcNet, err := v.pool.allocate() + if err != nil { + return nil, err + } + ipContext.DstRoutes = nil + ipContext.SrcIpAddrs = append([]string(nil), srcNet.String()) + storeAddress(ctx, srcNet.String()) + } + + addRoute(&ipContext.SrcRoutes, v.pool.selfAddress().String()) + addRoute(&ipContext.SrcRoutes, v.pool.selfPrefix().String()) + for _, srcAddr := range ipContext.SrcIpAddrs { + addRoute(&ipContext.DstRoutes, srcAddr) + } + addAddr(&ipContext.DstIpAddrs, v.pool.selfAddress().String()) + + conn.GetContext().IpContext = ipContext + + resp, err := next.Server(ctx).Request(ctx, request) + if err == nil { + addRoute(&resp.GetContext().GetIpContext().SrcRoutes, v.pool.globalIPNet().String()) + } + return resp, err +} + +func (v *vl3Server) Close(ctx context.Context, conn *networkservice.Connection) (*empty.Empty, error) { + for _, srcAddr := range conn.GetContext().GetIpContext().GetSrcIpAddrs() { + v.pool.freeIfAllocated(srcAddr) + } + return next.Server(ctx).Close(ctx, conn) +} + +func addRoute(routes *[]*networkservice.Route, prefix string) { + for _, route := range *routes { + if route.Prefix == prefix { + return + } + } + *routes = append(*routes, &networkservice.Route{ + Prefix: prefix, + }) +} + +func addAddr(addrs *[]string, addr string) { + for _, a := range *addrs { + if a == addr { + return + } + } + *addrs = append(*addrs, addr) +} diff --git a/pkg/networkservice/connectioncontext/ipcontext/vl3/server_test.go b/pkg/networkservice/connectioncontext/ipcontext/vl3/server_test.go new file mode 100644 index 0000000000..d0f17119c7 --- /dev/null +++ b/pkg/networkservice/connectioncontext/ipcontext/vl3/server_test.go @@ -0,0 +1,175 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package vl3_test + +import ( + "context" + "testing" + "time" + + "github.com/networkservicemesh/api/pkg/api/ipam" + "github.com/networkservicemesh/api/pkg/api/networkservice" + + "github.com/networkservicemesh/sdk/pkg/networkservice/connectioncontext/ipcontext/vl3" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" + + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" +) + +func Test_NSC_ConnectsToVl3NSE(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + var prefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(prefixCh) + + prefixCh <- &ipam.PrefixResponse{Prefix: "10.0.0.1/24"} + + var server = next.NewNetworkServiceServer( + metadata.NewServer(), + vl3.NewServer(context.Background(), prefixCh), + ) + + require.Eventually(t, func() bool { return len(prefixCh) == 0 }, time.Second, time.Millisecond*100) + + resp, err := server.Request(context.Background(), new(networkservice.NetworkServiceRequest)) + + require.NoError(t, err) + + require.Equal(t, "10.0.0.1/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "10.0.0.1/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + + for i := 0; i < 10; i++ { + resp, err = server.Request(context.Background(), &networkservice.NetworkServiceRequest{Connection: resp}) + + require.NoError(t, err) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "10.0.0.1/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + + require.Equal(t, "10.0.0.1/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + } +} + +func Test_NSC_ConnectsToVl3NSE_PrefixHasChanged(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + var prefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(prefixCh) + + prefixCh <- &ipam.PrefixResponse{Prefix: "12.0.0.1/24"} + + var server = next.NewNetworkServiceServer( + metadata.NewServer(), + vl3.NewServer(context.Background(), prefixCh), + ) + + require.Eventually(t, func() bool { return len(prefixCh) == 0 }, time.Second, time.Millisecond*120) + + resp, err := server.Request(context.Background(), new(networkservice.NetworkServiceRequest)) + + require.NoError(t, err) + + require.Equal(t, "12.0.0.1/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "12.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "12.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "12.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "12.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "12.0.0.1/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + + prefixCh <- &ipam.PrefixResponse{Prefix: "11.0.0.1/24"} + require.Eventually(t, func() bool { return len(prefixCh) == 0 }, time.Second, time.Millisecond*100) + + // refresh + for i := 0; i < 10; i++ { + resp, err = server.Request(context.Background(), &networkservice.NetworkServiceRequest{Connection: resp}) + + require.NoError(t, err) + + require.Equal(t, "11.0.0.1/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0]) + require.Equal(t, "11.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0]) + + require.Equal(t, "11.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix()) + require.Equal(t, "11.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix()) + require.Equal(t, "11.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix()) + require.Equal(t, "11.0.0.1/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix()) + } +} + +func Test_NSC_ConnectsToVl3NSE_Close(t *testing.T) { + t.Cleanup(func() { + goleak.VerifyNone(t) + }) + + var prefixCh = make(chan *ipam.PrefixResponse, 1) + defer close(prefixCh) + + prefixCh <- &ipam.PrefixResponse{Prefix: "10.0.0.1/24"} + + var server = next.NewNetworkServiceServer( + metadata.NewServer(), + vl3.NewServer(context.Background(), prefixCh), + ) + + require.Eventually(t, func() bool { return len(prefixCh) == 0 }, time.Second, time.Millisecond*100) + + for i := 0; i < 10; i++ { + resp, err := server.Request(context.Background(), new(networkservice.NetworkServiceRequest)) + + require.NoError(t, err) + + require.Equal(t, "10.0.0.1/32", resp.GetContext().GetIpContext().GetSrcIpAddrs()[0], i) + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetDstIpAddrs()[0], i) + + require.Equal(t, "10.0.0.0/32", resp.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix(), i) + require.Equal(t, "10.0.0.0/24", resp.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix(), i) + require.Equal(t, "10.0.0.0/16", resp.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix(), i) + require.Equal(t, "10.0.0.1/32", resp.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix(), i) + + resp1, err1 := server.Request(context.Background(), new(networkservice.NetworkServiceRequest)) + + require.NoError(t, err1) + + require.Equal(t, "10.0.0.2/32", resp1.GetContext().GetIpContext().GetSrcIpAddrs()[0], i) + require.Equal(t, "10.0.0.0/32", resp1.GetContext().GetIpContext().GetDstIpAddrs()[0], i) + + require.Equal(t, "10.0.0.0/32", resp1.GetContext().GetIpContext().GetSrcRoutes()[0].GetPrefix(), i) + require.Equal(t, "10.0.0.0/24", resp1.GetContext().GetIpContext().GetSrcRoutes()[1].GetPrefix(), i) + require.Equal(t, "10.0.0.0/16", resp1.GetContext().GetIpContext().GetSrcRoutes()[2].GetPrefix(), i) + require.Equal(t, "10.0.0.2/32", resp1.GetContext().GetIpContext().GetDstRoutes()[0].GetPrefix(), i) + + _, err = server.Close(context.Background(), resp1) + require.NoError(t, err, i) + _, err = server.Close(context.Background(), resp) + require.NoError(t, err, i) + } +} diff --git a/pkg/tools/ippool/ippool.go b/pkg/tools/ippool/ippool.go index 8bc32d1a02..b1b368ed5b 100644 --- a/pkg/tools/ippool/ippool.go +++ b/pkg/tools/ippool/ippool.go @@ -1,4 +1,6 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. +// Copyright (c) 2021-2022 Doc.ai and/or its affiliates. +// +// Copyright (c) 2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -145,6 +147,41 @@ func (tree *IPPool) AddNetString(ipNetString string) { tree.AddNet(ipNet) } +// ContainsNetString parses ipNetRaw string and checks that pool contains whole ipNet +func (tree *IPPool) ContainsNetString(ipNetRaw string) bool { + _, ipNet, err := net.ParseCIDR(ipNetRaw) + if err != nil { + return false + } + + return tree.ContainsNet(ipNet) +} + +// ContainsNet checks that pool contains whole ipNet +func (tree *IPPool) ContainsNet(ipNet *net.IPNet) bool { + if ipNet == nil { + return false + } + + var node = tree.root + var ipRange = ipRangeFromIPNet(ipNet) + + for node != nil { + compare := node.Value.CompareRange(ipRange) + switch { + case compare < 0: + node = node.Left + case compare > 0: + node = node.Right + default: + lRange, rRange := ipRange.Sub(node.Value) + return lRange == nil && rRange == nil + } + } + + return false +} + // Contains - check the pool contains ip address func (tree *IPPool) Contains(ip net.IP) bool { if ip == nil { diff --git a/pkg/tools/ippool/ippool_test.go b/pkg/tools/ippool/ippool_test.go index 50b55d4fa5..37a6c83752 100644 --- a/pkg/tools/ippool/ippool_test.go +++ b/pkg/tools/ippool/ippool_test.go @@ -1,4 +1,6 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. +// Copyright (c) 2021-2022 Doc.ai and/or its affiliates. +// +// Copyright (c) 2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -17,6 +19,7 @@ package ippool import ( + "fmt" "math/rand" "net" "runtime" @@ -62,6 +65,67 @@ func TestIPPoolTool_AddRange(t *testing.T) { require.Equal(t, ipPool.size, uint64(2)) } +func TestGlobalCIDR(t *testing.T) { + ipPool := NewWithNetString("192.168.0.0/16") + require.True(t, ipPool.ContainsNetString("192.168.0.1/32")) + require.False(t, ipPool.ContainsNetString("193.169.0.1/32")) +} + +func TestIsItWorkCorrect(t *testing.T) { + ipPool := NewWithNetString("192.168.0.0/16") + ipPool.ExcludeString("192.168.0.0/30") + p, err := ipPool.Pull() + + require.NoError(t, err) + + prefix := p.String() + "/24" + fmt.Println(prefix) + + pool2 := NewWithNetString(prefix) + pool2.ExcludeString("192.168.0.0/30") + require.False(t, pool2.ContainsString("192.168.0.1")) +} + +func TestIPPool_ExcludeRange(t *testing.T) { + ipPool := NewWithNetString("192.168.0.0/16") + + for i := 0; i < 3; i++ { + p, err := ipPool.Pull() + + require.NoError(t, err) + + prefix := p.String() + "/24" + + ipPool.ExcludeString(prefix) + + require.Equal(t, fmt.Sprintf("192.168.%v.0/24", i), prefix) + } +} + +func Test_IPPoolContains(t *testing.T) { + ipPool := NewWithNetString("10.10.0.0/16") + + for i := 1; i <= 255; i++ { + if i == 10 { + continue + } + for j := 1; j <= 255; j++ { + if j == 10 { + continue + } + require.False(t, ipPool.ContainsNetString(fmt.Sprintf("%v.%v.0.0/24", i, j))) + } + } + for i := 16; i < 32; i++ { + ipNet := "10.10.0.0/" + fmt.Sprint(i) + require.True(t, ipPool.ContainsNetString(ipNet), ipNet) + } + for i := 15; i > 0; i-- { + ipNet := "10.10.0.0/" + fmt.Sprint(i) + require.False(t, ipPool.ContainsNetString(ipNet), ipNet) + } +} + func TestIPPoolTool_Contains(t *testing.T) { ipPool := NewWithNetString("192.168.0.0/16") diff --git a/pkg/tools/ippool/tools.go b/pkg/tools/ippool/tools.go index 3731d88b29..e1821391da 100644 --- a/pkg/tools/ippool/tools.go +++ b/pkg/tools/ippool/tools.go @@ -1,4 +1,6 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. +// Copyright (c) 2021-2022 Doc.ai and/or its affiliates. +// +// Copyright (c) 2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/pkg/tools/ippool/types.go b/pkg/tools/ippool/types.go index d019b79436..1bad674fab 100644 --- a/pkg/tools/ippool/types.go +++ b/pkg/tools/ippool/types.go @@ -1,4 +1,6 @@ -// Copyright (c) 2021 Doc.ai and/or its affiliates. +// Copyright (c) 2021-2022 Doc.ai and/or its affiliates. +// +// Copyright (c) 2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -118,7 +120,7 @@ func (b *ipRange) CompareRange(ipR *ipRange) int { } if b.Compare(ipR.start) > 0 && b.Compare(ipR.end) > 0 { if !ipR.end.IsFirst() && b.Compare(ipR.start.Prev()) == 0 { - return -1 + return 1 } return 2 }