From 4b34a6d6dfee4546ef16871f148d30448a287e1d Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 18 Aug 2022 18:22:15 +0200 Subject: [PATCH] Add routing support to management service (#424) Management will receive and store routes that are associated with a peer ID. The routes are distributed to peers according to their ACLs. --- .github/workflows/golangci-lint.yml | 8 +- client/ui/client_ui.go | 5 +- management/cmd/management.go | 5 +- management/proto/management.pb.go | 294 +++++-- management/proto/management.proto | 12 + management/server/account.go | 12 +- management/server/file_store.go | 135 +++- management/server/grpcserver.go | 10 +- management/server/mock_server/account_mock.go | 55 ++ management/server/network.go | 2 + management/server/peer.go | 25 +- management/server/route.go | 362 +++++++++ management/server/route_test.go | 751 ++++++++++++++++++ management/server/store.go | 7 + route/route.go | 119 +++ 15 files changed, 1689 insertions(+), 113 deletions(-) create mode 100644 management/server/route.go create mode 100644 management/server/route_test.go create mode 100644 route/route.go diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 513b2d3c0..ef855f801 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -6,12 +6,16 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: 1.18.x - name: Install dependencies run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: - args: --timeout=6m + # SA1019: "io/ioutil" has been deprecated since Go 1.16 + args: --timeout=6m -e SA1019 diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 6701958a8..c6fdbc2c7 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -9,7 +9,6 @@ import ( "flag" "fmt" "github.com/netbirdio/netbird/client/system" - "io/ioutil" "os" "os/exec" "path" @@ -501,7 +500,7 @@ func (s *serviceClient) getSrvConfig() { // checkPIDFile exists and return error, or write new. func checkPIDFile() error { pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid") - if piddata, err := ioutil.ReadFile(pidFile); err == nil { + if piddata, err := os.ReadFile(pidFile); err == nil { if pid, err := strconv.Atoi(string(piddata)); err == nil { if process, err := os.FindProcess(pid); err == nil { if err := process.Signal(syscall.Signal(0)); err == nil { @@ -511,5 +510,5 @@ func checkPIDFile() error { } } - return ioutil.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664) + return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664) } diff --git a/management/cmd/management.go b/management/cmd/management.go index bce6c3080..a0fc8921f 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -11,7 +11,6 @@ import ( "golang.org/x/net/http2/h2c" "io" "io/fs" - "io/ioutil" "net" "net/http" "os" @@ -394,7 +393,7 @@ func copySymLink(source, dest string) error { func cpDir(src string, dst string) error { var err error - var fds []os.FileInfo + var fds []os.DirEntry var srcinfo os.FileInfo if srcinfo, err = os.Stat(src); err != nil { @@ -405,7 +404,7 @@ func cpDir(src string, dst string) error { return err } - if fds, err = ioutil.ReadDir(src); err != nil { + if fds, err = os.ReadDir(src); err != nil { return err } for _, fd := range fds { diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index 8fae8560f..b6b88e2da 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,15 +1,15 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.12.4 +// protoc v3.21.2 // source: management.proto package proto import ( - timestamp "github.com/golang/protobuf/ptypes/timestamp" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" ) @@ -611,7 +611,7 @@ type ServerKeyResponse struct { // Server's Wireguard public key Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` // Key expiration timestamp after which the key should be fetched again by the client - ExpiresAt *timestamp.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=expiresAt,proto3" json:"expiresAt,omitempty"` // Version of the Wiretrustee Management Service protocol Version int32 `protobuf:"varint,3,opt,name=version,proto3" json:"version,omitempty"` } @@ -655,7 +655,7 @@ func (x *ServerKeyResponse) GetKey() string { return "" } -func (x *ServerKeyResponse) GetExpiresAt() *timestamp.Timestamp { +func (x *ServerKeyResponse) GetExpiresAt() *timestamppb.Timestamp { if x != nil { return x.ExpiresAt } @@ -980,6 +980,8 @@ type NetworkMap struct { RemotePeers []*RemotePeerConfig `protobuf:"bytes,3,rep,name=remotePeers,proto3" json:"remotePeers,omitempty"` // Indicates whether remotePeers array is empty or not to bypass protobuf null and empty array equality. RemotePeersIsEmpty bool `protobuf:"varint,4,opt,name=remotePeersIsEmpty,proto3" json:"remotePeersIsEmpty,omitempty"` + // List of routes to be applied + Routes []*Route `protobuf:"bytes,5,rep,name=Routes,proto3" json:"Routes,omitempty"` } func (x *NetworkMap) Reset() { @@ -1042,6 +1044,13 @@ func (x *NetworkMap) GetRemotePeersIsEmpty() bool { return false } +func (x *NetworkMap) GetRoutes() []*Route { + if x != nil { + return x.Routes + } + return nil +} + // RemotePeerConfig represents a configuration of a remote peer. // The properties are used to configure Wireguard Peers sections type RemotePeerConfig struct { @@ -1343,6 +1352,94 @@ func (x *ProviderConfig) GetAudience() string { return "" } +// Route represents a route.Route object +type Route struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ID string `protobuf:"bytes,1,opt,name=ID,proto3" json:"ID,omitempty"` + Prefix string `protobuf:"bytes,2,opt,name=Prefix,proto3" json:"Prefix,omitempty"` + PrefixType int64 `protobuf:"varint,3,opt,name=PrefixType,proto3" json:"PrefixType,omitempty"` + Peer string `protobuf:"bytes,4,opt,name=Peer,proto3" json:"Peer,omitempty"` + Metric int64 `protobuf:"varint,5,opt,name=Metric,proto3" json:"Metric,omitempty"` + Masquerade bool `protobuf:"varint,6,opt,name=Masquerade,proto3" json:"Masquerade,omitempty"` +} + +func (x *Route) Reset() { + *x = Route{} + if protoimpl.UnsafeEnabled { + mi := &file_management_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Route) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Route) ProtoMessage() {} + +func (x *Route) ProtoReflect() protoreflect.Message { + mi := &file_management_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Route.ProtoReflect.Descriptor instead. +func (*Route) Descriptor() ([]byte, []int) { + return file_management_proto_rawDescGZIP(), []int{19} +} + +func (x *Route) GetID() string { + if x != nil { + return x.ID + } + return "" +} + +func (x *Route) GetPrefix() string { + if x != nil { + return x.Prefix + } + return "" +} + +func (x *Route) GetPrefixType() int64 { + if x != nil { + return x.PrefixType + } + return 0 +} + +func (x *Route) GetPeer() string { + if x != nil { + return x.Peer + } + return "" +} + +func (x *Route) GetMetric() int64 { + if x != nil { + return x.Metric + } + return 0 +} + +func (x *Route) GetMasquerade() bool { + if x != nil { + return x.Masquerade + } + return false +} + var File_management_proto protoreflect.FileDescriptor var file_management_proto_rawDesc = []byte{ @@ -1459,7 +1556,7 @@ var file_management_proto_rawDesc = []byte{ 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x22, 0xcc, 0x01, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x66, 0x69, 0x67, 0x22, 0xf7, 0x01, 0x0a, 0x0a, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x53, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, @@ -1472,67 +1569,80 @@ var file_management_proto_rawDesc = []byte{ 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x73, 0x49, 0x73, 0x45, 0x6d, 0x70, - 0x74, 0x79, 0x22, 0x83, 0x01, 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, - 0x4b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, - 0x4b, 0x65, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, - 0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, - 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, - 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, - 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, - 0x4b, 0x65, 0x79, 0x22, 0x20, 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, - 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, - 0x77, 0x12, 0x48, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x2c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, - 0x72, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, - 0x16, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, - 0x4f, 0x53, 0x54, 0x45, 0x44, 0x10, 0x00, 0x22, 0x84, 0x01, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x49, 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, - 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, - 0x69, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x32, 0xf7, - 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, - 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, - 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, - 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, - 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, - 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, + 0x74, 0x79, 0x12, 0x29, 0x0a, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x52, 0x06, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x83, 0x01, + 0x0a, 0x10, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x50, 0x65, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x67, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x1e, + 0x0a, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x49, 0x70, 0x73, 0x12, 0x33, + 0x0a, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, + 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x09, 0x73, 0x73, 0x68, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x22, 0x49, 0x0a, 0x09, 0x53, 0x53, 0x48, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x73, 0x73, 0x68, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x20, + 0x0a, 0x1e, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0xbf, 0x01, 0x0a, 0x17, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x48, 0x0a, 0x08, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x52, 0x08, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x16, 0x0a, 0x08, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x4f, 0x53, 0x54, 0x45, 0x44, + 0x10, 0x00, 0x22, 0x84, 0x01, 0x0a, 0x0e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, + 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, + 0x44, 0x12, 0x22, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x1a, 0x0a, + 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x41, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x22, 0x9b, 0x01, 0x0a, 0x05, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x44, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x49, 0x44, 0x12, 0x16, 0x0a, 0x06, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x1e, 0x0a, 0x0a, 0x50, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0a, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, + 0x65, 0x65, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x65, 0x65, 0x72, 0x12, + 0x16, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x1e, 0x0a, 0x0a, 0x4d, 0x61, 0x73, 0x71, 0x75, + 0x65, 0x72, 0x61, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x4d, 0x61, 0x73, + 0x71, 0x75, 0x65, 0x72, 0x61, 0x64, 0x65, 0x32, 0xf7, 0x02, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, + 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, + 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, + 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, + 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, + 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, + 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, + 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, - 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, - 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, - 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, - 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -1548,7 +1658,7 @@ func file_management_proto_rawDescGZIP() []byte { } var file_management_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 19) +var file_management_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_management_proto_goTypes = []interface{}{ (HostConfig_Protocol)(0), // 0: management.HostConfig.Protocol (DeviceAuthorizationFlowProvider)(0), // 1: management.DeviceAuthorizationFlow.provider @@ -1571,7 +1681,8 @@ var file_management_proto_goTypes = []interface{}{ (*DeviceAuthorizationFlowRequest)(nil), // 18: management.DeviceAuthorizationFlowRequest (*DeviceAuthorizationFlow)(nil), // 19: management.DeviceAuthorizationFlow (*ProviderConfig)(nil), // 20: management.ProviderConfig - (*timestamp.Timestamp)(nil), // 21: google.protobuf.Timestamp + (*Route)(nil), // 21: management.Route + (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp } var file_management_proto_depIdxs = []int32{ 11, // 0: management.SyncResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig @@ -1582,7 +1693,7 @@ var file_management_proto_depIdxs = []int32{ 6, // 5: management.LoginRequest.peerKeys:type_name -> management.PeerKeys 11, // 6: management.LoginResponse.wiretrusteeConfig:type_name -> management.WiretrusteeConfig 14, // 7: management.LoginResponse.peerConfig:type_name -> management.PeerConfig - 21, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp + 22, // 8: management.ServerKeyResponse.expiresAt:type_name -> google.protobuf.Timestamp 12, // 9: management.WiretrusteeConfig.stuns:type_name -> management.HostConfig 13, // 10: management.WiretrusteeConfig.turns:type_name -> management.ProtectedHostConfig 12, // 11: management.WiretrusteeConfig.signal:type_name -> management.HostConfig @@ -1591,24 +1702,25 @@ var file_management_proto_depIdxs = []int32{ 17, // 14: management.PeerConfig.sshConfig:type_name -> management.SSHConfig 14, // 15: management.NetworkMap.peerConfig:type_name -> management.PeerConfig 16, // 16: management.NetworkMap.remotePeers:type_name -> management.RemotePeerConfig - 17, // 17: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig - 1, // 18: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider - 20, // 19: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig - 2, // 20: management.ManagementService.Login:input_type -> management.EncryptedMessage - 2, // 21: management.ManagementService.Sync:input_type -> management.EncryptedMessage - 10, // 22: management.ManagementService.GetServerKey:input_type -> management.Empty - 10, // 23: management.ManagementService.isHealthy:input_type -> management.Empty - 2, // 24: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage - 2, // 25: management.ManagementService.Login:output_type -> management.EncryptedMessage - 2, // 26: management.ManagementService.Sync:output_type -> management.EncryptedMessage - 9, // 27: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse - 10, // 28: management.ManagementService.isHealthy:output_type -> management.Empty - 2, // 29: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage - 25, // [25:30] is the sub-list for method output_type - 20, // [20:25] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 21, // 17: management.NetworkMap.Routes:type_name -> management.Route + 17, // 18: management.RemotePeerConfig.sshConfig:type_name -> management.SSHConfig + 1, // 19: management.DeviceAuthorizationFlow.Provider:type_name -> management.DeviceAuthorizationFlow.provider + 20, // 20: management.DeviceAuthorizationFlow.ProviderConfig:type_name -> management.ProviderConfig + 2, // 21: management.ManagementService.Login:input_type -> management.EncryptedMessage + 2, // 22: management.ManagementService.Sync:input_type -> management.EncryptedMessage + 10, // 23: management.ManagementService.GetServerKey:input_type -> management.Empty + 10, // 24: management.ManagementService.isHealthy:input_type -> management.Empty + 2, // 25: management.ManagementService.GetDeviceAuthorizationFlow:input_type -> management.EncryptedMessage + 2, // 26: management.ManagementService.Login:output_type -> management.EncryptedMessage + 2, // 27: management.ManagementService.Sync:output_type -> management.EncryptedMessage + 9, // 28: management.ManagementService.GetServerKey:output_type -> management.ServerKeyResponse + 10, // 29: management.ManagementService.isHealthy:output_type -> management.Empty + 2, // 30: management.ManagementService.GetDeviceAuthorizationFlow:output_type -> management.EncryptedMessage + 26, // [26:31] is the sub-list for method output_type + 21, // [21:26] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_management_proto_init() } @@ -1845,6 +1957,18 @@ func file_management_proto_init() { return nil } } + file_management_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Route); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -1852,7 +1976,7 @@ func file_management_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_management_proto_rawDesc, NumEnums: 2, - NumMessages: 19, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, diff --git a/management/proto/management.proto b/management/proto/management.proto index b9522878a..010c1c2ef 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -176,6 +176,8 @@ message NetworkMap { // Indicates whether remotePeers array is empty or not to bypass protobuf null and empty array equality. bool remotePeersIsEmpty = 4; + // List of routes to be applied + repeated Route Routes = 5; } // RemotePeerConfig represents a configuration of a remote peer. @@ -229,3 +231,13 @@ message ProviderConfig { // An Audience for validation string Audience = 4; } + +// Route represents a route.Route object +message Route { + string ID = 1; + string Prefix = 2; + int64 PrefixType = 3; + string Peer = 4; + int64 Metric = 5; + bool Masquerade = 6; +} \ No newline at end of file diff --git a/management/server/account.go b/management/server/account.go index d2aae927a..5a727a9b1 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -7,6 +7,7 @@ import ( cacheStore "github.com/eko/gocache/v2/store" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/route" gocache "github.com/patrickmn/go-cache" "github.com/rs/xid" log "github.com/sirupsen/logrus" @@ -48,6 +49,7 @@ type AccountManager interface { RenamePeer(accountId string, peerKey string, newName string) (*Peer, error) DeletePeer(accountId string, peerKey string) (*Peer, error) GetPeerByIP(accountId string, peerIP string) (*Peer, error) + UpdatePeer(accountID string, peer *Peer) (*Peer, error) GetNetworkMap(peerKey string) (*NetworkMap, error) GetPeerNetwork(peerKey string) (*Network, error) AddPeer(setupKey string, userId string, peer *Peer) (*Peer, error) @@ -67,7 +69,12 @@ type AccountManager interface { UpdateRule(accountID string, ruleID string, operations []RuleUpdateOperation) (*Rule, error) DeleteRule(accountId, ruleID string) error ListRules(accountId string) ([]*Rule, error) - UpdatePeer(accountID string, peer *Peer) (*Peer, error) + GetRoute(accountID, routeID string) (*route.Route, error) + CreateRoute(accountID string, prefix, peer, description string, masquerade bool, metric int, enabled bool) (*route.Route, error) + SaveRoute(accountID string, route *route.Route) error + UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error) + DeleteRoute(accountID, routeID string) error + ListRoutes(accountID string) ([]*route.Route, error) } type DefaultAccountManager struct { @@ -94,6 +101,7 @@ type Account struct { Users map[string]*User Groups map[string]*Group Rules map[string]*Rule + Routes map[string]*route.Route } type UserInfo struct { @@ -686,6 +694,7 @@ func newAccountWithId(accountId, userId, domain string) *Account { network := NewNetwork() peers := make(map[string]*Peer) users := make(map[string]*User) + routes := make(map[string]*route.Route) users[userId] = NewAdminUser(userId) log.Debugf("created new account %s with setup key %s", accountId, defaultKey.Key) @@ -697,6 +706,7 @@ func newAccountWithId(accountId, userId, domain string) *Account { Users: users, CreatedBy: userId, Domain: domain, + Routes: routes, } addAllGroup(acc) diff --git a/management/server/file_store.go b/management/server/file_store.go index bb108514b..4d4101e44 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -2,6 +2,8 @@ package server import ( "fmt" + "github.com/netbirdio/netbird/route" + "net/netip" "os" "path/filepath" "strings" @@ -25,6 +27,8 @@ type FileStore struct { PrivateDomain2AccountId map[string]string `json:"-"` PeerKeyId2SrcRulesId map[string]map[string]struct{} `json:"-"` PeerKeyId2DstRulesId map[string]map[string]struct{} `json:"-"` + PeerKeyID2RouteIDs map[string]map[string]struct{} `json:"-"` + AccountPrefix2RouteIDs map[string]map[string][]string `json:"-"` // mutex to synchronise Store read/write operations mux sync.Mutex `json:"-"` @@ -51,7 +55,9 @@ func restore(file string) (*FileStore, error) { UserId2AccountId: make(map[string]string), PrivateDomain2AccountId: make(map[string]string), PeerKeyId2SrcRulesId: make(map[string]map[string]struct{}), + PeerKeyID2RouteIDs: make(map[string]map[string]struct{}), PeerKeyId2DstRulesId: make(map[string]map[string]struct{}), + AccountPrefix2RouteIDs: make(map[string]map[string][]string), storeFile: file, } @@ -74,8 +80,10 @@ func restore(file string) (*FileStore, error) { store.PeerKeyId2AccountId = make(map[string]string) store.UserId2AccountId = make(map[string]string) store.PrivateDomain2AccountId = make(map[string]string) - store.PeerKeyId2SrcRulesId = map[string]map[string]struct{}{} - store.PeerKeyId2DstRulesId = map[string]map[string]struct{}{} + store.PeerKeyId2SrcRulesId = make(map[string]map[string]struct{}) + store.PeerKeyId2DstRulesId = make(map[string]map[string]struct{}) + store.PeerKeyID2RouteIDs = make(map[string]map[string]struct{}) + store.AccountPrefix2RouteIDs = make(map[string]map[string][]string) for accountId, account := range store.Accounts { for setupKeyId := range account.SetupKeys { @@ -116,6 +124,24 @@ func restore(file string) (*FileStore, error) { for _, user := range account.Users { store.UserId2AccountId[user.Id] = accountId } + for _, route := range account.Routes { + if route.Peer != "" { + if store.PeerKeyID2RouteIDs[route.Peer] == nil { + store.PeerKeyID2RouteIDs[route.Peer] = make(map[string]struct{}) + } + store.PeerKeyID2RouteIDs[route.Peer][route.ID] = struct{}{} + } + if store.AccountPrefix2RouteIDs[account.Id] == nil { + store.AccountPrefix2RouteIDs[account.Id] = make(map[string][]string) + } + if _, ok := store.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()]; !ok { + store.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()] = make([]string, 0) + } + store.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()] = append( + store.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()], + route.ID, + ) + } if account.Domain != "" && account.DomainCategory == PrivateCategory && account.IsDomainPrimaryAccount { store.PrivateDomain2AccountId[account.Domain] = accountId @@ -177,11 +203,12 @@ func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error) if peer == nil { return nil, status.Errorf(codes.NotFound, "peer not found") } - + peerRoutes := s.PeerKeyID2RouteIDs[peerKey] delete(account.Peers, peerKey) delete(s.PeerKeyId2AccountId, peerKey) delete(s.PeerKeyId2DstRulesId, peerKey) delete(s.PeerKeyId2SrcRulesId, peerKey) + delete(s.PeerKeyID2RouteIDs, peerKey) // cleanup groups for _, g := range account.Groups { @@ -194,6 +221,11 @@ func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error) g.Peers = peers } + for routeID := range peerRoutes { + account.Routes[routeID].Enabled = false + account.Routes[routeID].Peer = "" + } + err = s.persist(s.storeFile) if err != nil { return nil, err @@ -238,10 +270,14 @@ func (s *FileStore) SaveAccount(account *Account) error { s.SetupKeyId2AccountId[strings.ToUpper(keyId)] = account.Id } + // enforce peer to account index and delete peer to route indexes for rebuild for _, peer := range account.Peers { s.PeerKeyId2AccountId[peer.Key] = account.Id + delete(s.PeerKeyID2RouteIDs, peer.Key) } + delete(s.AccountPrefix2RouteIDs, account.Id) + // remove all peers related to account from rules indexes cleanIDs := make([]string, 0) for key := range s.PeerKeyId2SrcRulesId { @@ -294,6 +330,25 @@ func (s *FileStore) SaveAccount(account *Account) error { } } + for _, route := range account.Routes { + if route.Peer != "" { + if s.PeerKeyID2RouteIDs[route.Peer] == nil { + s.PeerKeyID2RouteIDs[route.Peer] = make(map[string]struct{}) + } + s.PeerKeyID2RouteIDs[route.Peer][route.ID] = struct{}{} + } + if s.AccountPrefix2RouteIDs[account.Id] == nil { + s.AccountPrefix2RouteIDs[account.Id] = make(map[string][]string) + } + if _, ok := s.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()]; !ok { + s.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()] = make([]string, 0) + } + s.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()] = append( + s.AccountPrefix2RouteIDs[account.Id][route.Prefix.String()], + route.ID, + ) + } + for _, user := range account.Users { s.UserId2AccountId[user.Id] = account.Id } @@ -305,6 +360,7 @@ func (s *FileStore) SaveAccount(account *Account) error { return s.persist(s.storeFile) } +// GetAccountByPrivateDomain returns account by private domain func (s *FileStore) GetAccountByPrivateDomain(domain string) (*Account, error) { accountId, accountIdFound := s.PrivateDomain2AccountId[strings.ToLower(domain)] if !accountIdFound { @@ -322,6 +378,7 @@ func (s *FileStore) GetAccountByPrivateDomain(domain string) (*Account, error) { return account, nil } +// GetAccountBySetupKey returns account by setup key id func (s *FileStore) GetAccountBySetupKey(setupKey string) (*Account, error) { accountId, accountIdFound := s.SetupKeyId2AccountId[strings.ToUpper(setupKey)] if !accountIdFound { @@ -336,6 +393,7 @@ func (s *FileStore) GetAccountBySetupKey(setupKey string) (*Account, error) { return account, nil } +// GetAccountPeers returns account peers func (s *FileStore) GetAccountPeers(accountId string) ([]*Peer, error) { s.mux.Lock() defer s.mux.Unlock() @@ -353,6 +411,7 @@ func (s *FileStore) GetAccountPeers(accountId string) ([]*Peer, error) { return peers, nil } +// GetAllAccounts returns all accounts func (s *FileStore) GetAllAccounts() (all []*Account) { for _, a := range s.Accounts { all = append(all, a) @@ -361,6 +420,7 @@ func (s *FileStore) GetAllAccounts() (all []*Account) { return all } +// GetAccount returns an account for id func (s *FileStore) GetAccount(accountId string) (*Account, error) { account, accountFound := s.Accounts[accountId] if !accountFound { @@ -370,6 +430,7 @@ func (s *FileStore) GetAccount(accountId string) (*Account, error) { return account, nil } +// GetUserAccount returns a user account func (s *FileStore) GetUserAccount(userId string) (*Account, error) { s.mux.Lock() defer s.mux.Unlock() @@ -382,10 +443,7 @@ func (s *FileStore) GetUserAccount(userId string) (*Account, error) { return s.GetAccount(accountId) } -func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) { - s.mux.Lock() - defer s.mux.Unlock() - +func (s *FileStore) getPeerAccount(peerKey string) (*Account, error) { accountId, accountIdFound := s.PeerKeyId2AccountId[peerKey] if !accountIdFound { return nil, status.Errorf(codes.NotFound, "Provided peer key doesn't exists %s", peerKey) @@ -394,6 +452,15 @@ func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) { return s.GetAccount(accountId) } +// GetPeerAccount returns user account if exists +func (s *FileStore) GetPeerAccount(peerKey string) (*Account, error) { + s.mux.Lock() + defer s.mux.Unlock() + + return s.getPeerAccount(peerKey) +} + +// GetPeerSrcRules return list of source rules for peer func (s *FileStore) GetPeerSrcRules(accountId, peerKey string) ([]*Rule, error) { s.mux.Lock() defer s.mux.Unlock() @@ -419,6 +486,7 @@ func (s *FileStore) GetPeerSrcRules(accountId, peerKey string) ([]*Rule, error) return rules, nil } +// GetPeerDstRules return list of destination rules for peer func (s *FileStore) GetPeerDstRules(accountId, peerKey string) ([]*Rule, error) { s.mux.Lock() defer s.mux.Unlock() @@ -443,3 +511,56 @@ func (s *FileStore) GetPeerDstRules(accountId, peerKey string) ([]*Rule, error) return rules, nil } + +// GetPeerRoutes return list of routes for peer +func (s *FileStore) GetPeerRoutes(peerKey string) ([]*route.Route, error) { + s.mux.Lock() + defer s.mux.Unlock() + + account, err := s.getPeerAccount(peerKey) + if err != nil { + return nil, err + } + + var routes []*route.Route + + routeIDs, ok := s.PeerKeyID2RouteIDs[peerKey] + if !ok { + return routes, nil + } + + for id := range routeIDs { + route, found := account.Routes[id] + if found { + routes = append(routes, route) + } + } + + return routes, nil +} + +// GetRoutesByPrefix return list of routes by account and route prefix +func (s *FileStore) GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]*route.Route, error) { + s.mux.Lock() + defer s.mux.Unlock() + + account, err := s.GetAccount(accountID) + if err != nil { + return nil, err + } + + routeIDs, ok := s.AccountPrefix2RouteIDs[accountID][prefix.String()] + if !ok { + return nil, status.Errorf(codes.NotFound, "no routes for prefix: %v", prefix.String()) + } + + var routes []*route.Route + for _, id := range routeIDs { + route, found := account.Routes[id] + if found { + routes = append(routes, route) + } + } + + return routes, nil +} diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index ed211f063..e11f47302 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -3,6 +3,7 @@ package server import ( "context" "fmt" + "github.com/netbirdio/netbird/route" "strings" "time" @@ -230,7 +231,7 @@ func (s *GRPCServer) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) peersToSend = append(peersToSend, p) } } - update := toSyncResponse(s.config, remotePeer, peersToSend, nil, networkMap.Network.CurrentSerial(), networkMap.Network) + update := toSyncResponse(s.config, remotePeer, peersToSend, networkMap.Routes, nil, networkMap.Network.CurrentSerial(), networkMap.Network) err = s.peersUpdateManager.SendUpdate(remotePeer.Key, &UpdateMessage{Update: update}) if err != nil { // todo rethink if we should keep this return @@ -407,13 +408,15 @@ func toRemotePeerConfig(peers []*Peer) []*proto.RemotePeerConfig { return remotePeers } -func toSyncResponse(config *Config, peer *Peer, peers []*Peer, turnCredentials *TURNCredentials, serial uint64, network *Network) *proto.SyncResponse { +func toSyncResponse(config *Config, peer *Peer, peers []*Peer, routes []*route.Route, turnCredentials *TURNCredentials, serial uint64, network *Network) *proto.SyncResponse { wtConfig := toWiretrusteeConfig(config, turnCredentials) pConfig := toPeerConfig(peer, network) remotePeers := toRemotePeerConfig(peers) + routesUpdate := toProtocolRoutes(routes) + return &proto.SyncResponse{ WiretrusteeConfig: wtConfig, PeerConfig: pConfig, @@ -424,6 +427,7 @@ func toSyncResponse(config *Config, peer *Peer, peers []*Peer, turnCredentials * PeerConfig: pConfig, RemotePeers: remotePeers, RemotePeersIsEmpty: len(remotePeers) == 0, + Routes: routesUpdate, }, } } @@ -449,7 +453,7 @@ func (s *GRPCServer) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto. } else { turnCredentials = nil } - plainResp := toSyncResponse(s.config, peer, networkMap.Peers, turnCredentials, networkMap.Network.CurrentSerial(), networkMap.Network) + plainResp := toSyncResponse(s.config, peer, networkMap.Peers, networkMap.Routes, turnCredentials, networkMap.Network.CurrentSerial(), networkMap.Network) encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp) if err != nil { diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 587e7c54b..6e3118dea 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -3,6 +3,7 @@ package mock_server import ( "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/jwtclaims" + "github.com/netbirdio/netbird/route" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "time" @@ -44,6 +45,12 @@ type MockAccountManager struct { UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) error UpdatePeerSSHKeyFunc func(peerKey string, sshKey string) error UpdatePeerFunc func(accountID string, peer *server.Peer) (*server.Peer, error) + CreateRouteFunc func(accountID string, prefix, peer, description string, masquerade bool, metric int, enabled bool) (*route.Route, error) + GetRouteFunc func(accountID, routeID string) (*route.Route, error) + SaveRouteFunc func(accountID string, route *route.Route) error + UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error) + DeleteRouteFunc func(accountID, routeID string) error + ListRoutesFunc func(accountID string) ([]*route.Route, error) } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -360,3 +367,51 @@ func (am *MockAccountManager) UpdatePeer(accountID string, peer *server.Peer) (* } return nil, status.Errorf(codes.Unimplemented, "method UpdatePeerFunc is is not implemented") } + +// CreateRoute mock implementation of CreateRoute from server.AccountManager interface +func (am *MockAccountManager) CreateRoute(accountID string, prefix, peer, description string, masquerade bool, metric int, enabled bool) (*route.Route, error) { + if am.GetRouteFunc != nil { + return am.CreateRouteFunc(accountID, prefix, peer, description, masquerade, metric, enabled) + } + return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented") +} + +// GetRoute mock implementation of GetRoute from server.AccountManager interface +func (am *MockAccountManager) GetRoute(accountID, routeID string) (*route.Route, error) { + if am.GetRouteFunc != nil { + return am.GetRouteFunc(accountID, routeID) + } + return nil, status.Errorf(codes.Unimplemented, "method GetRoute is not implemented") +} + +// SaveRoute mock implementation of SaveRoute from server.AccountManager interface +func (am *MockAccountManager) SaveRoute(accountID string, route *route.Route) error { + if am.SaveRouteFunc != nil { + return am.SaveRouteFunc(accountID, route) + } + return status.Errorf(codes.Unimplemented, "method SaveRoute is not implemented") +} + +// UpdateRoute mock implementation of UpdateRoute from server.AccountManager interface +func (am *MockAccountManager) UpdateRoute(accountID string, ruleID string, operations []server.RouteUpdateOperation) (*route.Route, error) { + if am.UpdateRouteFunc != nil { + return am.UpdateRouteFunc(accountID, ruleID, operations) + } + return nil, status.Errorf(codes.Unimplemented, "method UpdateRoute not implemented") +} + +// DeleteRoute mock implementation of DeleteRoute from server.AccountManager interface +func (am *MockAccountManager) DeleteRoute(accountID, routeID string) error { + if am.DeleteRouteFunc != nil { + return am.DeleteRouteFunc(accountID, routeID) + } + return status.Errorf(codes.Unimplemented, "method DeleteRoute is not implemented") +} + +// ListRoutes mock implementation of ListRoutes from server.AccountManager interface +func (am *MockAccountManager) ListRoutes(accountID string) ([]*route.Route, error) { + if am.ListRoutesFunc != nil { + return am.ListRoutesFunc(accountID) + } + return nil, status.Errorf(codes.Unimplemented, "method ListRoutes is not implemented") +} diff --git a/management/server/network.go b/management/server/network.go index 82fe5fc83..b9b15b563 100644 --- a/management/server/network.go +++ b/management/server/network.go @@ -2,6 +2,7 @@ package server import ( "github.com/c-robinson/iplib" + "github.com/netbirdio/netbird/route" "github.com/rs/xid" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -24,6 +25,7 @@ const ( type NetworkMap struct { Peers []*Peer Network *Network + Routes []*route.Route } type Network struct { diff --git a/management/server/peer.go b/management/server/peer.go index 1b078abfc..e026c4c61 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -251,9 +251,13 @@ func (am *DefaultAccountManager) GetNetworkMap(peerKey string) (*NetworkMap, err return nil, status.Errorf(codes.Internal, "Invalid peer key %s", peerKey) } + aclPeers := am.getPeersByACL(account, peerKey) + routesUpdate := am.getPeersRoutes(append(aclPeers, account.Peers[peerKey])) + return &NetworkMap{ - Peers: am.getPeersByACL(account, peerKey), + Peers: aclPeers, Network: account.Network.Copy(), + Routes: routesUpdate, }, err } @@ -508,20 +512,23 @@ func (am *DefaultAccountManager) updateAccountPeers(account *Account) error { network := account.Network.Copy() - for _, p := range peers { - update := toRemotePeerConfig(am.getPeersByACL(account, p.Key)) - err = am.peersUpdateManager.SendUpdate(p.Key, + for _, peer := range peers { + aclPeers := am.getPeersByACL(account, peer.Key) + peersUpdate := toRemotePeerConfig(aclPeers) + routesUpdate := toProtocolRoutes(am.getPeersRoutes(append(aclPeers, peer))) + err = am.peersUpdateManager.SendUpdate(peer.Key, &UpdateMessage{ Update: &proto.SyncResponse{ // fill deprecated fields for backward compatibility - RemotePeers: update, - RemotePeersIsEmpty: len(update) == 0, + RemotePeers: peersUpdate, + RemotePeersIsEmpty: len(peersUpdate) == 0, // new field NetworkMap: &proto.NetworkMap{ Serial: account.Network.CurrentSerial(), - RemotePeers: update, - RemotePeersIsEmpty: len(update) == 0, - PeerConfig: toPeerConfig(p, network), + RemotePeers: peersUpdate, + RemotePeersIsEmpty: len(peersUpdate) == 0, + PeerConfig: toPeerConfig(peer, network), + Routes: routesUpdate, }, }, }) diff --git a/management/server/route.go b/management/server/route.go new file mode 100644 index 000000000..f8b4ddffe --- /dev/null +++ b/management/server/route.go @@ -0,0 +1,362 @@ +package server + +import ( + "github.com/netbirdio/netbird/management/proto" + "github.com/netbirdio/netbird/route" + "github.com/rs/xid" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "net/netip" + "strconv" +) + +const ( + // UpdateRouteDescription indicates a route description update operation + UpdateRouteDescription RouteUpdateOperationType = iota + // UpdateRoutePrefix indicates a route IP update operation + UpdateRoutePrefix + // UpdateRoutePeer indicates a route peer update operation + UpdateRoutePeer + // UpdateRouteMetric indicates a route metric update operation + UpdateRouteMetric + // UpdateRouteMasquerade indicates a route masquerade update operation + UpdateRouteMasquerade + // UpdateRouteEnabled indicates a route enabled update operation + UpdateRouteEnabled +) + +// RouteUpdateOperationType operation type +type RouteUpdateOperationType int + +func (t RouteUpdateOperationType) String() string { + switch t { + case UpdateRouteDescription: + return "UpdateRouteDescription" + case UpdateRoutePrefix: + return "UpdateRoutePrefix" + case UpdateRoutePeer: + return "UpdateRoutePeer" + case UpdateRouteMetric: + return "UpdateRouteMetric" + case UpdateRouteMasquerade: + return "UpdateRouteMasquerade" + case UpdateRouteEnabled: + return "UpdateRouteEnabled" + default: + return "InvalidOperation" + } +} + +// RouteUpdateOperation operation object with type and values to be applied +type RouteUpdateOperation struct { + Type RouteUpdateOperationType + Values []string +} + +// GetRoute gets a route object from account and route IDs +func (am *DefaultAccountManager) GetRoute(accountID, routeID string) (*route.Route, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + wantedRoute, found := account.Routes[routeID] + if found { + return wantedRoute, nil + } + + return nil, status.Errorf(codes.NotFound, "route with ID %s not found", routeID) +} + +// checkPrefixPeerExists checks the combination of prefix and peer id, if it exists returns an error, otehrwise returns nil +func (am *DefaultAccountManager) checkPrefixPeerExists(accountID, peer string, prefix netip.Prefix) error { + routesWithPrefix, err := am.Store.GetRoutesByPrefix(accountID, prefix) + + if err != nil { + if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound { + return nil + } + return status.Errorf(codes.InvalidArgument, "failed to parse prefix %s", prefix.String()) + } + for _, prefixRoute := range routesWithPrefix { + if prefixRoute.Peer == peer { + return status.Errorf(codes.AlreadyExists, "failed a route with prefix %s and peer already exist", prefix.String()) + } + } + return nil +} + +// CreateRoute creates and saves a new route +func (am *DefaultAccountManager) CreateRoute(accountID string, prefix, peer, description string, masquerade bool, metric int, enabled bool) (*route.Route, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + var newRoute route.Route + prefixType, newPrefix, err := route.ParsePrefix(prefix) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse IP %s", prefix) + } + err = am.checkPrefixPeerExists(accountID, peer, newPrefix) + if err != nil { + return nil, err + } + + if peer != "" { + _, peerExist := account.Peers[peer] + if !peerExist { + return nil, status.Errorf(codes.InvalidArgument, "failed to find Peer %s", peer) + } + } + + if metric < route.MinMetric || metric > route.MaxMetric { + return nil, status.Errorf(codes.InvalidArgument, "metric should be between %d and %d", route.MinMetric, route.MaxMetric) + } + + newRoute.Peer = peer + newRoute.ID = xid.New().String() + newRoute.Prefix = newPrefix + newRoute.PrefixType = prefixType + newRoute.Description = description + newRoute.Masquerade = masquerade + newRoute.Metric = metric + newRoute.Enabled = enabled + + if account.Routes == nil { + account.Routes = make(map[string]*route.Route) + } + + account.Routes[newRoute.ID] = &newRoute + + account.Network.IncSerial() + if err = am.Store.SaveAccount(account); err != nil { + return nil, err + } + + err = am.updateAccountPeers(account) + if err != nil { + log.Error(err) + return &newRoute, status.Errorf(codes.Unavailable, "failed to update peers after create route %s", newPrefix) + } + return &newRoute, nil +} + +// SaveRoute saves route +func (am *DefaultAccountManager) SaveRoute(accountID string, routeToSave *route.Route) error { + am.mux.Lock() + defer am.mux.Unlock() + + if routeToSave == nil { + return status.Errorf(codes.InvalidArgument, "route provided is nil") + } + + if !routeToSave.Prefix.IsValid() { + return status.Errorf(codes.InvalidArgument, "invalid Prefix %s", routeToSave.Prefix.String()) + } + + if routeToSave.Metric < route.MinMetric || routeToSave.Metric > route.MaxMetric { + return status.Errorf(codes.InvalidArgument, "metric should be between %d and %d", route.MinMetric, route.MaxMetric) + } + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return status.Errorf(codes.NotFound, "account not found") + } + + if routeToSave.Peer != "" { + _, peerExist := account.Peers[routeToSave.Peer] + if !peerExist { + return status.Errorf(codes.InvalidArgument, "failed to find Peer %s", routeToSave.Peer) + } + } + + account.Routes[routeToSave.ID] = routeToSave + + account.Network.IncSerial() + if err = am.Store.SaveAccount(account); err != nil { + return err + } + + return am.updateAccountPeers(account) +} + +// UpdateRoute updates existing route with set of operations +func (am *DefaultAccountManager) UpdateRoute(accountID, routeID string, operations []RouteUpdateOperation) (*route.Route, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + routeToUpdate, ok := account.Routes[routeID] + if !ok { + return nil, status.Errorf(codes.NotFound, "route %s no longer exists", routeID) + } + + newRoute := routeToUpdate.Copy() + + for _, operation := range operations { + + if len(operation.Values) != 1 { + return nil, status.Errorf(codes.InvalidArgument, "operation %s contains invalid number of values, it should be 1", operation.Type.String()) + } + + switch operation.Type { + case UpdateRouteDescription: + newRoute.Description = operation.Values[0] + case UpdateRoutePrefix: + prefixType, prefix, err := route.ParsePrefix(operation.Values[0]) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse IP %s", operation.Values[0]) + } + err = am.checkPrefixPeerExists(accountID, routeToUpdate.Peer, prefix) + if err != nil { + return nil, err + } + newRoute.Prefix = prefix + newRoute.PrefixType = prefixType + case UpdateRoutePeer: + if operation.Values[0] != "" { + _, peerExist := account.Peers[operation.Values[0]] + if !peerExist { + return nil, status.Errorf(codes.InvalidArgument, "failed to find Peer %s", operation.Values[0]) + } + } + + err = am.checkPrefixPeerExists(accountID, operation.Values[0], routeToUpdate.Prefix) + if err != nil { + return nil, err + } + newRoute.Peer = operation.Values[0] + case UpdateRouteMetric: + metric, err := strconv.Atoi(operation.Values[0]) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse metric %s, not int", operation.Values[0]) + } + if metric < route.MinMetric || metric > route.MaxMetric { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse metric %s, value should be %d > N < %d", + operation.Values[0], + route.MinMetric, + route.MaxMetric, + ) + } + newRoute.Metric = metric + case UpdateRouteMasquerade: + masquerade, err := strconv.ParseBool(operation.Values[0]) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse masquerade %s, not boolean", operation.Values[0]) + } + newRoute.Masquerade = masquerade + case UpdateRouteEnabled: + enabled, err := strconv.ParseBool(operation.Values[0]) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse enabled %s, not boolean", operation.Values[0]) + } + newRoute.Enabled = enabled + } + } + + account.Routes[routeID] = newRoute + + account.Network.IncSerial() + if err = am.Store.SaveAccount(account); err != nil { + return nil, err + } + + err = am.updateAccountPeers(account) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update account peers") + } + return newRoute, nil +} + +// DeleteRoute deletes route with routeID +func (am *DefaultAccountManager) DeleteRoute(accountID, routeID string) error { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return status.Errorf(codes.NotFound, "account not found") + } + + delete(account.Routes, routeID) + + account.Network.IncSerial() + if err = am.Store.SaveAccount(account); err != nil { + return err + } + + return am.updateAccountPeers(account) +} + +// ListRoutes returns a list of routes from account +func (am *DefaultAccountManager) ListRoutes(accountID string) ([]*route.Route, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + routes := make([]*route.Route, 0, len(account.Routes)) + for _, item := range account.Routes { + routes = append(routes, item) + } + + return routes, nil +} + +func toProtocolRoute(route *route.Route) *proto.Route { + return &proto.Route{ + ID: route.ID, + Prefix: route.Prefix.String(), + PrefixType: int64(route.PrefixType), + Peer: route.Peer, + Metric: int64(route.Metric), + Masquerade: route.Masquerade, + } +} + +func (am *DefaultAccountManager) getPeersRoutes(peers []*Peer) []*route.Route { + routes := make([]*route.Route, 0) + for _, peer := range peers { + peerRoutes, err := am.Store.GetPeerRoutes(peer.Key) + if err != nil { + errorStatus, ok := status.FromError(err) + if !ok && errorStatus.Code() != codes.NotFound { + log.Error(err) + } + continue + } + activeRoutes := make([]*route.Route, 0) + for _, pr := range peerRoutes { + if pr.Enabled { + activeRoutes = append(activeRoutes, pr) + } + } + if len(activeRoutes) > 0 { + routes = append(routes, activeRoutes...) + } + } + return routes +} + +func toProtocolRoutes(routes []*route.Route) []*proto.Route { + protoRoutes := make([]*proto.Route, 0) + for _, r := range routes { + protoRoutes = append(protoRoutes, toProtocolRoute(r)) + } + return protoRoutes +} diff --git a/management/server/route_test.go b/management/server/route_test.go new file mode 100644 index 000000000..7dbe844db --- /dev/null +++ b/management/server/route_test.go @@ -0,0 +1,751 @@ +package server + +import ( + "github.com/netbirdio/netbird/route" + "github.com/rs/xid" + "github.com/stretchr/testify/require" + "net/netip" + "testing" +) + +const peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=" +const peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI=" + +func TestCreateRoute(t *testing.T) { + + type input struct { + prefix string + peer string + description string + masquerade bool + metric int + enabled bool + } + + testCases := []struct { + name string + inputArgs input + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedRoute *route.Route + }{ + { + name: "Happy Path", + inputArgs: input{ + prefix: "192.168.0.0/16", + peer: peer1Key, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + }, + { + name: "Bad Prefix", + inputArgs: input{ + prefix: "192.168.0.0/34", + peer: peer1Key, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Bad Peer", + inputArgs: input{ + prefix: "192.168.0.0/16", + peer: "notExistingPeer", + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Empty Peer", + inputArgs: input{ + prefix: "192.168.0.0/16", + peer: "", + description: "super", + masquerade: false, + metric: 9999, + enabled: false, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: "", + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: false, + }, + }, + { + name: "Large Metric", + inputArgs: input{ + prefix: "192.168.0.0/16", + peer: peer1Key, + description: "super", + masquerade: false, + metric: 99999, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Small Metric", + inputArgs: input{ + prefix: "192.168.0.0/16", + peer: peer1Key, + description: "super", + masquerade: false, + metric: 0, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + am, err := createRouterManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestRouteAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + outRoute, err := am.CreateRoute( + account.Id, + testCase.inputArgs.prefix, + testCase.inputArgs.peer, + testCase.inputArgs.description, + testCase.inputArgs.masquerade, + testCase.inputArgs.metric, + testCase.inputArgs.enabled, + ) + + testCase.errFunc(t, err) + + if !testCase.shouldCreate { + return + } + + // assign generated ID + testCase.expectedRoute.ID = outRoute.ID + + if !testCase.expectedRoute.IsEqual(outRoute) { + t.Errorf("new route didn't match expected route:\nGot %#v\nExpected:%#v\n", outRoute, testCase.expectedRoute) + } + + }) + } +} + +func TestSaveRoute(t *testing.T) { + + validPeer := peer2Key + invalidPeer := "nonExisting" + validPrefix := netip.MustParsePrefix("192.168.0.0/24") + invalidPrefix, _ := netip.ParsePrefix("192.168.0.0/34") + validMetric := 1000 + invalidMetric := 99999 + + testCases := []struct { + name string + existingRoute *route.Route + newPeer *string + newMetric *int + newPrefix *netip.Prefix + skipCopying bool + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedRoute *route.Route + }{ + { + name: "Happy Path", + existingRoute: &route.Route{ + ID: "testingRoute", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + newPeer: &validPeer, + newMetric: &validMetric, + newPrefix: &validPrefix, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: "testingRoute", + Prefix: validPrefix, + PrefixType: route.IPv4Prefix, + Peer: validPeer, + Description: "super", + Masquerade: false, + Metric: validMetric, + Enabled: true, + }, + }, + { + name: "Bad Prefix", + existingRoute: &route.Route{ + ID: "testingRoute", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + newPrefix: &invalidPrefix, + errFunc: require.Error, + }, + { + name: "Bad Peer", + existingRoute: &route.Route{ + ID: "testingRoute", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + newPeer: &invalidPeer, + errFunc: require.Error, + }, + { + name: "Invalid Metric", + existingRoute: &route.Route{ + ID: "testingRoute", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + newMetric: &invalidMetric, + errFunc: require.Error, + }, + { + name: "Nil Route", + existingRoute: &route.Route{ + ID: "testingRoute", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + skipCopying: true, + errFunc: require.Error, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + am, err := createRouterManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestRouteAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + account.Routes[testCase.existingRoute.ID] = testCase.existingRoute + + err = am.Store.SaveAccount(account) + if err != nil { + t.Error("account should be saved") + } + + var routeToSave *route.Route + + if !testCase.skipCopying { + routeToSave = testCase.existingRoute.Copy() + if testCase.newPeer != nil { + routeToSave.Peer = *testCase.newPeer + } + + if testCase.newMetric != nil { + routeToSave.Metric = *testCase.newMetric + } + + if testCase.newPrefix != nil { + routeToSave.Prefix = *testCase.newPrefix + } + } + + err = am.SaveRoute(account.Id, routeToSave) + + testCase.errFunc(t, err) + + if !testCase.shouldCreate { + return + } + + savedRoute, saved := account.Routes[testCase.expectedRoute.ID] + require.True(t, saved) + + if !testCase.expectedRoute.IsEqual(savedRoute) { + t.Errorf("new route didn't match expected route:\nGot %#v\nExpected:%#v\n", savedRoute, testCase.expectedRoute) + } + + }) + } +} + +func TestUpdateRoute(t *testing.T) { + routeID := "testingRouteID" + + existingRoute := &route.Route{ + ID: routeID, + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + } + + testCases := []struct { + name string + existingRoute *route.Route + operations []RouteUpdateOperation + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedRoute *route.Route + }{ + { + name: "Happy Path Single OPS", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRoutePeer, + Values: []string{peer2Key}, + }, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: routeID, + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer2Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + }, + { + name: "Happy Path Multiple OPS", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRouteDescription, + Values: []string{"great"}, + }, + RouteUpdateOperation{ + Type: UpdateRoutePrefix, + Values: []string{"192.168.0.0/24"}, + }, + RouteUpdateOperation{ + Type: UpdateRoutePeer, + Values: []string{peer2Key}, + }, + RouteUpdateOperation{ + Type: UpdateRouteMetric, + Values: []string{"3030"}, + }, + RouteUpdateOperation{ + Type: UpdateRouteMasquerade, + Values: []string{"true"}, + }, + RouteUpdateOperation{ + Type: UpdateRouteEnabled, + Values: []string{"false"}, + }, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: routeID, + Prefix: netip.MustParsePrefix("192.168.0.0/24"), + PrefixType: route.IPv4Prefix, + Peer: peer2Key, + Description: "great", + Masquerade: true, + Metric: 3030, + Enabled: false, + }, + }, + { + name: "Empty Values", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRoutePeer, + }, + }, + errFunc: require.Error, + }, + { + name: "Multiple Values", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRoutePeer, + Values: []string{peer2Key, peer1Key}, + }, + }, + errFunc: require.Error, + }, + { + name: "Bad Prefix", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRoutePrefix, + Values: []string{"192.168.0.0/34"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Bad Peer", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRoutePeer, + Values: []string{"non existing Peer"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Empty Peer", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRoutePeer, + Values: []string{""}, + }, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: routeID, + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: "", + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + }, + }, + { + name: "Invalid Metric", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRouteMetric, + Values: []string{"999999"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Invalid Boolean", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + RouteUpdateOperation{ + Type: UpdateRouteMasquerade, + Values: []string{"yes"}, + }, + }, + errFunc: require.Error, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + am, err := createRouterManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestRouteAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + account.Routes[testCase.existingRoute.ID] = testCase.existingRoute + + err = am.Store.SaveAccount(account) + if err != nil { + t.Error("account should be saved") + } + + updatedRoute, err := am.UpdateRoute(account.Id, testCase.existingRoute.ID, testCase.operations) + + testCase.errFunc(t, err) + + if !testCase.shouldCreate { + return + } + + testCase.expectedRoute.ID = updatedRoute.ID + + if !testCase.expectedRoute.IsEqual(updatedRoute) { + t.Errorf("new route didn't match expected route:\nGot %#v\nExpected:%#v\n", updatedRoute, testCase.expectedRoute) + } + + }) + } +} + +func TestDeleteRoute(t *testing.T) { + + testingRoute := &route.Route{ + ID: "testingRoute", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + } + + am, err := createRouterManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestRouteAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + account.Routes[testingRoute.ID] = testingRoute + + err = am.Store.SaveAccount(account) + if err != nil { + t.Error("failed to save account") + } + + err = am.DeleteRoute(account.Id, testingRoute.ID) + if err != nil { + t.Error("deleting route failed with error: ", err) + } + + savedAccount, err := am.Store.GetAccount(account.Id) + if err != nil { + t.Error("failed to retrieve saved account with error: ", err) + } + + _, found := savedAccount.Routes[testingRoute.ID] + if found { + t.Error("route shouldn't be found after delete") + } +} + +func TestGetNetworkMap_RouteSync(t *testing.T) { + // no routes for peer in different groups + // no routes when route is deleted + + baseRoute := &route.Route{ + ID: "testingRoute", + Prefix: netip.MustParsePrefix("192.168.0.0/16"), + PrefixType: route.IPv4Prefix, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + } + + am, err := createRouterManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestRouteAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + newAccountRoutes, err := am.GetNetworkMap(peer1Key) + require.NoError(t, err) + require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes") + + createdRoute, err := am.CreateRoute(account.Id, baseRoute.Prefix.String(), baseRoute.Peer, + baseRoute.Description, baseRoute.Masquerade, baseRoute.Metric, false) + require.NoError(t, err) + + noDisabledRoutes, err := am.GetNetworkMap(peer1Key) + require.NoError(t, err) + require.Len(t, noDisabledRoutes.Routes, 0, "no routes for disabled routes") + + enabledRoute := createdRoute.Copy() + enabledRoute.Enabled = true + + err = am.SaveRoute(account.Id, enabledRoute) + require.NoError(t, err) + + peer1Routes, err := am.GetNetworkMap(peer1Key) + require.NoError(t, err) + require.Len(t, peer1Routes.Routes, 1, "we should receive one route for peer1") + require.True(t, enabledRoute.IsEqual(peer1Routes.Routes[0]), "received route should be equal") + + peer2Routes, err := am.GetNetworkMap(peer2Key) + require.NoError(t, err) + require.Len(t, peer2Routes.Routes, 1, "we should receive one route for peer2") + require.True(t, peer1Routes.Routes[0].IsEqual(peer2Routes.Routes[0]), "routes should be the same for peers in the same group") + + newGroup := &Group{ + ID: xid.New().String(), + Name: "peer1 group", + Peers: []string{peer1Key}, + } + err = am.SaveGroup(account.Id, newGroup) + require.NoError(t, err) + + rules, err := am.ListRules(account.Id) + require.NoError(t, err) + + defaultRule := rules[0] + newRule := defaultRule.Copy() + newRule.ID = xid.New().String() + newRule.Name = "peer1 only" + newRule.Source = []string{newGroup.ID} + newRule.Destination = []string{newGroup.ID} + + err = am.SaveRule(account.Id, newRule) + require.NoError(t, err) + + err = am.DeleteRule(account.Id, defaultRule.ID) + require.NoError(t, err) + + peer1GroupRoutes, err := am.GetNetworkMap(peer1Key) + require.NoError(t, err) + require.Len(t, peer1GroupRoutes.Routes, 1, "we should receive one route for peer1") + + peer2GroupRoutes, err := am.GetNetworkMap(peer2Key) + require.NoError(t, err) + require.Len(t, peer2GroupRoutes.Routes, 0, "we should not receive routes for peer2") + + err = am.DeleteRoute(account.Id, enabledRoute.ID) + require.NoError(t, err) + + peer1DeletedRoute, err := am.GetNetworkMap(peer1Key) + require.NoError(t, err) + require.Len(t, peer1DeletedRoute.Routes, 0, "we should receive one route for peer1") + +} + +func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { + store, err := createRouterStore(t) + if err != nil { + return nil, err + } + return BuildManager(store, NewPeersUpdateManager(), nil) +} + +func createRouterStore(t *testing.T) (Store, error) { + dataDir := t.TempDir() + store, err := NewStore(dataDir) + if err != nil { + return nil, err + } + + return store, nil +} + +func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) { + peer1 := &Peer{ + Key: peer1Key, + Name: "test-host1@netbird.io", + Meta: PeerSystemMeta{ + Hostname: "test-host1@netbird.io", + GoOS: "linux", + Kernel: "Linux", + Core: "21.04", + Platform: "x86_64", + OS: "Ubuntu", + WtVersion: "development", + UIVersion: "development", + }, + } + peer2 := &Peer{ + Key: peer2Key, + Name: "test-host2@netbird.io", + Meta: PeerSystemMeta{ + Hostname: "test-host2@netbird.io", + GoOS: "linux", + Kernel: "Linux", + Core: "21.04", + Platform: "x86_64", + OS: "Ubuntu", + WtVersion: "development", + UIVersion: "development", + }, + } + + accountID := "testingAcc" + userID := "testingUser" + domain := "example.com" + + account := newAccountWithId(accountID, userID, domain) + err := am.Store.SaveAccount(account) + if err != nil { + return nil, err + } + + _, err = am.AddPeer("", userID, peer1) + if err != nil { + return nil, err + } + _, err = am.AddPeer("", userID, peer2) + if err != nil { + return nil, err + } + + return account, nil +} diff --git a/management/server/store.go b/management/server/store.go index 3ce548d9b..9c0412304 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -1,5 +1,10 @@ package server +import ( + "github.com/netbirdio/netbird/route" + "net/netip" +) + type Store interface { GetPeer(peerKey string) (*Peer, error) DeletePeer(accountId string, peerKey string) (*Peer, error) @@ -14,4 +19,6 @@ type Store interface { GetAccountBySetupKey(setupKey string) (*Account, error) GetAccountByPrivateDomain(domain string) (*Account, error) SaveAccount(account *Account) error + GetPeerRoutes(peerKey string) ([]*route.Route, error) + GetRoutesByPrefix(accountID string, prefix netip.Prefix) ([]*route.Route, error) } diff --git a/route/route.go b/route/route.go new file mode 100644 index 000000000..e3ffdb77e --- /dev/null +++ b/route/route.go @@ -0,0 +1,119 @@ +package route + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "net/netip" +) + +// Windows has some limitation regarding metric size that differ from Unix-like systems. +// Because of that we are limiting the min and max metric size based on Windows limits: +// see based on info from https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/route_ws2008 +const ( + // MinMetric max metric input + MinMetric = 1 + // MaxMetric max metric input + MaxMetric = 9999 +) +const ( + // InvalidPrefixString invalid prefix type string + InvalidPrefixString = "Invalid" + // IPv4PrefixString IPv4 prefix type string + IPv4PrefixString = "IPv4" + // IPv6PrefixString IPv6 prefix type string + IPv6PrefixString = "IPv6" +) + +const ( + // InvalidPrefix invalid prefix type + InvalidPrefix PrefixType = iota + // IPv4Prefix IPv4 prefix type + IPv4Prefix + // IPv6Prefix IPv6 prefix type + IPv6Prefix +) + +// PrefixType route prefix type +type PrefixType int + +// String returns prefix type string +func (p PrefixType) String() string { + switch p { + case IPv4Prefix: + return IPv4PrefixString + case IPv6Prefix: + return IPv6PrefixString + default: + return InvalidPrefixString + } +} + +// ToPrefixType returns a prefix type +func ToPrefixType(prefix string) PrefixType { + switch prefix { + case IPv4PrefixString: + return IPv4Prefix + case IPv6PrefixString: + return IPv6Prefix + default: + return InvalidPrefix + } +} + +// Route represents a route +type Route struct { + Prefix netip.Prefix + ID string + Description string + Peer string + PrefixType PrefixType + Masquerade bool + Metric int + Enabled bool +} + +// Copy copies a route object +func (r *Route) Copy() *Route { + return &Route{ + ID: r.ID, + Description: r.Description, + Prefix: r.Prefix, + PrefixType: r.PrefixType, + Peer: r.Peer, + Metric: r.Metric, + Masquerade: r.Masquerade, + Enabled: r.Enabled, + } +} + +// IsEqual compares one route with the other +func (r *Route) IsEqual(other *Route) bool { + return other.ID == r.ID && + other.Description == r.Description && + other.Prefix == r.Prefix && + other.PrefixType == r.PrefixType && + other.Peer == r.Peer && + other.Metric == r.Metric && + other.Masquerade == r.Masquerade && + other.Enabled == r.Enabled +} + +// ParsePrefix Parses a prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6 +func ParsePrefix(prefixString string) (PrefixType, netip.Prefix, error) { + prefix, err := netip.ParsePrefix(prefixString) + if err != nil { + return InvalidPrefix, netip.Prefix{}, err + } + + masked := prefix.Masked() + + if !masked.IsValid() { + return InvalidPrefix, netip.Prefix{}, status.Errorf(codes.InvalidArgument, "invalid range %s", prefixString) + } + + if masked.Addr().Is6() { + return IPv6Prefix, masked, nil + } + + return IPv4Prefix, masked, nil +}