diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 64fe743a9..dbb31ccdb 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v2 - 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 + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev - name: Install modules run: go mod tidy @@ -60,7 +60,7 @@ jobs: uses: actions/checkout@v2 - 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 + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev - name: Install modules run: go mod tidy diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 5b8f8412a..956d37936 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -11,7 +11,7 @@ jobs: with: go-version: 1.19.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 + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1da745947..32dd4d02c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -104,7 +104,7 @@ jobs: run: git --no-pager diff --exit-code - 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 gcc-mingw-w64-x86-64 + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64 - name: Install rsrc run: go install github.com/akavel/rsrc@v0.10.2 - name: Generate windows rsrc diff --git a/management/server/account.go b/management/server/account.go index 2d434e2d9..824e45d92 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -75,7 +75,7 @@ type AccountManager interface { DeleteRule(accountId, ruleID string) error ListRules(accountID, userID string) ([]*Rule, error) GetRoute(accountID, routeID, userID string) (*route.Route, error) - CreateRoute(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) + CreateRoute(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, groups []string, 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 @@ -137,31 +137,41 @@ type UserInfo struct { Status string `json:"-"` } -// GetPeersRoutes returns all active routes of provided peers -func (a *Account) GetPeersRoutes(givenPeers []*Peer) []*route.Route { - //TODO Peer.ID migration: we will need to replace search by Peer.ID here - routes := make([]*route.Route, 0) - for _, peer := range givenPeers { - peerRoutes := a.GetPeerRoutes(peer.Key) - activeRoutes := make([]*route.Route, 0) - for _, pr := range peerRoutes { - if pr.Enabled { - activeRoutes = append(activeRoutes, pr) - } - } - if len(activeRoutes) > 0 { - routes = append(routes, activeRoutes...) - } +// getRoutesToSync returns the enabled routes for the peer ID and the routes +// from the ACL peers that have distribution groups associated with the peer ID +func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Route { + routes := a.getEnabledRoutesByPeer(peerID) + groupListMap := a.getPeerGroups(peerID) + for _, peer := range aclPeers { + activeRoutes := a.getEnabledRoutesByPeer(peer.Key) + filteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap) + routes = append(routes, filteredRoutes...) } + return routes } -// GetPeerRoutes returns a list of routes of a given peer -func (a *Account) GetPeerRoutes(peerPubKey string) []*route.Route { +// filterRoutesByGroups returns a list with routes that have distribution groups in the group's map +func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap lookupMap) []*route.Route { + var filteredRoutes []*route.Route + for _, r := range routes { + for _, groupID := range r.Groups { + _, found := groupListMap[groupID] + if found { + filteredRoutes = append(filteredRoutes, r) + break + } + } + } + return filteredRoutes +} + +// getEnabledRoutesByPeer returns a list of routes of a given peer +func (a *Account) getEnabledRoutesByPeer(peerPubKey string) []*route.Route { //TODO Peer.ID migration: we will need to replace search by Peer.ID here var routes []*route.Route for _, r := range a.Routes { - if r.Peer == peerPubKey { + if r.Peer == peerPubKey && r.Enabled { routes = append(routes, r) continue } @@ -299,6 +309,19 @@ func (a *Account) getUserGroups(userID string) ([]string, error) { return user.AutoGroups, nil } +func (a *Account) getPeerGroups(peerID string) lookupMap { + groupList := make(lookupMap) + for groupID, group := range a.Groups { + for _, id := range group.Peers { + if id == peerID { + groupList[groupID] = struct{}{} + break + } + } + } + return groupList +} + func (a *Account) getSetupKeyGroups(setupKey string) ([]string, error) { key, err := a.FindSetupKey(setupKey) if err != nil { diff --git a/management/server/account_test.go b/management/server/account_test.go index 28d1e991b..667d582be 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1075,7 +1075,7 @@ func TestFileStore_GetRoutesByPrefix(t *testing.T) { assert.Contains(t, routeIDs, "route-2") } -func TestAccount_GetPeersRoutes(t *testing.T) { +func TestAccount_GetRoutesToSync(t *testing.T) { _, prefix, err := route.ParseNetwork("192.168.64.0/24") if err != nil { t.Fatal(err) @@ -1084,6 +1084,7 @@ func TestAccount_GetPeersRoutes(t *testing.T) { Peers: map[string]*Peer{ "peer-1": {Key: "peer-1"}, "peer-2": {Key: "peer-2"}, "peer-3": {Key: "peer-1"}, }, + Groups: map[string]*Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}}, Routes: map[string]*route.Route{ "route-1": { ID: "route-1", @@ -1095,6 +1096,7 @@ func TestAccount_GetPeersRoutes(t *testing.T) { Masquerade: false, Metric: 999, Enabled: true, + Groups: []string{"group1"}, }, "route-2": { ID: "route-2", @@ -1106,11 +1108,12 @@ func TestAccount_GetPeersRoutes(t *testing.T) { Masquerade: false, Metric: 999, Enabled: true, + Groups: []string{"group1"}, }, }, } - routes := account.GetPeersRoutes([]*Peer{{Key: "peer-1"}, {Key: "peer-2"}, {Key: "non-existing-peer"}}) + routes := account.getRoutesToSync("peer-2", []*Peer{{Key: "peer-1"}, {Key: "peer-3"}}) assert.Len(t, routes, 2) routeIDs := make(map[string]struct{}, 2) @@ -1120,6 +1123,9 @@ func TestAccount_GetPeersRoutes(t *testing.T) { assert.Contains(t, routeIDs, "route-1") assert.Contains(t, routeIDs, "route-2") + emptyRoutes := account.getRoutesToSync("peer-3", []*Peer{{Key: "peer-1"}, {Key: "peer-2"}}) + + assert.Len(t, emptyRoutes, 0) } func TestAccount_Copy(t *testing.T) { diff --git a/management/server/dns.go b/management/server/dns.go index 62a2e1274..85c4e481f 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -78,15 +78,7 @@ func getPeersCustomZone(account *Account, dnsDomain string) nbdns.CustomZone { } func getPeerNSGroups(account *Account, peerID string) []*nbdns.NameServerGroup { - groupList := make(lookupMap) - for groupID, group := range account.Groups { - for _, id := range group.Peers { - if id == peerID { - groupList[groupID] = struct{}{} - break - } - } - } + groupList := account.getPeerGroups(peerID) var peerNSGroups []*nbdns.NameServerGroup diff --git a/management/server/file_store.go b/management/server/file_store.go index aab3eb8d8..cd66d4e5b 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -105,6 +105,19 @@ func restore(file string) (*FileStore, error) { if len(existingLabels) != len(account.Peers) { addPeerLabelsToAccount(account, existingLabels) } + + allGroup, err := account.GetGroupAll() + if err != nil { + log.Errorf("unable to find the All group, this should happen only when migrate from a version that didn't support groups. Error: %v", err) + // if the All group didn't exist we probably don't have routes to update + continue + } + + for _, route := range account.Routes { + if len(route.Groups) == 0 { + route.Groups = []string{allGroup.ID} + } + } } // we need this persist to apply changes we made to account.Peers (we set them to Disconnected) diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 96a90053a..c25e14fa0 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -379,6 +379,11 @@ components: masquerade: description: Indicate if peer should masquerade traffic to this route's prefix type: boolean + groups: + description: Route group tag groups + type: array + items: + type: string required: - id - description @@ -388,6 +393,7 @@ components: - network - metric - masquerade + - groups Route: allOf: - type: object @@ -410,7 +416,7 @@ components: path: description: Route field to update in form / type: string - enum: [ "network","network_id","description","enabled","peer","metric","masquerade" ] + enum: [ "network","network_id","description","enabled","peer","metric","masquerade", "groups" ] required: - path Nameserver: diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index c42a0e6b1..3ba8e6775 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -65,6 +65,7 @@ const ( const ( RoutePatchOperationPathDescription RoutePatchOperationPath = "description" RoutePatchOperationPathEnabled RoutePatchOperationPath = "enabled" + RoutePatchOperationPathGroups RoutePatchOperationPath = "groups" RoutePatchOperationPathMasquerade RoutePatchOperationPath = "masquerade" RoutePatchOperationPathMetric RoutePatchOperationPath = "metric" RoutePatchOperationPathNetwork RoutePatchOperationPath = "network" @@ -296,6 +297,9 @@ type Route struct { // Enabled Route status Enabled bool `json:"enabled"` + // Groups Route group tag groups + Groups []string `json:"groups"` + // Id Route Id Id string `json:"id"` @@ -344,6 +348,9 @@ type RouteRequest struct { // Enabled Route status Enabled bool `json:"enabled"` + // Groups Route group tag groups + Groups []string `json:"groups"` + // Masquerade Indicate if peer should masquerade traffic to this route's prefix Masquerade bool `json:"masquerade"` diff --git a/management/server/http/routes.go b/management/server/http/routes.go index f85e3ee20..8145ba0e1 100644 --- a/management/server/http/routes.go +++ b/management/server/http/routes.go @@ -89,7 +89,7 @@ func (h *Routes) CreateRouteHandler(w http.ResponseWriter, r *http.Request) { return } - newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), peerKey, req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Enabled) + newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), peerKey, req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Groups, req.Enabled) if err != nil { util.WriteError(err, w) return @@ -162,6 +162,7 @@ func (h *Routes) UpdateRouteHandler(w http.ResponseWriter, r *http.Request) { Metric: req.Metric, Description: req.Description, Enabled: req.Enabled, + Groups: req.Groups, } err = h.accountManager.SaveRoute(account.Id, newRoute) @@ -298,6 +299,16 @@ func (h *Routes) PatchRouteHandler(w http.ResponseWriter, r *http.Request) { Type: server.UpdateRouteEnabled, Values: patch.Value, }) + case api.RoutePatchOperationPathGroups: + if patch.Op != api.RoutePatchOperationOpReplace { + util.WriteError(status.Errorf(status.InvalidArgument, + "groups field only accepts replace operation, got %s", patch.Op), w) + return + } + operations = append(operations, server.RouteUpdateOperation{ + Type: server.UpdateRouteGroups, + Values: patch.Value, + }) default: util.WriteError(status.Errorf(status.InvalidArgument, "invalid patch path"), w) return @@ -383,5 +394,6 @@ func toRouteResponse(account *server.Account, serverRoute *route.Route) *api.Rou NetworkType: serverRoute.NetworkType.String(), Masquerade: serverRoute.Masquerade, Metric: serverRoute.Metric, + Groups: serverRoute.Groups, } } diff --git a/management/server/http/routes_test.go b/management/server/http/routes_test.go index cd3822585..9af5e7f55 100644 --- a/management/server/http/routes_test.go +++ b/management/server/http/routes_test.go @@ -28,6 +28,7 @@ const ( notFoundPeerID = "100.64.0.200" existingPeerKey = "existingPeerKey" testAccountID = "test_id" + existingGroupID = "testGroup" ) var baseExistingRoute = &route.Route{ @@ -39,6 +40,7 @@ var baseExistingRoute = &route.Route{ Metric: 9999, Masquerade: false, Enabled: true, + Groups: []string{existingGroupID}, } var testingAccount = &server.Account{ @@ -64,7 +66,7 @@ func initRoutesTestData() *Routes { } return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID) }, - CreateRouteFunc: func(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) { + CreateRouteFunc: func(accountID string, network, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) { networkType, p, _ := route.ParseNetwork(network) return &route.Route{ ID: existingRouteID, @@ -75,6 +77,7 @@ func initRoutesTestData() *Routes { Description: description, Masquerade: masquerade, Enabled: enabled, + Groups: groups, }, nil }, SaveRouteFunc: func(_ string, _ *route.Route) error { @@ -116,6 +119,8 @@ func initRoutesTestData() *Routes { routeToUpdate.Masquerade, _ = strconv.ParseBool(operation.Values[0]) case server.UpdateRouteEnabled: routeToUpdate.Enabled, _ = strconv.ParseBool(operation.Values[0]) + case server.UpdateRouteGroups: + routeToUpdate.Groups = operation.Values default: return nil, fmt.Errorf("no operation") } @@ -181,7 +186,7 @@ func TestRoutesHandlers(t *testing.T) { requestType: http.MethodPost, requestPath: "/api/routes", requestBody: bytes.NewBuffer( - []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID))), + []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID))), expectedStatus: http.StatusOK, expectedBody: true, expectedRoute: &api.Route{ @@ -193,13 +198,14 @@ func TestRoutesHandlers(t *testing.T) { NetworkType: route.IPv4NetworkString, Masquerade: false, Enabled: false, + Groups: []string{existingGroupID}, }, }, { name: "POST Not Found Peer", requestType: http.MethodPost, requestPath: "/api/routes", - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", notFoundPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", notFoundPeerID, existingGroupID)), expectedStatus: http.StatusNotFound, expectedBody: false, }, @@ -207,7 +213,7 @@ func TestRoutesHandlers(t *testing.T) { name: "POST Invalid Network Identifier", requestType: http.MethodPost, requestPath: "/api/routes", - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -215,7 +221,7 @@ func TestRoutesHandlers(t *testing.T) { name: "POST Invalid Network", requestType: http.MethodPost, requestPath: "/api/routes", - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -223,7 +229,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT OK", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusOK, expectedBody: true, expectedRoute: &api.Route{ @@ -235,13 +241,14 @@ func TestRoutesHandlers(t *testing.T) { NetworkType: route.IPv4NetworkString, Masquerade: false, Enabled: false, + Groups: []string{existingGroupID}, }, }, { name: "PUT Not Found Route", requestType: http.MethodPut, requestPath: "/api/routes/" + notFoundRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusNotFound, expectedBody: false, }, @@ -249,7 +256,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT Not Found Peer", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", notFoundPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", notFoundPeerID, existingGroupID)), expectedStatus: http.StatusNotFound, expectedBody: false, }, @@ -257,7 +264,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT Invalid Network Identifier", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"12345678901234567890qwertyuiopqwertyuiop1\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -265,7 +272,7 @@ func TestRoutesHandlers(t *testing.T) { name: "PUT Invalid Network", requestType: http.MethodPut, requestPath: "/api/routes/" + existingRouteID, - requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\"}", existingPeerID)), + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/34\",\"network_id\":\"awesomeNet\",\"Peer\":\"%s\",\"groups\":[\"%s\"]}", existingPeerID, existingGroupID)), expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, @@ -285,6 +292,7 @@ func TestRoutesHandlers(t *testing.T) { Masquerade: baseExistingRoute.Masquerade, Enabled: baseExistingRoute.Enabled, Metric: baseExistingRoute.Metric, + Groups: baseExistingRoute.Groups, }, }, { @@ -304,6 +312,7 @@ func TestRoutesHandlers(t *testing.T) { Masquerade: baseExistingRoute.Masquerade, Enabled: baseExistingRoute.Enabled, Metric: baseExistingRoute.Metric, + Groups: baseExistingRoute.Groups, }, }, { diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 3030b831d..8efeee33a 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -43,7 +43,7 @@ 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, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) + CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) GetRouteFunc func(accountID, routeID, userID string) (*route.Route, error) SaveRouteFunc func(accountID string, route *route.Route) error UpdateRouteFunc func(accountID string, routeID string, operations []server.RouteUpdateOperation) (*route.Route, error) @@ -325,9 +325,9 @@ func (am *MockAccountManager) UpdatePeer(accountID string, peer *server.Peer) (* } // CreateRoute mock implementation of CreateRoute from server.AccountManager interface -func (am *MockAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) { +func (am *MockAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) { if am.CreateRouteFunc != nil { - return am.CreateRouteFunc(accountID, network, peer, description, netID, masquerade, metric, enabled) + return am.CreateRouteFunc(accountID, network, peer, description, netID, masquerade, metric, groups, enabled) } return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented") } diff --git a/management/server/peer.go b/management/server/peer.go index 01e7ffed4..d4bc089dc 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -120,7 +120,7 @@ func (am *DefaultAccountManager) GetPeers(accountID, userID string) ([]*Peer, er // fetch all the peers that have access to the user's peers for _, peer := range peers { - aclPeers := am.getPeersByACL(account, peer.Key) + aclPeers := account.getPeersByACL(peer.Key) for _, p := range aclPeers { peersMap[p.Key] = p } @@ -293,8 +293,8 @@ func (am *DefaultAccountManager) GetNetworkMap(peerPubKey string) (*NetworkMap, return nil, err } - aclPeers := am.getPeersByACL(account, peerPubKey) - routesUpdate := account.GetPeersRoutes(append(aclPeers, account.Peers[peerPubKey])) + aclPeers := account.getPeersByACL(peerPubKey) + routesUpdate := account.getRoutesToSync(peerPubKey, aclPeers) var zones []nbdns.CustomZone peersCustomZone := getPeersCustomZone(account, am.dnsDomain) @@ -515,9 +515,9 @@ func (am *DefaultAccountManager) UpdatePeerMeta(peerPubKey string, meta PeerSyst } // getPeersByACL returns all peers that given peer has access to. -func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey string) []*Peer { +func (a *Account) getPeersByACL(peerPubKey string) []*Peer { var peers []*Peer - srcRules, dstRules := account.GetPeerRules(peerPubKey) + srcRules, dstRules := a.GetPeerRules(peerPubKey) groups := map[string]*Group{} for _, r := range srcRules { @@ -526,7 +526,7 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey stri } if r.Flow == TrafficFlowBidirect { for _, gid := range r.Destination { - if group, ok := account.Groups[gid]; ok { + if group, ok := a.Groups[gid]; ok { groups[gid] = group } } @@ -539,7 +539,7 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey stri } if r.Flow == TrafficFlowBidirect { for _, gid := range r.Source { - if group, ok := account.Groups[gid]; ok { + if group, ok := a.Groups[gid]; ok { groups[gid] = group } } @@ -549,13 +549,13 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerPubKey stri peersSet := make(map[string]struct{}) for _, g := range groups { for _, pid := range g.Peers { - peer, ok := account.Peers[pid] + peer, ok := a.Peers[pid] if !ok { log.Warnf( "peer %s found in group %s but doesn't belong to account %s", pid, g.ID, - account.Id, + a.Id, ) continue } diff --git a/management/server/route.go b/management/server/route.go index feef82533..36fd39c2d 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -26,6 +26,8 @@ const ( UpdateRouteEnabled // UpdateRouteNetworkIdentifier indicates a route net ID update operation UpdateRouteNetworkIdentifier + // UpdateRouteGroups indicates a group list update operation + UpdateRouteGroups ) // RouteUpdateOperationType operation type @@ -47,6 +49,8 @@ func (t RouteUpdateOperationType) String() string { return "UpdateRouteEnabled" case UpdateRouteNetworkIdentifier: return "UpdateRouteNetworkIdentifier" + case UpdateRouteGroups: + return "UpdateRouteGroups" default: return "InvalidOperation" } @@ -114,7 +118,7 @@ func (am *DefaultAccountManager) checkPrefixPeerExists(accountID, peer string, p } // CreateRoute creates and saves a new route -func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, enabled bool) (*route.Route, error) { +func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool) (*route.Route, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -148,6 +152,11 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, de return nil, status.Errorf(status.InvalidArgument, "identifier should be between 1 and %d", route.MaxNetIDChar) } + err = validateGroups(groups, account.Groups) + if err != nil { + return nil, err + } + newRoute.Peer = peer newRoute.ID = xid.New().String() newRoute.Network = newPrefix @@ -157,6 +166,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peer, de newRoute.Masquerade = masquerade newRoute.Metric = metric newRoute.Enabled = enabled + newRoute.Groups = groups if account.Routes == nil { account.Routes = make(map[string]*route.Route) @@ -210,6 +220,11 @@ func (am *DefaultAccountManager) SaveRoute(accountID string, routeToSave *route. } } + err = validateGroups(routeToSave.Groups, account.Groups) + if err != nil { + return err + } + account.Routes[routeToSave.ID] = routeToSave account.Network.IncSerial() @@ -300,6 +315,12 @@ func (am *DefaultAccountManager) UpdateRoute(accountID, routeID string, operatio return nil, status.Errorf(status.InvalidArgument, "failed to parse enabled %s, not boolean", operation.Values[0]) } newRoute.Enabled = enabled + case UpdateRouteGroups: + err = validateGroups(operation.Values, account.Groups) + if err != nil { + return nil, err + } + newRoute.Groups = operation.Values } } diff --git a/management/server/route_test.go b/management/server/route_test.go index 8acb95b2e..1bd0598cc 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -8,8 +8,13 @@ import ( "testing" ) -const peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=" -const peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI=" +const ( + peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=" + peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI=" + routeGroup1 = "routeGroup1" + routeGroup2 = "routeGroup2" + routeInvalidGroup1 = "routeInvalidGroup1" +) func TestCreateRoute(t *testing.T) { @@ -21,6 +26,7 @@ func TestCreateRoute(t *testing.T) { masquerade bool metric int enabled bool + groups []string } testCases := []struct { @@ -40,6 +46,7 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.NoError, shouldCreate: true, @@ -52,10 +59,11 @@ func TestCreateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, }, { - name: "Bad Prefix", + name: "Bad Prefix Should Fail", inputArgs: input{ network: "192.168.0.0/34", netID: "happy", @@ -64,12 +72,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Bad Peer", + name: "Bad Peer Should Fail", inputArgs: input{ network: "192.168.0.0/16", netID: "happy", @@ -78,12 +87,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Empty Peer", + name: "Empty Peer Should Create", inputArgs: input{ network: "192.168.0.0/16", netID: "happy", @@ -92,6 +102,7 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: false, + groups: []string{routeGroup1}, }, errFunc: require.NoError, shouldCreate: true, @@ -104,10 +115,11 @@ func TestCreateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: false, + Groups: []string{routeGroup1}, }, }, { - name: "Large Metric", + name: "Large Metric Should Fail", inputArgs: input{ network: "192.168.0.0/16", peer: peer1Key, @@ -116,12 +128,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 99999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Small Metric", + name: "Small Metric Should Fail", inputArgs: input{ network: "192.168.0.0/16", netID: "happy", @@ -130,12 +143,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 0, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Large NetID", + name: "Large NetID Should Fail", inputArgs: input{ network: "192.168.0.0/16", peer: peer1Key, @@ -144,12 +158,13 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, }, errFunc: require.Error, shouldCreate: false, }, { - name: "Small NetID", + name: "Small NetID Should Fail", inputArgs: input{ network: "192.168.0.0/16", netID: "", @@ -158,6 +173,52 @@ func TestCreateRoute(t *testing.T) { masquerade: false, metric: 9999, enabled: true, + groups: []string{routeGroup1}, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Empty Group List Should Fail", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "NewId", + peer: peer1Key, + description: "", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{}, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Empty Group ID string Should Fail", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "NewId", + peer: peer1Key, + description: "", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{""}, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Invalid Group Should Fail", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "NewId", + peer: peer1Key, + description: "", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeInvalidGroup1}, }, errFunc: require.Error, shouldCreate: false, @@ -183,6 +244,7 @@ func TestCreateRoute(t *testing.T) { testCase.inputArgs.netID, testCase.inputArgs.masquerade, testCase.inputArgs.metric, + testCase.inputArgs.groups, testCase.inputArgs.enabled, ) @@ -220,6 +282,7 @@ func TestSaveRoute(t *testing.T) { newPeer *string newMetric *int newPrefix *netip.Prefix + newGroups []string skipCopying bool shouldCreate bool errFunc require.ErrorAssertionFunc @@ -237,10 +300,12 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newPeer: &validPeer, newMetric: &validMetric, newPrefix: &validPrefix, + newGroups: []string{routeGroup2}, errFunc: require.NoError, shouldCreate: true, expectedRoute: &route.Route{ @@ -253,10 +318,11 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: validMetric, Enabled: true, + Groups: []string{routeGroup2}, }, }, { - name: "Bad Prefix", + name: "Bad Prefix Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -267,12 +333,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newPrefix: &invalidPrefix, errFunc: require.Error, }, { - name: "Bad Peer", + name: "Bad Peer Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -283,12 +350,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newPeer: &invalidPeer, errFunc: require.Error, }, { - name: "Invalid Metric", + name: "Invalid Metric Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -299,12 +367,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newMetric: &invalidMetric, errFunc: require.Error, }, { - name: "Invalid NetID", + name: "Invalid NetID Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -315,12 +384,13 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, newMetric: &invalidMetric, errFunc: require.Error, }, { - name: "Nil Route", + name: "Nil Route Should Fail", existingRoute: &route.Route{ ID: "testingRoute", Network: netip.MustParsePrefix("192.168.0.0/16"), @@ -331,10 +401,62 @@ func TestSaveRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, skipCopying: true, errFunc: require.Error, }, + { + name: "Empty Group List Should Fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newGroups: []string{}, + errFunc: require.Error, + }, + { + name: "Empty Group ID String Should Fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newGroups: []string{""}, + errFunc: require.Error, + }, + { + name: "Invalid Group Should Fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1Key, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newGroups: []string{routeInvalidGroup1}, + errFunc: require.Error, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -370,6 +492,10 @@ func TestSaveRoute(t *testing.T) { if testCase.newPrefix != nil { routeToSave.Network = *testCase.newPrefix } + + if testCase.newGroups != nil { + routeToSave.Groups = testCase.newGroups + } } err = am.SaveRoute(account.Id, routeToSave) @@ -409,6 +535,7 @@ func TestUpdateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, } testCases := []struct { @@ -423,7 +550,7 @@ func TestUpdateRoute(t *testing.T) { name: "Happy Path Single OPS", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{peer2Key}, }, @@ -440,40 +567,45 @@ func TestUpdateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, }, { name: "Happy Path Multiple OPS", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteDescription, Values: []string{"great"}, }, - RouteUpdateOperation{ + { Type: UpdateRouteNetwork, 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"}, }, - RouteUpdateOperation{ + { Type: UpdateRouteNetworkIdentifier, Values: []string{"megaRoute"}, }, + { + Type: UpdateRouteGroups, + Values: []string{routeGroup2}, + }, }, errFunc: require.NoError, shouldCreate: true, @@ -487,23 +619,24 @@ func TestUpdateRoute(t *testing.T) { Masquerade: true, Metric: 3030, Enabled: false, + Groups: []string{routeGroup2}, }, }, { - name: "Empty Values", + name: "Empty Values Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, }, }, errFunc: require.Error, }, { - name: "Multiple Values", + name: "Multiple Values Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{peer2Key, peer1Key}, }, @@ -511,10 +644,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Bad Prefix", + name: "Bad Prefix Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteNetwork, Values: []string{"192.168.0.0/34"}, }, @@ -522,10 +655,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Bad Peer", + name: "Bad Peer Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{"non existing Peer"}, }, @@ -536,7 +669,7 @@ func TestUpdateRoute(t *testing.T) { name: "Empty Peer", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRoutePeer, Values: []string{""}, }, @@ -553,13 +686,14 @@ func TestUpdateRoute(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, }, }, { - name: "Large Network ID", + name: "Large Network ID Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteNetworkIdentifier, Values: []string{"12345678901234567890qwertyuiopqwertyuiop1"}, }, @@ -567,10 +701,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Empty Network ID", + name: "Empty Network ID Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteNetworkIdentifier, Values: []string{""}, }, @@ -578,10 +712,10 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Invalid Metric", + name: "Invalid Metric Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteMetric, Values: []string{"999999"}, }, @@ -589,16 +723,27 @@ func TestUpdateRoute(t *testing.T) { errFunc: require.Error, }, { - name: "Invalid Boolean", + name: "Invalid Boolean Should Fail", existingRoute: existingRoute, operations: []RouteUpdateOperation{ - RouteUpdateOperation{ + { Type: UpdateRouteMasquerade, Values: []string{"yes"}, }, }, errFunc: require.Error, }, + { + name: "Invalid Group Should Fail", + existingRoute: existingRoute, + operations: []RouteUpdateOperation{ + { + Type: UpdateRouteGroups, + Values: []string{routeInvalidGroup1}, + }, + }, + errFunc: require.Error, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -697,6 +842,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { Masquerade: false, Metric: 9999, Enabled: true, + Groups: []string{routeGroup1}, } am, err := createRouterManager(t) @@ -714,7 +860,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes") createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network.String(), baseRoute.Peer, - baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, false) + baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, false) require.NoError(t, err) noDisabledRoutes, err := am.GetNetworkMap(peer1Key) @@ -734,7 +880,14 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { peer2Routes, err := am.GetNetworkMap(peer2Key) require.NoError(t, err) - require.Len(t, peer2Routes.Routes, 1, "we should receive one route for peer2") + require.Len(t, peer2Routes.Routes, 0, "no routes for peers not in the distribution group") + + err = am.GroupAddPeer(account.Id, routeGroup1, peer2Key) + require.NoError(t, err) + + peer2Routes, err = am.GetNetworkMap(peer2Key) + require.NoError(t, err) + require.Len(t, peer2Routes.Routes, 1, "we should receive one route") require.True(t, peer1Routes.Routes[0].IsEqual(peer2Routes.Routes[0]), "routes should be the same for peers in the same group") newGroup := &Group{ @@ -844,6 +997,26 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er if err != nil { return nil, err } + newGroup := &Group{ + ID: routeGroup1, + Name: routeGroup1, + Peers: []string{peer1Key}, + } + err = am.SaveGroup(accountID, newGroup) + if err != nil { + return nil, err + } + + newGroup = &Group{ + ID: routeGroup2, + Name: routeGroup2, + Peers: []string{peer1Key}, + } + + err = am.SaveGroup(accountID, newGroup) + if err != nil { + return nil, err + } return am.Store.GetAccount(account.Id) } diff --git a/route/route.go b/route/route.go index 4173783df..62a76db82 100644 --- a/route/route.go +++ b/route/route.go @@ -73,6 +73,7 @@ type Route struct { Masquerade bool Metric int Enabled bool + Groups []string } // Copy copies a route object @@ -87,6 +88,7 @@ func (r *Route) Copy() *Route { Metric: r.Metric, Masquerade: r.Masquerade, Enabled: r.Enabled, + Groups: r.Groups, } } @@ -100,7 +102,8 @@ func (r *Route) IsEqual(other *Route) bool { other.Peer == r.Peer && other.Metric == r.Metric && other.Masquerade == r.Masquerade && - other.Enabled == r.Enabled + other.Enabled == r.Enabled && + compareGroupsList(r.Groups, other.Groups) } // ParseNetwork Parses a network prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6 @@ -122,3 +125,23 @@ func ParseNetwork(networkString string) (NetworkType, netip.Prefix, error) { return IPv4Network, masked, nil } + +func compareGroupsList(list, other []string) bool { + if len(list) != len(other) { + return false + } + for _, id := range list { + match := false + for _, otherID := range other { + if id == otherID { + match = true + break + } + } + if !match { + return false + } + } + + return true +}