diff --git a/pkg/networkservice/chains/forwarder/server.go b/pkg/networkservice/chains/forwarder/server.go index 59591be3..5aa47d51 100644 --- a/pkg/networkservice/chains/forwarder/server.go +++ b/pkg/networkservice/chains/forwarder/server.go @@ -25,12 +25,15 @@ import ( "context" "net" "net/url" + "sync" "time" "git.fd.io/govpp.git/api" "github.com/google/uuid" "github.com/networkservicemesh/api/pkg/api/networkservice" + ipsecapi "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" + "github.com/networkservicemesh/sdk/pkg/networkservice/chains/client" "github.com/networkservicemesh/sdk/pkg/networkservice/chains/endpoint" "github.com/networkservicemesh/sdk/pkg/networkservice/common/authorize" @@ -54,6 +57,7 @@ import ( "github.com/networkservicemesh/sdk-kernel/pkg/kernel/networkservice/ethernetcontext" "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/connectioncontext/mtu" + "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/ipsec" "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/kernel" "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/memif" "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/vlan" @@ -103,6 +107,7 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, vppConn registryclient.WithDialOptions(opts.dialOpts...)) rv := &xconnectNSServer{} + pinholeMutex := new(sync.Mutex) additionalFunctionality := []networkservice.NetworkServiceServer{ recvfd.NewServer(), sendfd.NewServer(), @@ -122,8 +127,9 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, vppConn kernel.MECHANISM: kernel.NewServer(vppConn), vxlan.MECHANISM: vxlan.NewServer(vppConn, tunnelIP, opts.vxlanOpts...), wireguard.MECHANISM: wireguard.NewServer(vppConn, tunnelIP), + ipsecapi.MECHANISM: ipsec.NewServer(vppConn, tunnelIP), }), - pinhole.NewServer(vppConn), + pinhole.NewServer(vppConn, pinhole.WithSharedMutex(pinholeMutex)), connect.NewServer( client.NewClient(ctx, client.WithoutRefresh(), @@ -146,9 +152,10 @@ func NewServer(ctx context.Context, tokenGenerator token.GeneratorFunc, vppConn kernel.NewClient(vppConn), vxlan.NewClient(vppConn, tunnelIP, opts.vxlanOpts...), wireguard.NewClient(vppConn, tunnelIP), + ipsec.NewClient(vppConn, tunnelIP), vlan.NewClient(vppConn, opts.domain2Device), filtermechanisms.NewClient(), - pinhole.NewClient(vppConn), + pinhole.NewClient(vppConn, pinhole.WithSharedMutex(pinholeMutex)), recvfd.NewClient(), nsmonitor.NewClient(ctx), sendfd.NewClient(), diff --git a/pkg/networkservice/mechanisms/ipsec/client.go b/pkg/networkservice/mechanisms/ipsec/client.go new file mode 100644 index 00000000..91c70af1 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/client.go @@ -0,0 +1,117 @@ +// 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 ipsec + +import ( + "context" + "net" + + "git.fd.io/govpp.git/api" + "github.com/golang/protobuf/ptypes/empty" + "github.com/pkg/errors" + "google.golang.org/grpc" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/cls" + ipsecMech "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" + "github.com/networkservicemesh/api/pkg/api/networkservice/payload" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/chain" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" + "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" + "github.com/networkservicemesh/sdk/pkg/tools/postpone" + + "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/ipsec/mtu" + "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/pinhole" +) + +type ipsecClient struct { + vppConn api.Connection + tunnelIP net.IP +} + +// NewClient - returns a new client for the IPSec remote mechanism +func NewClient(vppConn api.Connection, tunnelIP net.IP) networkservice.NetworkServiceClient { + return chain.NewNetworkServiceClient( + &ipsecClient{ + vppConn: vppConn, + tunnelIP: tunnelIP, + }, + mtu.NewClient(vppConn, tunnelIP), + ) +} + +func (i *ipsecClient) Request(ctx context.Context, request *networkservice.NetworkServiceRequest, opts ...grpc.CallOption) (*networkservice.Connection, error) { + if request.GetConnection().GetPayload() != payload.IP { + return next.Client(ctx).Request(ctx, request, opts...) + } + + rsaKey, err := generateRSAKey() + if err != nil { + return nil, err + } + publicKey, err := createCertBase64(rsaKey, metadata.IsClient(i)) + if err != nil { + return nil, err + } + // If we already have a key we can reuse it + // else create a new one and store it after successful interface creation + if mechanism := ipsecMech.ToMechanism(request.GetConnection().GetMechanism()); mechanism != nil { + // If there is a key in mechanism then we can use it + publicKey = mechanism.SrcPublicKey() + } + mechanism := &networkservice.Mechanism{ + Cls: cls.REMOTE, + Type: ipsecMech.MECHANISM, + Parameters: make(map[string]string), + } + ipsecMech.ToMechanism(mechanism). + SetSrcPublicKey(publicKey). + SetSrcIP(i.tunnelIP). + SetSrcPort(ikev2DefaultPort) + + request.MechanismPreferences = append(request.MechanismPreferences, mechanism) + + // Store extra IPPort entry to allow IKE protocol - https://www.rfc-editor.org/rfc/rfc5996 + pinhole.StoreExtra(ctx, metadata.IsClient(i), pinhole.NewIPPort(i.tunnelIP.String(), 500)) + + postponeCtxFunc := postpone.ContextWithValues(ctx) + + conn, err := next.Client(ctx).Request(ctx, request, opts...) + if err != nil { + return nil, err + } + + if err = create(ctx, conn, i.vppConn, rsaKey, metadata.IsClient(i)); err != nil { + closeCtx, cancelClose := postponeCtxFunc() + defer cancelClose() + + if _, closeErr := i.Close(closeCtx, conn, opts...); closeErr != nil { + err = errors.Wrapf(err, "connection closed with error: %s", closeErr.Error()) + } + + return nil, err + } + + return conn, nil +} + +func (i *ipsecClient) Close(ctx context.Context, conn *networkservice.Connection, opts ...grpc.CallOption) (*empty.Empty, error) { + if mechanism := ipsecMech.ToMechanism(conn.GetMechanism()); mechanism != nil { + delInterface(ctx, conn, i.vppConn, metadata.IsClient(i)) + } + return next.Client(ctx).Close(ctx, conn, opts...) +} diff --git a/pkg/networkservice/mechanisms/ipsec/common.go b/pkg/networkservice/mechanisms/ipsec/common.go new file mode 100644 index 00000000..063f6300 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/common.go @@ -0,0 +1,608 @@ +// 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 ipsec + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "os" + "path" + "time" + + "git.fd.io/govpp.git/api" + "github.com/edwarnicke/govpp/binapi/ikev2_types" + interfaces "github.com/edwarnicke/govpp/binapi/interface" + "github.com/edwarnicke/govpp/binapi/interface_types" + "github.com/edwarnicke/govpp/binapi/ip" + "github.com/edwarnicke/govpp/binapi/ip_types" + "github.com/networkservicemesh/sdk/pkg/tools/log" + "github.com/pkg/errors" + + "github.com/networkservicemesh/sdk-vpp/pkg/tools/ifindex" + "github.com/networkservicemesh/sdk-vpp/pkg/tools/types" + + "github.com/edwarnicke/govpp/binapi/ikev2" + ipsecapi "github.com/edwarnicke/govpp/binapi/ipsec" + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" +) + +// create - creates IPSEC with IKEv2 +func create(ctx context.Context, conn *networkservice.Connection, vppConn api.Connection, privateKey *rsa.PrivateKey, isClient bool) error { + if mechanism := ipsec.ToMechanism(conn.GetMechanism()); mechanism != nil { + _, ok := ifindex.Load(ctx, isClient) + if ok { + return nil + } + profileName := fmt.Sprintf("%s-%s", isClientPrefix(isClient), conn.Id) + + // *** CREATE IP TUNNEL *** // + swIfIndex, err := createIPSecTunnel(ctx, vppConn) + if err != nil { + return errors.WithStack(err) + } + + // *** CREATE PROFILE *** // + err = addDelProfile(ctx, vppConn, profileName, true) + if err != nil { + return errors.WithStack(err) + } + + // *** SET UDP ENCAPSULATION *** // + err = setUDPEncap(ctx, vppConn, profileName) + if err != nil { + return errors.WithStack(err) + } + + // *** SET KEYS *** // + err = setKeys(ctx, vppConn, profileName, mechanism, privateKey, isClient) + if err != nil { + return errors.WithStack(err) + } + + // *** SET FQDN *** // + err = setFQDN(ctx, vppConn, mechanism, profileName, isClient) + if err != nil { + return errors.WithStack(err) + } + + // *** SET TRAFFIC-SELECTOR *** // + err = setTrafficSelector(ctx, vppConn, profileName, conn, isClient) + if err != nil { + return errors.WithStack(err) + } + + // *** PROTECT THE TUNNEL *** // + err = protectTunnel(ctx, vppConn, profileName, swIfIndex) + if err != nil { + return errors.WithStack(err) + } + + // *** INITIATOR STEPS *** // + if isClient { + err = initiate(ctx, vppConn, mechanism, profileName) + if err != nil { + return err + } + } + + ifindex.Store(ctx, isClient, swIfIndex) + } + return nil +} + +func initiate(ctx context.Context, vppConn api.Connection, mechanism *ipsec.Mechanism, profileName string) error { + hostSwIfIndex, err := getSwIfIndexByIP(ctx, vppConn, mechanism.SrcIP()) + if err != nil { + return err + } + + // *** SET RESPONDER *** // + err = setResponder(ctx, vppConn, profileName, hostSwIfIndex, mechanism.DstIP()) + if err != nil { + return err + } + + // *** SET TRANSFORMS *** // + err = setTransforms(ctx, vppConn, profileName) + if err != nil { + return err + } + + // *** SET SA LIFETIME *** // + err = setSaLifetime(ctx, vppConn, profileName) + if err != nil { + return err + } + + // *** START INITIATION *** // + err = saInit(ctx, vppConn, profileName) + if err != nil { + return err + } + + return nil +} + +func getSwIfIndexByIP(ctx context.Context, vppConn api.Connection, interfaceIP net.IP) (interface_types.InterfaceIndex, error) { + client, err := interfaces.NewServiceClient(vppConn).SwInterfaceDump(ctx, &interfaces.SwInterfaceDump{}) + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get interface dump client for IP %q", interfaceIP) + } + defer func() { _ = client.Close() }() + + for { + details, err := client.Recv() + if err == io.EOF { + break + } + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get interface details for IP %q", interfaceIP) + } + + ipAddressClient, err := ip.NewServiceClient(vppConn).IPAddressDump(ctx, &ip.IPAddressDump{ + SwIfIndex: details.SwIfIndex, + IsIPv6: interfaceIP.To4() == nil, + }) + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get ip address dump client for vpp interface %q ", details.InterfaceName) + } + defer func() { _ = ipAddressClient.Close() }() + + for { + ipAddressDetails, err := ipAddressClient.Recv() + if err == io.EOF { + break + } + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get ip address for %q (swIfIndex: %q)", details.InterfaceName, details.SwIfIndex) + } + if types.FromVppAddressWithPrefix(ipAddressDetails.Prefix).IP.Equal(interfaceIP) { + return details.SwIfIndex, nil + } + } + } + return interface_types.InterfaceIndex(^uint32(0)), errors.Errorf("unable to find interface in vpp with IP: %q", interfaceIP) +} + +func createIPSecTunnel(ctx context.Context, vppConn api.Connection) (interface_types.InterfaceIndex, error) { + now := time.Now() + + reply, err := ipsecapi.NewServiceClient(vppConn).IpsecItfCreate(ctx, &ipsecapi.IpsecItfCreate{ + Itf: ipsecapi.IpsecItf{UserInstance: ^uint32(0)}}) + + if err != nil { + return interface_types.InterfaceIndex(^uint32(0)), err + } + log.FromContext(ctx). + WithField("swIfIndex", reply.SwIfIndex). + WithField("duration", time.Since(now)). + WithField("vppapi", "IpsecItfCreate").Debug("completed") + return reply.SwIfIndex, nil +} + +func delIPSecTunnel(ctx context.Context, vppConn api.Connection, isClient bool) error { + now := time.Now() + swIfIndex, ok := ifindex.LoadAndDelete(ctx, isClient) + if !ok { + return nil + } + + _, err := ipsecapi.NewServiceClient(vppConn).IpsecItfDelete(ctx, &ipsecapi.IpsecItfDelete{SwIfIndex: swIfIndex}) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("swIfIndex", swIfIndex). + WithField("duration", time.Since(now)). + WithField("vppapi", "IpsecItfDelete").Debug("completed") + return nil +} + +func addDelProfile(ctx context.Context, vppConn api.Connection, profileName string, isAdd bool) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2ProfileAddDel(ctx, &ikev2.Ikev2ProfileAddDel{ + Name: profileName, + IsAdd: isAdd, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("IsAdd", isAdd). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2ProfileAddDel").Debug("completed") + return nil +} + +func setUDPEncap(ctx context.Context, vppConn api.Connection, profileName string) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2ProfileSetUDPEncap(ctx, &ikev2.Ikev2ProfileSetUDPEncap{ + Name: profileName, + }) + if err != nil { + return errors.WithStack(err) + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2ProfileSetUDPEncap").Debug("completed") + return nil +} + +func setKeys(ctx context.Context, vppConn api.Connection, profileName string, mechanism *ipsec.Mechanism, privateKey *rsa.PrivateKey, isClient bool) error { + publicKeyBase64 := mechanism.SrcPublicKey() + if isClient { + publicKeyBase64 = mechanism.DstPublicKey() + } + publicKeyFileName, err := dumpCertBase64ToFile(publicKeyBase64, profileName, isClient) + if err != nil { + return err + } + log.FromContext(ctx).WithField("operation", "dumpCertBase64ToFile").Debug("completed") + + privateKeyFileName, err := dumpPrivateKeyToFile(privateKey, profileName, isClient) + if err != nil { + return err + } + log.FromContext(ctx).WithField("operation", "dumpPrivateKeyToFile").Debug("completed") + + now := time.Now() + _, err = ikev2.NewServiceClient(vppConn).Ikev2ProfileSetAuth(ctx, &ikev2.Ikev2ProfileSetAuth{ + Name: profileName, + AuthMethod: 1, // rsa-sig + DataLen: uint32(len(publicKeyFileName)), + Data: []byte(publicKeyFileName), + }) + if err != nil { + return errors.WithStack(err) + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2ProfileSetAuth").Debug("completed") + + now = time.Now() + _, err = ikev2.NewServiceClient(vppConn).Ikev2SetLocalKey(ctx, &ikev2.Ikev2SetLocalKey{ + KeyFile: privateKeyFileName, + }) + if err != nil { + return errors.WithStack(err) + } + log.FromContext(ctx). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2SetLocalKey").Debug("completed") + + return nil +} + +func setFQDN(ctx context.Context, vppConn api.Connection, mechanism *ipsec.Mechanism, profileName string, isClient bool) error { + now := time.Now() + + // We need unique values per client/server. Using public keys + fqdnLocal := mechanism.SrcPublicKey()[:64] + fqdnRemote := mechanism.DstPublicKey()[:64] + if !isClient { + fqdnLocal, fqdnRemote = fqdnRemote, fqdnLocal + } + _, err := ikev2.NewServiceClient(vppConn).Ikev2ProfileSetID(ctx, &ikev2.Ikev2ProfileSetID{ + Name: profileName, + IsLocal: true, + IDType: 2, // FQDN + DataLen: uint32(len(fqdnLocal)), + Data: []byte(fqdnLocal), + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("IsLocal", "true"). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2ProfileSetID").Debug("completed") + + now = time.Now() + _, err = ikev2.NewServiceClient(vppConn).Ikev2ProfileSetID(ctx, &ikev2.Ikev2ProfileSetID{ + Name: profileName, + IsLocal: false, + IDType: 2, // FQDN + DataLen: uint32(len(fqdnRemote)), + Data: []byte(fqdnRemote), + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("IsLocal", "false"). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2ProfileSetID").Debug("completed") + return nil +} + +func setTrafficSelector(ctx context.Context, vppConn api.Connection, profileName string, conn *networkservice.Connection, isClient bool) error { + now := time.Now() + for _, addr := range conn.GetContext().GetIpContext().GetSrcIpAddrs() { + a, err := ip_types.ParseAddressWithPrefix(addr) + if err != nil { + return err + } + _, err = ikev2.NewServiceClient(vppConn).Ikev2ProfileSetTs(ctx, &ikev2.Ikev2ProfileSetTs{ + Name: profileName, + Ts: ikev2_types.Ikev2Ts{ + IsLocal: isClient, + StartPort: 0, + EndPort: 65535, + StartAddr: a.Address, + EndAddr: a.Address, + }, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("IsLocal", isClient). + WithField("Address", addr). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2ProfileSetTs").Debug("completed") + } + + for _, addr := range conn.GetContext().GetIpContext().GetDstIpAddrs() { + a, err := ip_types.ParseAddressWithPrefix(addr) + if err != nil { + return err + } + _, err = ikev2.NewServiceClient(vppConn).Ikev2ProfileSetTs(ctx, &ikev2.Ikev2ProfileSetTs{ + Name: profileName, + Ts: ikev2_types.Ikev2Ts{ + IsLocal: !isClient, + StartPort: 0, + EndPort: 65535, + StartAddr: a.Address, + EndAddr: a.Address, + }, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("IsLocal", !isClient). + WithField("Address", addr). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2ProfileSetTs").Debug("completed") + } + return nil +} + +func protectTunnel(ctx context.Context, vppConn api.Connection, profileName string, tunSwIfIndex interface_types.InterfaceIndex) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2SetTunnelInterface(ctx, &ikev2.Ikev2SetTunnelInterface{ + Name: profileName, + SwIfIndex: tunSwIfIndex, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("SwIfIndex", tunSwIfIndex). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2SetTunnelInterface").Debug("completed") + return nil +} + +func setResponder(ctx context.Context, vppConn api.Connection, profileName string, hostSwIfIndex interface_types.InterfaceIndex, responderIP net.IP) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2SetResponder(ctx, &ikev2.Ikev2SetResponder{ + Name: profileName, + Responder: ikev2_types.Ikev2Responder{ + SwIfIndex: hostSwIfIndex, + Addr: types.ToVppAddress(responderIP), + }, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("SwIfIndex", hostSwIfIndex). + WithField("Addr", responderIP.String()). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2SetResponder").Debug("completed") + return nil +} + +func setTransforms(ctx context.Context, vppConn api.Connection, profileName string) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2SetIkeTransforms(ctx, &ikev2.Ikev2SetIkeTransforms{ + Name: profileName, + Tr: ikev2_types.Ikev2IkeTransforms{ + CryptoAlg: 12, // aes-cbc + CryptoKeySize: 256, + IntegAlg: 2, // sha1-96 + DhGroup: 14, // modp-2048 + }, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2SetIkeTransforms").Debug("completed") + + now = time.Now() + _, err = ikev2.NewServiceClient(vppConn).Ikev2SetEspTransforms(ctx, &ikev2.Ikev2SetEspTransforms{ + Name: profileName, + Tr: ikev2_types.Ikev2EspTransforms{ + CryptoAlg: 12, // aes-cbc, + CryptoKeySize: 256, + IntegAlg: 2, // sha1-96 + }, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2SetEspTransforms").Debug("completed") + return nil +} + +func setSaLifetime(ctx context.Context, vppConn api.Connection, profileName string) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2SetSaLifetime(ctx, &ikev2.Ikev2SetSaLifetime{ + Name: profileName, + Lifetime: 3600, + LifetimeJitter: 10, + Handover: 5, + LifetimeMaxdata: 0, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2SetSaLifetime").Debug("completed") + return nil +} + +func saInit(ctx context.Context, vppConn api.Connection, profileName string) error { + now := time.Now() + _, err := ikev2.NewServiceClient(vppConn).Ikev2InitiateSaInit(ctx, &ikev2.Ikev2InitiateSaInit{ + Name: profileName, + }) + if err != nil { + return err + } + log.FromContext(ctx). + WithField("Name", profileName). + WithField("duration", time.Since(now)). + WithField("vppapi", "Ikev2InitiateSaInit").Debug("completed") + return nil +} + +func delInterface(ctx context.Context, conn *networkservice.Connection, vppConn api.Connection, isClient bool) { + if mechanism := ipsec.ToMechanism(conn.GetMechanism()); mechanism != nil { + profileName := fmt.Sprintf("%s-%s", isClientPrefix(isClient), conn.Id) + _ = addDelProfile(ctx, vppConn, profileName, false) + _ = delIPSecTunnel(ctx, vppConn, isClient) + } +} + +func generateRSAKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, 2048) +} + +func dumpPrivateKeyToFile(privatekey *rsa.PrivateKey, profileName string, isClient bool) (string, error) { + dir := path.Join(os.TempDir(), profileName) + err := os.Mkdir(dir, 0o700) + if err != nil && !os.IsExist(err) { + return "", err + } + + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privatekey) + privateKeyBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: privateKeyBytes, + } + + file, err := os.Create(path.Clean(path.Join(dir, isClientPrefix(isClient)+"-key.pem"))) + if err != nil { + return "", err + } + err = pem.Encode(file, privateKeyBlock) + if err != nil { + return "", err + } + + return file.Name(), nil +} + +func createCertBase64(privatekey *rsa.PrivateKey, isClient bool) (string, error) { + // Generate cryptographically strong pseudo-random between 0 - max + max := new(big.Int) + max.Exp(big.NewInt(2), big.NewInt(130), nil).Sub(max, big.NewInt(1)) + n, err := rand.Int(rand.Reader, max) + if err != nil { + return "", err + } + + template := &x509.Certificate{ + SerialNumber: n, + Subject: pkix.Name{ + CommonName: isClientPrefix(isClient), + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Minute * 5), + } + certbytes, err := x509.CreateCertificate(rand.Reader, template, template, &privatekey.PublicKey, privatekey) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(certbytes), nil +} + +func dumpCertBase64ToFile(base64key, profileName string, isClient bool) (string, error) { + dir := path.Join(os.TempDir(), profileName) + err := os.Mkdir(dir, 0o700) + if err != nil && !os.IsExist(err) { + return "", err + } + + certbytes, err := base64.StdEncoding.DecodeString(base64key) + if err != nil { + return "", err + } + + publicKeyBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: certbytes, + } + + file, err := os.Create(path.Clean(path.Join(dir, isClientPrefix(!isClient)+"-cert.pem"))) + if err != nil { + return "", err + } + err = pem.Encode(file, publicKeyBlock) + if err != nil { + return "", err + } + + return file.Name(), nil +} + +func isClientPrefix(isClient bool) string { + if isClient { + return "client" + } + return "server" +} diff --git a/pkg/networkservice/mechanisms/ipsec/constants.go b/pkg/networkservice/mechanisms/ipsec/constants.go new file mode 100644 index 00000000..169d4d43 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/constants.go @@ -0,0 +1,22 @@ +// Copyright (c) 2022 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 ipsec + +const ( + // ikev2DefaultPort - ikev2 default port + ikev2DefaultPort = 4500 +) diff --git a/pkg/networkservice/mechanisms/ipsec/doc.go b/pkg/networkservice/mechanisms/ipsec/doc.go new file mode 100644 index 00000000..2e1e84d5 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/doc.go @@ -0,0 +1,19 @@ +// 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 ipsec provides networkservice.NetworkService{Client,Server} chain elements for the ipsec mechanism +// The implementation is based on IKEv2 protocol +package ipsec diff --git a/pkg/networkservice/mechanisms/ipsec/mtu/client.go b/pkg/networkservice/mechanisms/ipsec/mtu/client.go new file mode 100644 index 00000000..740f4101 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/mtu/client.go @@ -0,0 +1,91 @@ +// 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 mtu + +import ( + "context" + "net" + "sync" + "sync/atomic" + + "git.fd.io/govpp.git/api" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" + + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" +) + +type mtuClient struct { + vppConn api.Connection + tunnelIP net.IP + mtu uint32 + + inited uint32 + initMutex sync.Mutex +} + +// NewClient - returns client chain element to manage ipsec MTU +func NewClient(vppConn api.Connection, tunnelIP net.IP) networkservice.NetworkServiceClient { + return &mtuClient{ + vppConn: vppConn, + tunnelIP: tunnelIP, + } +} + +func (m *mtuClient) Request(ctx context.Context, request *networkservice.NetworkServiceRequest, opts ...grpc.CallOption) (*networkservice.Connection, error) { + if err := m.init(ctx); err != nil { + return nil, err + } + if mechanism := ipsec.ToMechanism(request.GetConnection().GetMechanism()); mechanism != nil && (mechanism.MTU() == 0 || mechanism.MTU() > m.mtu) { + mechanism.SetMTU(m.mtu) + } + for _, mech := range request.GetMechanismPreferences() { + if mechanism := ipsec.ToMechanism(mech); mechanism != nil && (mechanism.MTU() == 0 || mechanism.MTU() > m.mtu) { + mechanism.SetMTU(m.mtu) + } + } + conn, err := next.Client(ctx).Request(ctx, request, opts...) + if err != nil { + return nil, err + } + return conn, nil +} + +func (m *mtuClient) Close(ctx context.Context, conn *networkservice.Connection, opts ...grpc.CallOption) (*emptypb.Empty, error) { + return next.Client(ctx).Close(ctx, conn, opts...) +} + +func (m *mtuClient) init(ctx context.Context) error { + if atomic.LoadUint32(&m.inited) > 0 { + return nil + } + m.initMutex.Lock() + defer m.initMutex.Unlock() + if atomic.LoadUint32(&m.inited) > 0 { + return nil + } + + var err error + m.mtu, err = getMTU(ctx, m.vppConn, m.tunnelIP) + if err == nil { + atomic.StoreUint32(&m.inited, 1) + } + return err +} diff --git a/pkg/networkservice/mechanisms/ipsec/mtu/common.go b/pkg/networkservice/mechanisms/ipsec/mtu/common.go new file mode 100644 index 00000000..31b7cc20 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/mtu/common.go @@ -0,0 +1,96 @@ +// 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 mtu + +import ( + "context" + "io" + "net" + + "git.fd.io/govpp.git/api" + interfaces "github.com/edwarnicke/govpp/binapi/interface" + "github.com/edwarnicke/govpp/binapi/ip" + "github.com/pkg/errors" + + "github.com/networkservicemesh/sdk-vpp/pkg/tools/types" +) + +func getMTU(ctx context.Context, vppConn api.Connection, tunnelIP net.IP) (uint32, error) { + client, err := interfaces.NewServiceClient(vppConn).SwInterfaceDump(ctx, &interfaces.SwInterfaceDump{}) + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get interface dump client to determine MTU for tunnelIP %q", tunnelIP) + } + defer func() { _ = client.Close() }() + + for { + details, err := client.Recv() + if err == io.EOF { + break + } + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get interface details to determine MTU for tunnelIP %q", tunnelIP) + } + + ipAddressClient, err := ip.NewServiceClient(vppConn).IPAddressDump(ctx, &ip.IPAddressDump{ + SwIfIndex: details.SwIfIndex, + IsIPv6: tunnelIP.To4() == nil, + }) + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get ip address for vpp interface %q determine MTU for tunnelIP %q", details.InterfaceName, tunnelIP) + } + defer func() { _ = ipAddressClient.Close() }() + + for { + ipAddressDetails, err := ipAddressClient.Recv() + if err == io.EOF { + break + } + if err != nil { + return 0, errors.Wrapf(err, "error attempting to get interface ip address for %q (swIfIndex: %q) to determine MTU for tunnelIP %q", details.InterfaceName, details.SwIfIndex, tunnelIP) + } + if types.FromVppAddressWithPrefix(ipAddressDetails.Prefix).IP.Equal(tunnelIP) && details.Mtu[0] != 0 { + return (details.Mtu[0] - overhead(tunnelIP.To4() == nil)), nil + } + } + } + return 0, errors.Errorf("unable to find interface in vpp with tunnelIP: %q or interface IP MTU is zero", tunnelIP) +} + +func overhead(isV6 bool) uint32 { + if !isV6 { + // IP Header 20 + // UDP Header 8 + // IPsec Sequence Number 4 + // IPsec SPI 4 + // Initialization Vector 16 + // Padding 0 – 15 + // Padding Length 1 + // Next Header 1 + // Authentication Data 12 + return 81 + } + // IPv6 Header 40 + // UDP Header 8 + // IPsec Sequence Number 4 + // IPsec SPI 4 + // Initialization Vector 16 + // Padding 0 – 15 + // Padding Length 1 + // Next Header 1 + // Authentication Data 12 + return 101 +} diff --git a/pkg/networkservice/mechanisms/ipsec/mtu/doc.go b/pkg/networkservice/mechanisms/ipsec/mtu/doc.go new file mode 100644 index 00000000..fba966ba --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/mtu/doc.go @@ -0,0 +1,18 @@ +// 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 mtu computes the mtu for the ipsec tunnel and adds it to the mechanism +package mtu diff --git a/pkg/networkservice/mechanisms/ipsec/mtu/server.go b/pkg/networkservice/mechanisms/ipsec/mtu/server.go new file mode 100644 index 00000000..0b0270d8 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/mtu/server.go @@ -0,0 +1,94 @@ +// 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 mtu + +import ( + "context" + "net" + "sync" + "sync/atomic" + + "git.fd.io/govpp.git/api" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" + + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" +) + +type mtuServer struct { + vppConn api.Connection + tunnelIP net.IP + mtu uint32 + + inited uint32 + initMutex sync.Mutex +} + +// NewServer - server chain element to manage ipsec MTU +func NewServer(vppConn api.Connection, tunnelIP net.IP) networkservice.NetworkServiceServer { + return &mtuServer{ + vppConn: vppConn, + tunnelIP: tunnelIP, + } +} + +func (m *mtuServer) Request(ctx context.Context, request *networkservice.NetworkServiceRequest) (*networkservice.Connection, error) { + if mechanism := ipsec.ToMechanism(request.GetConnection().GetMechanism()); mechanism != nil { + if err := m.init(ctx); err != nil { + return nil, err + } + // If the clients MTU is zero or larger than the mtu for the local end of the tunnel, use the the mtu from the local end of the tunnel + if mechanism.MTU() > m.mtu || mechanism.MTU() == 0 { + mechanism.SetMTU(m.mtu) + } + // If the ConnectionContext's MTU is zero or larger than the MTU for the tunnel, set the ConnectionContexts MTU to the MTU for the tunnel + if request.GetConnection().GetContext().GetMTU() > mechanism.MTU() || request.GetConnection().GetContext().GetMTU() == 0 { + if request.GetConnection() == nil { + request.Connection = &networkservice.Connection{} + } + if request.GetConnection().GetContext() == nil { + request.GetConnection().Context = &networkservice.ConnectionContext{} + } + request.GetConnection().GetContext().MTU = mechanism.MTU() + } + } + return next.Server(ctx).Request(ctx, request) +} + +func (m *mtuServer) Close(ctx context.Context, conn *networkservice.Connection) (*emptypb.Empty, error) { + return next.Server(ctx).Close(ctx, conn) +} + +func (m *mtuServer) init(ctx context.Context) error { + if atomic.LoadUint32(&m.inited) > 0 { + return nil + } + m.initMutex.Lock() + defer m.initMutex.Unlock() + if atomic.LoadUint32(&m.inited) > 0 { + return nil + } + + var err error + m.mtu, err = getMTU(ctx, m.vppConn, m.tunnelIP) + if err == nil { + atomic.StoreUint32(&m.inited, 1) + } + return err +} diff --git a/pkg/networkservice/mechanisms/ipsec/server.go b/pkg/networkservice/mechanisms/ipsec/server.go new file mode 100644 index 00000000..92a015e2 --- /dev/null +++ b/pkg/networkservice/mechanisms/ipsec/server.go @@ -0,0 +1,106 @@ +// 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 ipsec + +import ( + "context" + "net" + + "git.fd.io/govpp.git/api" + "github.com/golang/protobuf/ptypes/empty" + "github.com/pkg/errors" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + ipsecMech "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" + "github.com/networkservicemesh/api/pkg/api/networkservice/payload" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/chain" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" + "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" + "github.com/networkservicemesh/sdk/pkg/tools/postpone" + + "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/mechanisms/ipsec/mtu" + "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/pinhole" +) + +type ipsecServer struct { + vppConn api.Connection + tunnelIP net.IP +} + +// NewServer - returns a new server for the IPSec remote mechanism +func NewServer(vppConn api.Connection, tunnelIP net.IP) networkservice.NetworkServiceServer { + return chain.NewNetworkServiceServer( + mtu.NewServer(vppConn, tunnelIP), + &ipsecServer{ + vppConn: vppConn, + tunnelIP: tunnelIP, + }, + ) +} + +func (i *ipsecServer) Request(ctx context.Context, request *networkservice.NetworkServiceRequest) (*networkservice.Connection, error) { + if request.GetConnection().GetPayload() != payload.IP { + return next.Server(ctx).Request(ctx, request) + } + if mechanism := ipsecMech.ToMechanism(request.GetConnection().GetMechanism()); mechanism != nil { + mechanism.SetDstIP(i.tunnelIP) + mechanism.SetDstPort(ikev2DefaultPort) + + // Store extra IPPort entry to allow IKE protocol - https://www.rfc-editor.org/rfc/rfc5996 + pinhole.StoreExtra(ctx, metadata.IsClient(i), pinhole.NewIPPort(i.tunnelIP.String(), 500)) + } + + postponeCtxFunc := postpone.ContextWithValues(ctx) + + conn, err := next.Server(ctx).Request(ctx, request) + if err != nil { + return nil, err + } + + if mechanism := ipsecMech.ToMechanism(conn.GetMechanism()); mechanism != nil { + rsaKey, err := generateRSAKey() + if err != nil { + return nil, err + } + publicKey, err := createCertBase64(rsaKey, metadata.IsClient(i)) + if err != nil { + return nil, err + } + mechanism.SetDstPublicKey(publicKey) + + err = create(ctx, conn, i.vppConn, rsaKey, metadata.IsClient(i)) + if err != nil { + closeCtx, cancelClose := postponeCtxFunc() + defer cancelClose() + + if _, closeErr := i.Close(closeCtx, conn); closeErr != nil { + err = errors.Wrapf(err, "connection closed with error: %s", closeErr.Error()) + } + + return nil, err + } + } + + return conn, nil +} + +func (i *ipsecServer) Close(ctx context.Context, conn *networkservice.Connection) (*empty.Empty, error) { + if mechanism := ipsecMech.ToMechanism(conn.GetMechanism()); mechanism != nil { + delInterface(ctx, conn, i.vppConn, metadata.IsClient(i)) + } + return next.Server(ctx).Close(ctx, conn) +} diff --git a/pkg/networkservice/pinhole/client.go b/pkg/networkservice/pinhole/client.go index ab1c02dd..b8560dd6 100644 --- a/pkg/networkservice/pinhole/client.go +++ b/pkg/networkservice/pinhole/client.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021 Cisco and/or its affiliates. +// Copyright (c) 2020-2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -19,6 +19,7 @@ package pinhole import ( "context" "fmt" + "sync" "git.fd.io/govpp.git/api" "github.com/golang/protobuf/ptypes/empty" @@ -33,13 +34,26 @@ import ( type pinholeClient struct { vppConn api.Connection - IPPortMap + ipPortMap + + // We need to protect ACL rules applying with a mutex. + // Because adding new entries is based on a dump and applying modified data. + // This must be an atomic operation, otherwise a data race is possible. + mutex *sync.Mutex } -// NewClient - returns a new client that will set an ACL permitting vxlan packets through if and only if there's an ACL on the interface -func NewClient(vppConn api.Connection) networkservice.NetworkServiceClient { +// NewClient - returns a new client that will set an ACL permitting remote protocols packets through if and only if there's an ACL on the interface +func NewClient(vppConn api.Connection, opts ...Option) networkservice.NetworkServiceClient { + o := &option{ + mutex: new(sync.Mutex), + } + for _, opt := range opts { + opt(o) + } + return &pinholeClient{ vppConn: vppConn, + mutex: o.mutex, } } @@ -51,8 +65,16 @@ func (v *pinholeClient) Request(ctx context.Context, request *networkservice.Net return nil, err } - if key := fromMechanism(conn.GetMechanism(), metadata.IsClient(v)); key != nil { - if _, ok := v.IPPortMap.LoadOrStore(*key, struct{}{}); !ok { + keys := []*IPPort{ + fromMechanism(conn.GetMechanism(), metadata.IsClient(v)), + fromContext(ctx, metadata.IsClient(v)), + } + for _, key := range keys { + if key == nil { + continue + } + if _, ok := v.ipPortMap.LoadOrStore(*key, struct{}{}); !ok { + v.mutex.Lock() if err := create(ctx, v.vppConn, key.IP(), key.Port(), fmt.Sprintf("%s port %d", aclTag, key.port)); err != nil { closeCtx, cancelClose := postponeCtxFunc() defer cancelClose() @@ -61,8 +83,10 @@ func (v *pinholeClient) Request(ctx context.Context, request *networkservice.Net err = errors.Wrapf(err, "connection closed with error: %s", closeErr.Error()) } + v.mutex.Unlock() return nil, err } + v.mutex.Unlock() } } diff --git a/pkg/networkservice/pinhole/gen.go b/pkg/networkservice/pinhole/gen.go index 521f6d43..7df12fa5 100644 --- a/pkg/networkservice/pinhole/gen.go +++ b/pkg/networkservice/pinhole/gen.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021 Cisco and/or its affiliates. +// Copyright (c) 2020-2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -20,7 +20,7 @@ import ( "sync" ) -//go:generate go-syncmap -output tunnel_ip_map.gen.go -type IPPortMap +//go:generate go-syncmap -output tunnel_ip_map.gen.go -type ipPortMap -// IPPortMap - sync.Map with key ipPortKey value of struct{} -type IPPortMap sync.Map +// IPPortMap - sync.Map with key IPPort value of struct{} +type ipPortMap sync.Map diff --git a/pkg/networkservice/pinhole/ipport.go b/pkg/networkservice/pinhole/ipport.go index 3db7977e..0fd58907 100644 --- a/pkg/networkservice/pinhole/ipport.go +++ b/pkg/networkservice/pinhole/ipport.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Cisco and/or its affiliates. +// Copyright (c) 2021-2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -17,6 +17,7 @@ package pinhole import ( + "context" "net" "strconv" @@ -24,13 +25,21 @@ import ( "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/common" ) -type ipPortKey struct { +// IPPort stores IP and port for an ACL rule +type IPPort struct { ip string port uint16 } -func fromMechanism(mechanism *networkservice.Mechanism, isClient bool) *ipPortKey { - rv := &ipPortKey{} +// NewIPPort returns *IPPort entry +func NewIPPort(ip string, port uint16) *IPPort { + return &IPPort{ + ip: ip, + port: port, + } +} + +func fromMechanism(mechanism *networkservice.Mechanism, isClient bool) *IPPort { if mechanism.GetParameters() == nil { return nil } @@ -44,7 +53,6 @@ func fromMechanism(mechanism *networkservice.Mechanism, isClient bool) *ipPortKe if !ok || ipStr == "" { return nil } - rv.ip = ipStr portStr, ok := mechanism.GetParameters()[portKey] if !ok { return nil @@ -53,14 +61,23 @@ func fromMechanism(mechanism *networkservice.Mechanism, isClient bool) *ipPortKe if err != nil { return nil } - rv.port = uint16(port) - return rv + return NewIPPort(ipStr, uint16(port)) +} + +func fromContext(ctx context.Context, isClient bool) *IPPort { + v, ok := LoadExtra(ctx, isClient) + if !ok { + return nil + } + return v } -func (i *ipPortKey) IP() net.IP { +// IP - converts string to net.IP +func (i *IPPort) IP() net.IP { return net.ParseIP(i.ip) } -func (i *ipPortKey) Port() uint16 { +// Port - returns port +func (i *IPPort) Port() uint16 { return i.port } diff --git a/pkg/networkservice/pinhole/metadata.go b/pkg/networkservice/pinhole/metadata.go new file mode 100644 index 00000000..b54b5917 --- /dev/null +++ b/pkg/networkservice/pinhole/metadata.go @@ -0,0 +1,47 @@ +// 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 pinhole + +import ( + "context" + + "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" +) + +type key struct{} + +// StoreExtra sets an extra IPPort stored in per Connection.Id metadata. +func StoreExtra(ctx context.Context, isClient bool, ipPort *IPPort) { + metadata.Map(ctx, isClient).Store(key{}, ipPort) +} + +// DeleteExtra deletes an extra IPPort stored in per Connection.Id metadata +func DeleteExtra(ctx context.Context, isClient bool) { + metadata.Map(ctx, isClient).Delete(key{}) +} + +// LoadExtra returns an extra IPPort stored in per Connection.Id metadata, or nil if no +// value is present. +// The ok result indicates whether value was found in the per Connection.Id metadata. +func LoadExtra(ctx context.Context, isClient bool) (value *IPPort, ok bool) { + rawValue, ok := metadata.Map(ctx, isClient).Load(key{}) + if !ok { + return + } + value, ok = rawValue.(*IPPort) + return value, ok +} diff --git a/pkg/networkservice/pinhole/options.go b/pkg/networkservice/pinhole/options.go new file mode 100644 index 00000000..5a4a7b68 --- /dev/null +++ b/pkg/networkservice/pinhole/options.go @@ -0,0 +1,37 @@ +// 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 pinhole + +import ( + "sync" +) + +type option struct { + mutex *sync.Mutex +} + +// Option - options for the pinhole chain element +type Option func(*option) + +// WithSharedMutex - set shared mutex between client and server chain elements. +// This is necessary because the pinhole chain element dumps VPP, performs some manipulations and reassign ACL rules. +// Possible data race. +func WithSharedMutex(mutex *sync.Mutex) Option { + return func(o *option) { + o.mutex = mutex + } +} diff --git a/pkg/networkservice/pinhole/server.go b/pkg/networkservice/pinhole/server.go index 49049b59..8c0027f5 100644 --- a/pkg/networkservice/pinhole/server.go +++ b/pkg/networkservice/pinhole/server.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2021 Cisco and/or its affiliates. +// Copyright (c) 2020-2022 Cisco and/or its affiliates. // // SPDX-License-Identifier: Apache-2.0 // @@ -19,6 +19,7 @@ package pinhole import ( "context" "fmt" + "sync" "git.fd.io/govpp.git/api" "github.com/golang/protobuf/ptypes/empty" @@ -32,13 +33,26 @@ import ( type pinholeServer struct { vppConn api.Connection - IPPortMap + ipPortMap + + // We need to protect ACL rules applying with a mutex. + // Because adding new entries is based on a dump and applying modified data. + // This must be an atomic operation, otherwise a data race is possible. + mutex *sync.Mutex } -// NewServer - returns a new client that will set an ACL permitting vxlan packets through if and only if there's an ACL on the interface -func NewServer(vppConn api.Connection) networkservice.NetworkServiceServer { +// NewServer - returns a new client that will set an ACL permitting remote protocols packets through if and only if there's an ACL on the interface +func NewServer(vppConn api.Connection, opts ...Option) networkservice.NetworkServiceServer { + o := &option{ + mutex: new(sync.Mutex), + } + for _, opt := range opts { + opt(o) + } + return &pinholeServer{ vppConn: vppConn, + mutex: o.mutex, } } @@ -50,8 +64,16 @@ func (v *pinholeServer) Request(ctx context.Context, request *networkservice.Net return nil, err } - if key := fromMechanism(conn.GetMechanism(), metadata.IsClient(v)); key != nil { - if _, ok := v.IPPortMap.LoadOrStore(*key, struct{}{}); !ok { + keys := []*IPPort{ + fromMechanism(conn.GetMechanism(), metadata.IsClient(v)), + fromContext(ctx, metadata.IsClient(v)), + } + for _, key := range keys { + if key == nil { + continue + } + if _, ok := v.ipPortMap.LoadOrStore(*key, struct{}{}); !ok { + v.mutex.Lock() if err := create(ctx, v.vppConn, key.IP(), key.Port(), fmt.Sprintf("%s port %d", aclTag, key.port)); err != nil { closeCtx, cancelClose := postponeCtxFunc() defer cancelClose() @@ -60,8 +82,10 @@ func (v *pinholeServer) Request(ctx context.Context, request *networkservice.Net err = errors.Wrapf(err, "connection closed with error: %s", closeErr.Error()) } + v.mutex.Unlock() return nil, err } + v.mutex.Unlock() } } diff --git a/pkg/networkservice/pinhole/tunnel_ip_map.gen.go b/pkg/networkservice/pinhole/tunnel_ip_map.gen.go index b6521e3c..e37cf883 100644 --- a/pkg/networkservice/pinhole/tunnel_ip_map.gen.go +++ b/pkg/networkservice/pinhole/tunnel_ip_map.gen.go @@ -1,4 +1,4 @@ -// Code generated by "-output tunnel_ip_map.gen.go -type IPPortMap -output tunnel_ip_map.gen.go -type IPPortMap"; DO NOT EDIT. +// Code generated by "-output tunnel_ip_map.gen.go -type ipPortMap -output tunnel_ip_map.gen.go -type ipPortMap"; DO NOT EDIT. package pinhole import ( @@ -7,52 +7,52 @@ import ( // Generate code that will fail if the constants change value. func _() { - // An "cannot convert IPPortMap literal (type IPPortMap) to type sync.Map" compiler error signifies that the base type have changed. + // An "cannot convert ipPortMap literal (type ipPortMap) to type sync.Map" compiler error signifies that the base type have changed. // Re-run the go-syncmap command to generate them again. - _ = (sync.Map)(IPPortMap{}) + _ = (sync.Map)(ipPortMap{}) } -var _nil_IPPortMap_struct___value = func() (val struct{}) { return }() +var _nil_ipPortMap_struct___value = func() (val struct{}) { return }() // Load returns the value stored in the map for a key, or nil if no // value is present. // The ok result indicates whether value was found in the map. -func (m *IPPortMap) Load(key ipPortKey) (struct{}, bool) { +func (m *ipPortMap) Load(key IPPort) (struct{}, bool) { value, ok := (*sync.Map)(m).Load(key) if value == nil { - return _nil_IPPortMap_struct___value, ok + return _nil_ipPortMap_struct___value, ok } return value.(struct{}), ok } // Store sets the value for a key. -func (m *IPPortMap) Store(key ipPortKey, value struct{}) { +func (m *ipPortMap) Store(key IPPort, value struct{}) { (*sync.Map)(m).Store(key, value) } // LoadOrStore returns the existing value for the key if present. // Otherwise, it stores and returns the given value. // The loaded result is true if the value was loaded, false if stored. -func (m *IPPortMap) LoadOrStore(key ipPortKey, value struct{}) (struct{}, bool) { +func (m *ipPortMap) LoadOrStore(key IPPort, value struct{}) (struct{}, bool) { actual, loaded := (*sync.Map)(m).LoadOrStore(key, value) if actual == nil { - return _nil_IPPortMap_struct___value, loaded + return _nil_ipPortMap_struct___value, loaded } return actual.(struct{}), loaded } // LoadAndDelete deletes the value for a key, returning the previous value if any. // The loaded result reports whether the key was present. -func (m *IPPortMap) LoadAndDelete(key ipPortKey) (value struct{}, loaded bool) { +func (m *ipPortMap) LoadAndDelete(key IPPort) (value struct{}, loaded bool) { actual, loaded := (*sync.Map)(m).LoadAndDelete(key) if actual == nil { - return _nil_IPPortMap_struct___value, loaded + return _nil_ipPortMap_struct___value, loaded } return actual.(struct{}), loaded } // Delete deletes the value for a key. -func (m *IPPortMap) Delete(key ipPortKey) { +func (m *ipPortMap) Delete(key IPPort) { (*sync.Map)(m).Delete(key) } @@ -66,8 +66,8 @@ func (m *IPPortMap) Delete(key ipPortKey) { // // Range may be O(N) with the number of elements in the map even if f returns // false after a constant number of calls. -func (m *IPPortMap) Range(f func(key ipPortKey, value struct{}) bool) { +func (m *ipPortMap) Range(f func(key IPPort, value struct{}) bool) { (*sync.Map)(m).Range(func(key, value interface{}) bool { - return f(key.(ipPortKey), value.(struct{})) + return f(key.(IPPort), value.(struct{})) }) } diff --git a/pkg/networkservice/up/client.go b/pkg/networkservice/up/client.go index 6c0b5f92..0f16df28 100644 --- a/pkg/networkservice/up/client.go +++ b/pkg/networkservice/up/client.go @@ -32,6 +32,7 @@ import ( "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" "github.com/networkservicemesh/sdk/pkg/tools/postpone" + "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/up/ipsecup" "github.com/networkservicemesh/sdk-vpp/pkg/networkservice/up/peerup" "github.com/networkservicemesh/sdk-vpp/pkg/tools/ifindex" ) @@ -61,6 +62,7 @@ func NewClient(ctx context.Context, vppConn Connection, opts ...Option) networks vppConn: vppConn, loadIfIndex: o.loadIfIndex, }, + ipsecup.NewClient(ctx, vppConn), ) } diff --git a/pkg/networkservice/up/ipsecup/client.go b/pkg/networkservice/up/ipsecup/client.go new file mode 100644 index 00000000..b3500838 --- /dev/null +++ b/pkg/networkservice/up/ipsecup/client.go @@ -0,0 +1,74 @@ +// 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 ipsecup provides chain elements that wait the 'up' of the IPSec interface +package ipsecup + +import ( + "context" + + "github.com/pkg/errors" + "google.golang.org/grpc" + + "github.com/golang/protobuf/ptypes/empty" + "github.com/networkservicemesh/api/pkg/api/networkservice/mechanisms/ipsec" + "github.com/networkservicemesh/sdk/pkg/networkservice/utils/metadata" + + "github.com/networkservicemesh/api/pkg/api/networkservice" + "github.com/networkservicemesh/sdk/pkg/networkservice/core/next" + "github.com/networkservicemesh/sdk/pkg/tools/postpone" +) + +type ipsecUpClient struct { + ctx context.Context + vppConn Connection +} + +// NewClient provides a NetworkServiceClient chain element that waits the 'up' of the IPSec interface +func NewClient(ctx context.Context, vppConn Connection) networkservice.NetworkServiceClient { + return &ipsecUpClient{ + ctx: ctx, + vppConn: vppConn, + } +} + +func (u *ipsecUpClient) Request(ctx context.Context, request *networkservice.NetworkServiceRequest, opts ...grpc.CallOption) (*networkservice.Connection, error) { + postponeCtxFunc := postpone.ContextWithValues(ctx) + + conn, err := next.Client(ctx).Request(ctx, request, opts...) + if err != nil { + return nil, err + } + + if mechanism := ipsec.ToMechanism(conn.GetMechanism()); mechanism != nil { + if err := waitForUpLinkUp(ctx, u.vppConn, metadata.IsClient(u)); err != nil { + closeCtx, cancelClose := postponeCtxFunc() + defer cancelClose() + + if _, closeErr := u.Close(closeCtx, conn, opts...); closeErr != nil { + err = errors.Wrapf(err, "connection closed with error: %s", closeErr.Error()) + } + + return nil, err + } + } + + return conn, nil +} + +func (u *ipsecUpClient) Close(ctx context.Context, conn *networkservice.Connection, opts ...grpc.CallOption) (*empty.Empty, error) { + return next.Client(ctx).Close(ctx, conn, opts...) +} diff --git a/pkg/networkservice/up/ipsecup/common.go b/pkg/networkservice/up/ipsecup/common.go new file mode 100644 index 00000000..4b06e78a --- /dev/null +++ b/pkg/networkservice/up/ipsecup/common.go @@ -0,0 +1,98 @@ +// 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 ipsecup + +import ( + "context" + "time" + + "git.fd.io/govpp.git/api" + interfaces "github.com/edwarnicke/govpp/binapi/interface" + "github.com/edwarnicke/govpp/binapi/interface_types" + "github.com/pkg/errors" + + "github.com/networkservicemesh/sdk/pkg/tools/log" + + "github.com/networkservicemesh/sdk-vpp/pkg/tools/ifindex" +) + +// Connection - simply combines tha api.Connection and api.ChannelProvider interfaces +type Connection interface { + api.Connection + api.ChannelProvider +} + +func waitForUpLinkUp(ctx context.Context, vppConn Connection, isClient bool) error { + swIfIndex, ok := ifindex.Load(ctx, isClient) + if !ok { + return nil + } + apiChannel, err := vppConn.NewAPIChannelBuffered(256, 256) + if err != nil { + return errors.WithStack(err) + } + defer apiChannel.Close() + notifCh := make(chan api.Message, 256) + subscription, err := apiChannel.SubscribeNotification(notifCh, &interfaces.SwInterfaceEvent{}) + if err != nil { + return errors.WithStack(err) + } + defer func() { _ = subscription.Unsubscribe() }() + + now := time.Now() + dc, err := interfaces.NewServiceClient(vppConn).SwInterfaceDump(ctx, &interfaces.SwInterfaceDump{ + SwIfIndex: swIfIndex, + }) + if err != nil { + return errors.WithStack(err) + } + defer func() { _ = dc.Close() }() + + details, err := dc.Recv() + if err != nil { + return errors.Wrapf(err, "error retrieving SwInterfaceDetails for swIfIndex %d", swIfIndex) + } + log.FromContext(ctx). + WithField("swIfIndex", swIfIndex). + WithField("details.Flags", details.Flags). + WithField("duration", time.Since(now)). + WithField("vppapi", "SwInterfaceDump").Debug("completed") + + isUp := details.Flags & interface_types.IF_STATUS_API_FLAG_LINK_UP + if isUp != 0 { + return nil + } + + now = time.Now() + for { + select { + case <-ctx.Done(): + return errors.WithStack(ctx.Err()) + case rawMsg := <-notifCh: + if msg, ok := rawMsg.(*interfaces.SwInterfaceEvent); ok && + msg.SwIfIndex == swIfIndex && + msg.Flags&interface_types.IF_STATUS_API_FLAG_LINK_UP != 0 { + log.FromContext(ctx). + WithField("swIfIndex", swIfIndex). + WithField("msg.Flags", msg.Flags). + WithField("duration", time.Since(now)). + WithField("vppapi", "SwInterfaceEvent").Debug("completed") + return nil + } + } + } +}