From 8118d60ffb252686a3643f58ca276573cf50977d Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Thu, 28 Sep 2023 14:32:36 +0200 Subject: [PATCH] Add peer groups support for network routes (#1150) This commit enhances the functionality of the network routes endpoint by introducing a new parameter called `peers_group`. This addition allows users to associate network routes with specific peer groups, simplifying the management and distribution of routes within a network. --- management/server/account.go | 96 +++- management/server/account_test.go | 5 +- management/server/group.go | 4 +- management/server/http/api/openapi.yml | 12 +- management/server/http/api/types.gen.go | 16 +- management/server/http/routes_handler.go | 69 ++- management/server/http/routes_handler_test.go | 134 +++++- management/server/mock_server/account_mock.go | 18 +- management/server/route.go | 123 ++++- management/server/route_test.go | 449 +++++++++++++++++- route/route.go | 10 +- 11 files changed, 832 insertions(+), 104 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 65a284b5f..b4f939166 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -83,14 +83,14 @@ type AccountManager interface { DeleteGroup(accountId, userId, groupID string) error ListGroups(accountId string) ([]*Group, error) GroupAddPeer(accountId, groupID, peerID string) error - GroupDeletePeer(accountId, groupID, peerKey string) error + GroupDeletePeer(accountId, groupID, peerID string) error GroupListPeers(accountId, groupID string) ([]*Peer, error) GetPolicy(accountID, policyID, userID string) (*Policy, error) SavePolicy(accountID, userID string, policy *Policy) error DeletePolicy(accountID, policyID, userID string) error ListPolicies(accountID, userID string) ([]*Policy, error) GetRoute(accountID, routeID, userID string) (*route.Route, error) - CreateRoute(accountID string, prefix, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) + CreateRoute(accountID, prefix, peerID string, peerGroupIDs []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) SaveRoute(accountID, userID string, route *route.Route) error DeleteRoute(accountID, routeID, userID string) error ListRoutes(accountID, userID string) ([]*route.Route, error) @@ -253,22 +253,39 @@ func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap looku func (a *Account) getEnabledAndDisabledRoutesByPeer(peerID string) ([]*route.Route, []*route.Route) { var enabledRoutes []*route.Route var disabledRoutes []*route.Route + + takeRoute := func(r *route.Route, id string) { + peer := a.GetPeer(peerID) + if peer == nil { + log.Errorf("route %s has peer %s that doesn't exist under account %s", r.ID, peerID, a.Id) + return + } + + if r.Enabled { + enabledRoutes = append(enabledRoutes, r) + return + } + disabledRoutes = append(disabledRoutes, r) + } + for _, r := range a.Routes { + if len(r.PeerGroups) != 0 { + for _, groupID := range r.PeerGroups { + group := a.GetGroup(groupID) + if group == nil { + log.Errorf("route %s has peers group %s that doesn't exist under account %s", r.ID, groupID, a.Id) + continue + } + for _, id := range group.Peers { + if id == peerID { + takeRoute(r, id) + break + } + } + } + } if r.Peer == peerID { - // We need to set Peer.Key instead of Peer.ID because this object will be sent to agents as part of a network map. - // Ideally we should have a separate field for that, but fine for now. - peer := a.GetPeer(peerID) - if peer == nil { - log.Errorf("route %s has peer %s that doesn't exist under account %s", r.ID, peerID, a.Id) - continue - } - raut := r.Copy() - raut.Peer = peer.Key - if r.Enabled { - enabledRoutes = append(enabledRoutes, raut) - continue - } - disabledRoutes = append(disabledRoutes, raut) + takeRoute(r, peerID) } } return enabledRoutes, disabledRoutes @@ -316,8 +333,51 @@ func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap { } peersToConnect = append(peersToConnect, p) } - // Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID. - routesUpdate := a.getRoutesToSync(peerID, peersToConnect) + + routes := a.getRoutesToSync(peerID, peersToConnect) + + takePeer := func(id string) (*Peer, bool) { + peer := a.GetPeer(id) + if peer == nil || peer.Meta.GoOS != "linux" { + return nil, false + } + return peer, true + } + + // We need to set Peer.Key instead of Peer.ID because this object will be sent to agents as part of a network map. + // Ideally we should have a separate field for that, but fine for now. + var routesUpdate []*route.Route + seenPeers := make(map[string]bool) + for _, r := range routes { + if r.Peer != "" { + peer, valid := takePeer(r.Peer) + if !valid { + continue + } + rCopy := r.Copy() + rCopy.Peer = peer.Key // client expects the key + routesUpdate = append(routesUpdate, rCopy) + continue + } + for _, groupID := range r.PeerGroups { + if group := a.GetGroup(groupID); group != nil { + for _, peerId := range group.Peers { + peer, valid := takePeer(peerId) + if !valid { + continue + } + + if _, ok := seenPeers[peer.ID]; !ok { + rCopy := r.Copy() + rCopy.ID = r.ID + ":" + peer.ID // we have to provide unit route id when distribute network map + rCopy.Peer = peer.Key // client expects the key + routesUpdate = append(routesUpdate, rCopy) + } + seenPeers[peer.ID] = true + } + } + } + } dnsManagementStatus := a.getPeerDNSManagementStatus(peerID) dnsUpdate := nbdns.Config{ diff --git a/management/server/account_test.go b/management/server/account_test.go index 204e98947..8c8a1b001 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1385,8 +1385,9 @@ func TestAccount_Copy(t *testing.T) { }, Routes: map[string]*route.Route{ "route1": { - ID: "route1", - Groups: []string{"group1"}, + ID: "route1", + PeerGroups: []string{}, + Groups: []string{"group1"}, }, }, NameServerGroups: map[string]*nbdns.NameServerGroup{ diff --git a/management/server/group.go b/management/server/group.go index 697fe5d70..0aebc6454 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -285,7 +285,7 @@ func (am *DefaultAccountManager) GroupAddPeer(accountID, groupID, peerID string) } // GroupDeletePeer removes peer from the group -func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerKey string) error { +func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerID string) error { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -301,7 +301,7 @@ func (am *DefaultAccountManager) GroupDeletePeer(accountID, groupID, peerKey str account.Network.IncSerial() for i, itemID := range group.Peers { - if itemID == peerKey { + if itemID == peerID { group.Peers = append(group.Peers[:i], group.Peers[i+1:]...) if err := am.Store.SaveAccount(account); err != nil { return err diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index db2561481..cfd14958f 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -745,9 +745,15 @@ components: type: boolean example: true peer: - description: Peer Identifier associated with route + description: Peer Identifier associated with route. This property can not be set together with `peer_groups` type: string example: chacbco6lnnbn6cg5s91 + peer_groups: + description: Peers Group Identifier associated with route. This property can not be set together with `peer` + type: array + items: + type: string + example: chacbco6lnnbn6cg5s91 network: description: Network range in CIDR format type: string @@ -773,7 +779,9 @@ components: - description - network_id - enabled - - peer + # Only one property has to be set + #- peer + #- peer_groups - network - metric - masquerade diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index dec73630d..fd3eedde3 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -1,6 +1,6 @@ // Package api provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/deepmap/oapi-codegen version v1.11.1-0.20220912230023-4a1477f6a8ba DO NOT EDIT. +// Code generated by github.com/deepmap/oapi-codegen version v1.15.0 DO NOT EDIT. package api import ( @@ -599,8 +599,11 @@ type Route struct { // NetworkType Network type indicating if it is IPv4 or IPv6 NetworkType string `json:"network_type"` - // Peer Peer Identifier associated with route - Peer string `json:"peer"` + // Peer Peer Identifier associated with route. This property can not be set together with `peer_groups` + Peer *string `json:"peer,omitempty"` + + // PeerGroups Peers Group Identifier associated with route. This property can not be set together with `peer` + PeerGroups *[]string `json:"peer_groups,omitempty"` } // RouteRequest defines model for RouteRequest. @@ -626,8 +629,11 @@ type RouteRequest struct { // NetworkId Route network identifier, to group HA routes NetworkId string `json:"network_id"` - // Peer Peer Identifier associated with route - Peer string `json:"peer"` + // Peer Peer Identifier associated with route. This property can not be set together with `peer_groups` + Peer *string `json:"peer,omitempty"` + + // PeerGroups Peers Group Identifier associated with route. This property can not be set together with `peer` + PeerGroups *[]string `json:"peer_groups,omitempty"` } // Rule defines model for Rule. diff --git a/management/server/http/routes_handler.go b/management/server/http/routes_handler.go index a6dfa9c74..348bdbfd6 100644 --- a/management/server/http/routes_handler.go +++ b/management/server/http/routes_handler.go @@ -82,7 +82,33 @@ func (h *RoutesHandler) CreateRoute(w http.ResponseWriter, r *http.Request) { return } - newRoute, err := h.accountManager.CreateRoute(account.Id, newPrefix.String(), req.Peer, req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Groups, req.Enabled, user.Id) + peerId := "" + if req.Peer != nil { + peerId = *req.Peer + } + + peerGroupIds := []string{} + if req.PeerGroups != nil { + peerGroupIds = *req.PeerGroups + } + + if (peerId != "" && len(peerGroupIds) > 0) || (peerId == "" && len(peerGroupIds) == 0) { + util.WriteError(status.Errorf(status.InvalidArgument, "only one peer or peer_groups should be provided"), w) + return + } + + // do not allow non Linux peers + if peer := account.GetPeer(peerId); peer != nil { + if peer.Meta.GoOS != "linux" { + util.WriteError(status.Errorf(status.InvalidArgument, "non-linux peers are non supported as network routes"), w) + return + } + } + + newRoute, err := h.accountManager.CreateRoute( + account.Id, newPrefix.String(), peerId, peerGroupIds, + req.Description, req.NetworkId, req.Masquerade, req.Metric, req.Groups, req.Enabled, user.Id, + ) if err != nil { util.WriteError(err, w) return @@ -135,19 +161,49 @@ func (h *RoutesHandler) UpdateRoute(w http.ResponseWriter, r *http.Request) { return } + if req.Peer != nil && req.PeerGroups != nil { + util.WriteError(status.Errorf(status.InvalidArgument, "only peer or peers_group should be provided"), w) + return + } + + if req.Peer == nil && req.PeerGroups == nil { + util.WriteError(status.Errorf(status.InvalidArgument, "either peer or peers_group should be provided"), w) + return + } + + peerID := "" + if req.Peer != nil { + peerID = *req.Peer + } + + // do not allow non Linux peers + if peer := account.GetPeer(peerID); peer != nil { + if peer.Meta.GoOS != "linux" { + util.WriteError(status.Errorf(status.InvalidArgument, "non-linux peers are non supported as network routes"), w) + return + } + } + newRoute := &route.Route{ ID: routeID, Network: newPrefix, NetID: req.NetworkId, NetworkType: prefixType, Masquerade: req.Masquerade, - Peer: req.Peer, Metric: req.Metric, Description: req.Description, Enabled: req.Enabled, Groups: req.Groups, } + if req.Peer != nil { + newRoute.Peer = peerID + } + + if req.PeerGroups != nil { + newRoute.PeerGroups = *req.PeerGroups + } + err = h.accountManager.SaveRoute(account.Id, user.Id, newRoute) if err != nil { util.WriteError(err, w) @@ -208,16 +264,21 @@ func (h *RoutesHandler) GetRoute(w http.ResponseWriter, r *http.Request) { } func toRouteResponse(serverRoute *route.Route) *api.Route { - return &api.Route{ + route := &api.Route{ Id: serverRoute.ID, Description: serverRoute.Description, NetworkId: serverRoute.NetID, Enabled: serverRoute.Enabled, - Peer: serverRoute.Peer, + Peer: &serverRoute.Peer, Network: serverRoute.Network.String(), NetworkType: serverRoute.NetworkType.String(), Masquerade: serverRoute.Masquerade, Metric: serverRoute.Metric, Groups: serverRoute.Groups, } + + if len(serverRoute.PeerGroups) > 0 { + route.PeerGroups = &serverRoute.PeerGroups + } + return route } diff --git a/management/server/http/routes_handler_test.go b/management/server/http/routes_handler_test.go index 3f2b7b910..0f797c3b3 100644 --- a/management/server/http/routes_handler_test.go +++ b/management/server/http/routes_handler_test.go @@ -23,16 +23,23 @@ import ( ) const ( - existingRouteID = "existingRouteID" - notFoundRouteID = "notFoundRouteID" - existingPeerIP = "100.64.0.100" - existingPeerID = "peer-id" - notFoundPeerID = "nonExistingPeer" - existingPeerKey = "existingPeerKey" - testAccountID = "test_id" - existingGroupID = "testGroup" + existingRouteID = "existingRouteID" + existingRouteID2 = "existingRouteID2" // for peer_groups test + notFoundRouteID = "notFoundRouteID" + existingPeerIP1 = "100.64.0.100" + existingPeerIP2 = "100.64.0.101" + notFoundPeerID = "nonExistingPeer" + existingPeerKey = "existingPeerKey" + nonLinuxExistingPeerKey = "darwinExistingPeerKey" + testAccountID = "test_id" + existingGroupID = "testGroup" + notFoundGroupID = "nonExistingGroup" ) +var emptyString = "" +var existingPeerID = "peer-id" +var nonLinuxExistingPeerID = "darwin-peer-id" + var baseExistingRoute = &route.Route{ ID: existingRouteID, Description: "base route", @@ -51,8 +58,19 @@ var testingAccount = &server.Account{ Peers: map[string]*server.Peer{ existingPeerID: { Key: existingPeerKey, - IP: netip.MustParseAddr(existingPeerIP).AsSlice(), + IP: netip.MustParseAddr(existingPeerIP1).AsSlice(), ID: existingPeerID, + Meta: server.PeerSystemMeta{ + GoOS: "linux", + }, + }, + nonLinuxExistingPeerID: { + Key: nonLinuxExistingPeerID, + IP: netip.MustParseAddr(existingPeerIP2).AsSlice(), + ID: nonLinuxExistingPeerID, + Meta: server.PeerSystemMeta{ + GoOS: "darwin", + }, }, }, Users: map[string]*server.User{ @@ -67,17 +85,26 @@ func initRoutesTestData() *RoutesHandler { if routeID == existingRouteID { return baseExistingRoute, nil } + if routeID == existingRouteID2 { + route := baseExistingRoute.Copy() + route.PeerGroups = []string{existingGroupID} + return route, nil + } return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID) }, - CreateRouteFunc: func(accountID string, network, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, _ string) (*route.Route, error) { + CreateRouteFunc: func(accountID, network, peerID string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, _ string) (*route.Route, error) { if peerID == notFoundPeerID { return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) } + if len(peerGroups) > 0 && peerGroups[0] == notFoundGroupID { + return nil, status.Errorf(status.InvalidArgument, "peer groups with ID %s not found", peerGroups[0]) + } networkType, p, _ := route.ParseNetwork(network) return &route.Route{ ID: existingRouteID, NetID: netID, Peer: peerID, + PeerGroups: peerGroups, Network: p, NetworkType: networkType, Description: description, @@ -124,6 +151,9 @@ func initRoutesTestData() *RoutesHandler { } func TestRoutesHandlers(t *testing.T) { + baseExistingRouteWithPeerGroups := baseExistingRoute.Copy() + baseExistingRouteWithPeerGroups.PeerGroups = []string{existingGroupID} + tt := []struct { name string expectedStatus int @@ -147,6 +177,14 @@ func TestRoutesHandlers(t *testing.T) { requestPath: "/api/routes/" + notFoundRouteID, expectedStatus: http.StatusNotFound, }, + { + name: "Get Existing Route with Peer Groups", + requestType: http.MethodGet, + requestPath: "/api/routes/" + existingRouteID2, + expectedStatus: http.StatusOK, + expectedBody: true, + expectedRoute: toRouteResponse(baseExistingRouteWithPeerGroups), + }, { name: "Delete Existing Route", requestType: http.MethodDelete, @@ -173,13 +211,21 @@ func TestRoutesHandlers(t *testing.T) { Description: "Post", NetworkId: "awesomeNet", Network: "192.168.0.0/16", - Peer: existingPeerID, + Peer: &existingPeerID, NetworkType: route.IPv4NetworkString, Masquerade: false, Enabled: false, Groups: []string{existingGroupID}, }, }, + { + name: "POST Non Linux 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\",\"groups\":[\"%s\"]}", nonLinuxExistingPeerID, existingGroupID)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, { name: "POST Not Found Peer", requestType: http.MethodPost, @@ -204,6 +250,24 @@ func TestRoutesHandlers(t *testing.T) { expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, + { + name: "POST UnprocessableEntity when both peer and peer_groups are provided", + 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\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingPeerID, existingGroupID, existingGroupID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "POST UnprocessableEntity when no peer and peer_groups are provided", + requestType: http.MethodPost, + requestPath: "/api/routes", + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"groups\":[\"%s\"]}", existingPeerID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, { name: "PUT OK", requestType: http.MethodPut, @@ -216,7 +280,27 @@ func TestRoutesHandlers(t *testing.T) { Description: "Post", NetworkId: "awesomeNet", Network: "192.168.0.0/16", - Peer: existingPeerID, + Peer: &existingPeerID, + NetworkType: route.IPv4NetworkString, + Masquerade: false, + Enabled: false, + Groups: []string{existingGroupID}, + }, + }, + { + name: "PUT OK when peer_groups provided", + requestType: http.MethodPut, + requestPath: "/api/routes/" + existingRouteID, + requestBody: bytes.NewBufferString(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingGroupID, existingGroupID)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedRoute: &api.Route{ + Id: existingRouteID, + Description: "Post", + NetworkId: "awesomeNet", + Network: "192.168.0.0/16", + Peer: &emptyString, + PeerGroups: &[]string{existingGroupID}, NetworkType: route.IPv4NetworkString, Masquerade: false, Enabled: false, @@ -239,6 +323,14 @@ func TestRoutesHandlers(t *testing.T) { expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, + { + name: "PUT Non Linux 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\",\"groups\":[\"%s\"]}", nonLinuxExistingPeerID, existingGroupID)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, { name: "PUT Invalid Network Identifier", requestType: http.MethodPut, @@ -255,6 +347,24 @@ func TestRoutesHandlers(t *testing.T) { expectedStatus: http.StatusUnprocessableEntity, expectedBody: false, }, + { + name: "PUT UnprocessableEntity when both peer and peer_groups are provided", + requestType: http.MethodPut, + requestPath: "/api/routes/" + existingRouteID, + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"peer\":\"%s\",\"peer_groups\":[\"%s\"],\"groups\":[\"%s\"]}", existingPeerID, existingGroupID, existingGroupID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "PUT UnprocessableEntity when no peer and peer_groups are provided", + requestType: http.MethodPut, + requestPath: "/api/routes/" + existingRouteID, + requestBody: bytes.NewBuffer( + []byte(fmt.Sprintf("{\"Description\":\"Post\",\"Network\":\"192.168.0.0/16\",\"network_id\":\"awesomeNet\",\"groups\":[\"%s\"]}", existingPeerID))), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, } p := initRoutesTestData() diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 24bf9f3c9..ed1130e09 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -33,8 +33,8 @@ type MockAccountManager struct { SaveGroupFunc func(accountID, userID string, group *server.Group) error DeleteGroupFunc func(accountID, userId, groupID string) error ListGroupsFunc func(accountID string) ([]*server.Group, error) - GroupAddPeerFunc func(accountID, groupID, peerKey string) error - GroupDeletePeerFunc func(accountID, groupID, peerKey string) error + GroupAddPeerFunc func(accountID, groupID, peerID string) error + GroupDeletePeerFunc func(accountID, groupID, peerID string) error GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error) GetRuleFunc func(accountID, ruleID, userID string) (*server.Rule, error) SaveRuleFunc func(accountID, userID string, rule *server.Rule) error @@ -50,7 +50,7 @@ type MockAccountManager struct { UpdatePeerMetaFunc func(peerID string, meta server.PeerSystemMeta) error UpdatePeerSSHKeyFunc func(peerID string, sshKey string) error UpdatePeerFunc func(accountID, userID string, peer *server.Peer) (*server.Peer, error) - CreateRouteFunc func(accountID string, prefix, peer, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) + CreateRouteFunc func(accountID, prefix, peer string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) GetRouteFunc func(accountID, routeID, userID string) (*route.Route, error) SaveRouteFunc func(accountID, userID string, route *route.Route) error DeleteRouteFunc func(accountID, routeID, userID string) error @@ -281,17 +281,17 @@ func (am *MockAccountManager) ListGroups(accountID string) ([]*server.Group, err } // GroupAddPeer mock implementation of GroupAddPeer from server.AccountManager interface -func (am *MockAccountManager) GroupAddPeer(accountID, groupID, peerKey string) error { +func (am *MockAccountManager) GroupAddPeer(accountID, groupID, peerID string) error { if am.GroupAddPeerFunc != nil { - return am.GroupAddPeerFunc(accountID, groupID, peerKey) + return am.GroupAddPeerFunc(accountID, groupID, peerID) } return status.Errorf(codes.Unimplemented, "method GroupAddPeer is not implemented") } // GroupDeletePeer mock implementation of GroupDeletePeer from server.AccountManager interface -func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerKey string) error { +func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerID string) error { if am.GroupDeletePeerFunc != nil { - return am.GroupDeletePeerFunc(accountID, groupID, peerKey) + return am.GroupDeletePeerFunc(accountID, groupID, peerID) } return status.Errorf(codes.Unimplemented, "method GroupDeletePeer is not implemented") } @@ -401,9 +401,9 @@ func (am *MockAccountManager) UpdatePeer(accountID, userID string, peer *server. } // CreateRoute mock implementation of CreateRoute from server.AccountManager interface -func (am *MockAccountManager) CreateRoute(accountID string, network, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) { +func (am *MockAccountManager) CreateRoute(accountID, network, peerID string, peerGroups []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) { if am.CreateRouteFunc != nil { - return am.CreateRouteFunc(accountID, network, peerID, description, netID, masquerade, metric, groups, enabled, userID) + return am.CreateRouteFunc(accountID, network, peerID, peerGroups, description, netID, masquerade, metric, groups, enabled, userID) } return nil, status.Errorf(codes.Unimplemented, "method CreateRoute is not implemented") } diff --git a/management/server/route.go b/management/server/route.go index b232c2bb6..cdd214203 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -39,30 +39,82 @@ func (am *DefaultAccountManager) GetRoute(accountID, routeID, userID string) (*r return nil, status.Errorf(status.NotFound, "route with ID %s not found", routeID) } -// checkPrefixPeerExists checks the combination of prefix and peer id, if it exists returns an error, otherwise returns nil -func (am *DefaultAccountManager) checkPrefixPeerExists(accountID, peerID string, prefix netip.Prefix) error { - - if peerID == "" { - return nil - } - - account, err := am.Store.GetAccount(accountID) - if err != nil { - return err - } - +// checkRoutePrefixExistsForPeers checks if a route with a given prefix exists for a single peer or multiple peer groups. +func (am *DefaultAccountManager) checkRoutePrefixExistsForPeers(account *Account, peerID, routeID string, peerGroupIDs []string, prefix netip.Prefix) error { + // routes can have both peer and peer_groups routesWithPrefix := account.GetRoutesByPrefix(prefix) + // lets remember all the peers and the peer groups from routesWithPrefix + seenPeers := make(map[string]bool) + seenPeerGroups := make(map[string]bool) + for _, prefixRoute := range routesWithPrefix { - if prefixRoute.Peer == peerID { - return status.Errorf(status.AlreadyExists, "failed to add route with prefix %s - peer already has this route", prefix.String()) + // we skip route(s) with the same network ID as we want to allow updating of the existing route + // when create a new route routeID is newly generated so nothing will be skipped + if routeID == prefixRoute.ID { + continue + } + + if prefixRoute.Peer != "" { + seenPeers[prefixRoute.ID] = true + } + for _, groupID := range prefixRoute.PeerGroups { + seenPeerGroups[groupID] = true + + group := account.GetGroup(groupID) + if group == nil { + return status.Errorf( + status.InvalidArgument, "failed to add route with prefix %s - peer group %s doesn't exist", + prefix.String(), groupID) + } + + for _, pID := range group.Peers { + seenPeers[pID] = true + } } } + + if peerID != "" { + // check that peerID exists and is not in any route as single peer or part of the group + peer := account.GetPeer(peerID) + if peer == nil { + return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) + } + if _, ok := seenPeers[peerID]; ok { + return status.Errorf(status.AlreadyExists, + "failed to add route with prefix %s - peer %s already has this route", prefix.String(), peerID) + } + } + + // check that peerGroupIDs are not in any route peerGroups list + for _, groupID := range peerGroupIDs { + group := account.GetGroup(groupID) // we validated the group existent before entering this function, o need to check again. + + if _, ok := seenPeerGroups[groupID]; ok { + return status.Errorf( + status.AlreadyExists, "failed to add route with prefix %s - peer group %s already has this route", + prefix.String(), group.Name) + } + + // check that the peers from peerGroupIDs groups are not the same peers we saw in routesWithPrefix + for _, id := range group.Peers { + if _, ok := seenPeers[id]; ok { + peer := account.GetPeer(peerID) + if peer == nil { + return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) + } + return status.Errorf(status.AlreadyExists, + "failed to add route with prefix %s - peer %s from the group %s already has this route", + prefix.String(), peer.Name, group.Name) + } + } + } + return nil } // CreateRoute creates and saves a new route -func (am *DefaultAccountManager) CreateRoute(accountID string, network, peerID, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) { +func (am *DefaultAccountManager) CreateRoute(accountID, network, peerID string, peerGroupIDs []string, description, netID string, masquerade bool, metric int, groups []string, enabled bool, userID string) (*route.Route, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -71,19 +123,29 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peerID, return nil, err } - if peerID != "" { - peer := account.GetPeer(peerID) - if peer == nil { - return nil, status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) - } + if peerID != "" && len(peerGroupIDs) != 0 { + return nil, status.Errorf( + status.InvalidArgument, + "peer with ID %s and peers group %s should not be provided at the same time", + peerID, peerGroupIDs) } var newRoute route.Route + newRoute.ID = xid.New().String() + prefixType, newPrefix, err := route.ParseNetwork(network) if err != nil { return nil, status.Errorf(status.InvalidArgument, "failed to parse IP %s", network) } - err = am.checkPrefixPeerExists(accountID, peerID, newPrefix) + + if len(peerGroupIDs) > 0 { + err = validateGroups(peerGroupIDs, account.Groups) + if err != nil { + return nil, err + } + } + + err = am.checkRoutePrefixExistsForPeers(account, peerID, newRoute.ID, peerGroupIDs, newPrefix) if err != nil { return nil, err } @@ -102,7 +164,7 @@ func (am *DefaultAccountManager) CreateRoute(accountID string, network, peerID, } newRoute.Peer = peerID - newRoute.ID = xid.New().String() + newRoute.PeerGroups = peerGroupIDs newRoute.Network = newPrefix newRoute.NetworkType = prefixType newRoute.Description = description @@ -160,13 +222,22 @@ func (am *DefaultAccountManager) SaveRoute(accountID, userID string, routeToSave return err } - if routeToSave.Peer != "" { - peer := account.GetPeer(routeToSave.Peer) - if peer == nil { - return status.Errorf(status.InvalidArgument, "peer with ID %s not found", routeToSave.Peer) + if routeToSave.Peer != "" && len(routeToSave.PeerGroups) != 0 { + return status.Errorf(status.InvalidArgument, "peer with ID and peer groups should not be provided at the same time") + } + + if len(routeToSave.PeerGroups) > 0 { + err = validateGroups(routeToSave.PeerGroups, account.Groups) + if err != nil { + return err } } + err = am.checkRoutePrefixExistsForPeers(account, routeToSave.Peer, routeToSave.ID, routeToSave.Copy().PeerGroups, routeToSave.Network) + if err != nil { + return err + } + err = validateGroups(routeToSave.Groups, account.Groups) if err != nil { return err diff --git a/management/server/route_test.go b/management/server/route_test.go index 81ce21a3f..32f15843b 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -14,24 +14,37 @@ import ( const ( peer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=" peer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI=" + peer3Key = "ayF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NaF=" + peer4Key = "ayF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5acc=" + peer5Key = "ayF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5a55=" peer1ID = "peer-1-id" peer2ID = "peer-2-id" + peer3ID = "peer-3-id" + peer4ID = "peer-4-id" + peer5ID = "peer-5-id" routeGroup1 = "routeGroup1" routeGroup2 = "routeGroup2" + routeGroup3 = "routeGroup3" // for existing route + routeGroup4 = "routeGroup4" // for existing route + routeGroupHA1 = "routeGroupHA1" + routeGroupHA2 = "routeGroupHA2" routeInvalidGroup1 = "routeInvalidGroup1" userID = "testingUser" + existingNetwork = "10.10.10.0/24" + existingRouteID = "random-id" ) func TestCreateRoute(t *testing.T) { type input struct { - network string - netID string - peerKey string - description string - masquerade bool - metric int - enabled bool - groups []string + network string + netID string + peerKey string + peerGroupIDs []string + description string + masquerade bool + metric int + enabled bool + groups []string } testCases := []struct { @@ -67,6 +80,48 @@ func TestCreateRoute(t *testing.T) { Groups: []string{routeGroup1}, }, }, + { + name: "Happy Path Peer Groups", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "happy", + peerGroupIDs: []string{routeGroupHA1, routeGroupHA2}, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1, routeGroup2}, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetworkType: route.IPv4Network, + NetID: "happy", + PeerGroups: []string{routeGroupHA1, routeGroupHA2}, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1, routeGroup2}, + }, + }, + { + name: "Both peer and peer_groups Provided Should Fail", + inputArgs: input{ + network: "192.168.0.0/16", + netID: "happy", + peerKey: peer1ID, + peerGroupIDs: []string{routeGroupHA1}, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, + }, + errFunc: require.Error, + shouldCreate: false, + }, { name: "Bad Prefix Should Fail", inputArgs: input{ @@ -97,6 +152,36 @@ func TestCreateRoute(t *testing.T) { errFunc: require.Error, shouldCreate: false, }, + { + name: "Bad Peer already has this route", + inputArgs: input{ + network: existingNetwork, + netID: "bad", + peerKey: peer5ID, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Bad Peers Group already has this route", + inputArgs: input{ + network: existingNetwork, + netID: "bad", + peerGroupIDs: []string{routeGroup1, routeGroup3}, + description: "super", + masquerade: false, + metric: 9999, + enabled: true, + groups: []string{routeGroup1}, + }, + errFunc: require.Error, + shouldCreate: false, + }, { name: "Empty Peer Should Create", inputArgs: input{ @@ -238,13 +323,14 @@ func TestCreateRoute(t *testing.T) { account, err := initTestRouteAccount(t, am) if err != nil { - t.Error("failed to init testing account") + t.Errorf("failed to init testing account: %s", err) } outRoute, err := am.CreateRoute( account.Id, testCase.inputArgs.network, testCase.inputArgs.peerKey, + testCase.inputArgs.peerGroupIDs, testCase.inputArgs.description, testCase.inputArgs.netID, testCase.inputArgs.masquerade, @@ -272,6 +358,7 @@ func TestCreateRoute(t *testing.T) { func TestSaveRoute(t *testing.T) { validPeer := peer2ID + validUsedPeer := peer5ID invalidPeer := "nonExisting" validPrefix := netip.MustParsePrefix("192.168.0.0/24") invalidPrefix, _ := netip.ParsePrefix("192.168.0.0/34") @@ -279,11 +366,14 @@ func TestSaveRoute(t *testing.T) { invalidMetric := 99999 validNetID := "12345678901234567890qw" invalidNetID := "12345678901234567890qwertyuiopqwertyuiop1" + validGroupHA1 := routeGroupHA1 + validGroupHA2 := routeGroupHA2 testCases := []struct { name string existingRoute *route.Route newPeer *string + newPeerGroups []string newMetric *int newPrefix *netip.Prefix newGroups []string @@ -325,6 +415,55 @@ func TestSaveRoute(t *testing.T) { Groups: []string{routeGroup2}, }, }, + { + name: "Happy Path Peer Groups", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPeerGroups: []string{validGroupHA1, validGroupHA2}, + newMetric: &validMetric, + newPrefix: &validPrefix, + newGroups: []string{routeGroup2}, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: "testingRoute", + Network: validPrefix, + NetID: validNetID, + NetworkType: route.IPv4Network, + PeerGroups: []string{validGroupHA1, validGroupHA2}, + Description: "super", + Masquerade: false, + Metric: validMetric, + Enabled: true, + Groups: []string{routeGroup2}, + }, + }, + { + name: "Both peer and peers_roup Provided Should Fail", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: validNetID, + NetworkType: route.IPv4Network, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPeer: &validPeer, + newPeerGroups: []string{validGroupHA1}, + errFunc: require.Error, + }, { name: "Bad Prefix Should Fail", existingRoute: &route.Route{ @@ -461,6 +600,71 @@ func TestSaveRoute(t *testing.T) { newGroups: []string{routeInvalidGroup1}, errFunc: require.Error, }, + { + name: "Allow to modify existing route with new peer", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix(existingNetwork), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPeer: &validPeer, + errFunc: require.NoError, + shouldCreate: true, + expectedRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix(existingNetwork), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: validPeer, + PeerGroups: []string{}, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + }, + { + name: "Do not allow to modify existing route with a peer from another route", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix(existingNetwork), + NetID: validNetID, + NetworkType: route.IPv4Network, + Peer: peer1ID, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPeer: &validUsedPeer, + errFunc: require.Error, + }, + { + name: "Do not allow to modify existing route with a peers group from another route", + existingRoute: &route.Route{ + ID: "testingRoute", + Network: netip.MustParsePrefix(existingNetwork), + NetID: validNetID, + NetworkType: route.IPv4Network, + PeerGroups: []string{routeGroup3}, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + }, + newPeerGroups: []string{routeGroup4}, + errFunc: require.Error, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -488,6 +692,9 @@ func TestSaveRoute(t *testing.T) { if testCase.newPeer != nil { routeToSave.Peer = *testCase.newPeer } + if len(testCase.newPeerGroups) != 0 { + routeToSave.PeerGroups = testCase.newPeerGroups + } if testCase.newMetric != nil { routeToSave.Metric = *testCase.newMetric } @@ -569,6 +776,96 @@ func TestDeleteRoute(t *testing.T) { } } +func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) { + baseRoute := &route.Route{ + Network: netip.MustParsePrefix("192.168.0.0/16"), + NetID: "superNet", + NetworkType: route.IPv4Network, + PeerGroups: []string{routeGroupHA1, routeGroupHA2}, + Description: "ha route", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1, routeGroup2}, + } + + 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(peer1ID) + require.NoError(t, err) + require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes") + + newRoute, err := am.CreateRoute( + account.Id, baseRoute.Network.String(), baseRoute.Peer, baseRoute.PeerGroups, baseRoute.Description, + baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, baseRoute.Enabled, userID) + require.NoError(t, err) + require.Equal(t, newRoute.Enabled, true) + + peer1Routes, err := am.GetNetworkMap(peer1ID) + require.NoError(t, err) + require.Len(t, peer1Routes.Routes, 3, "HA route should have more than 1 routes") + + peer2Routes, err := am.GetNetworkMap(peer2ID) + require.NoError(t, err) + require.Len(t, peer2Routes.Routes, 3, "HA route should have more than 1 routes") + + peer4Routes, err := am.GetNetworkMap(peer4ID) + require.NoError(t, err) + require.Len(t, peer4Routes.Routes, 3, "HA route should have more than 1 routes") + + groups, err := am.ListGroups(account.Id) + require.NoError(t, err) + var groupHA1, groupHA2 *Group + for _, group := range groups { + switch group.Name { + case routeGroupHA1: + groupHA1 = group + case routeGroupHA2: + groupHA2 = group + } + } + + err = am.GroupDeletePeer(account.Id, groupHA1.ID, peer2ID) + require.NoError(t, err) + + peer2RoutesAfterDelete, err := am.GetNetworkMap(peer2ID) + require.NoError(t, err) + require.Len(t, peer2RoutesAfterDelete.Routes, 2, "after peer deletion group should have only 2 route") + + err = am.GroupDeletePeer(account.Id, groupHA2.ID, peer4ID) + require.NoError(t, err) + + peer2RoutesAfterDelete, err = am.GetNetworkMap(peer2ID) + require.NoError(t, err) + require.Len(t, peer2RoutesAfterDelete.Routes, 1, "after peer deletion group should have only 1 route") + + err = am.GroupAddPeer(account.Id, groupHA2.ID, peer4ID) + require.NoError(t, err) + + peer1RoutesAfterAdd, err := am.GetNetworkMap(peer1ID) + require.NoError(t, err) + require.Len(t, peer1RoutesAfterAdd.Routes, 2, "HA route should have more than 1 route") + + peer2RoutesAfterAdd, err := am.GetNetworkMap(peer2ID) + require.NoError(t, err) + require.Len(t, peer2RoutesAfterAdd.Routes, 2, "HA route should have more than 1 route") + + err = am.DeleteRoute(account.Id, newRoute.ID, userID) + require.NoError(t, err) + + peer1DeletedRoute, err := am.GetNetworkMap(peer1ID) + require.NoError(t, err) + require.Len(t, peer1DeletedRoute.Routes, 0, "we should receive one route for peer1") +} + func TestGetNetworkMap_RouteSync(t *testing.T) { // no routes for peer in different groups // no routes when route is deleted @@ -599,7 +896,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { require.NoError(t, err) require.Len(t, newAccountRoutes.Routes, 0, "new accounts should have no routes") - createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network.String(), peer1ID, + createdRoute, err := am.CreateRoute(account.Id, baseRoute.Network.String(), peer1ID, []string{}, baseRoute.Description, baseRoute.NetID, baseRoute.Masquerade, baseRoute.Metric, baseRoute.Groups, false, userID) require.NoError(t, err) @@ -695,6 +992,8 @@ func createRouterStore(t *testing.T) (Store, error) { } func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) { + t.Helper() + accountID := "testingAcc" domain := "example.com" @@ -754,6 +1053,81 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er } account.Peers[peer2.ID] = peer2 + ips = account.getTakenIPs() + peer3IP, err := AllocatePeerIP(account.Network.Net, ips) + if err != nil { + return nil, err + } + + peer3 := &Peer{ + IP: peer3IP, + ID: peer3ID, + Key: peer3Key, + Name: "test-host3@netbird.io", + UserID: userID, + Meta: PeerSystemMeta{ + Hostname: "test-host3@netbird.io", + GoOS: "darwin", + Kernel: "Darwin", + Core: "13.4.1", + Platform: "arm64", + OS: "darwin", + WtVersion: "development", + UIVersion: "development", + }, + } + account.Peers[peer3.ID] = peer3 + + ips = account.getTakenIPs() + peer4IP, err := AllocatePeerIP(account.Network.Net, ips) + if err != nil { + return nil, err + } + + peer4 := &Peer{ + IP: peer4IP, + ID: peer4ID, + Key: peer4Key, + Name: "test-host4@netbird.io", + UserID: userID, + Meta: PeerSystemMeta{ + Hostname: "test-host4@netbird.io", + GoOS: "linux", + Kernel: "Linux", + Core: "21.04", + Platform: "x86_64", + OS: "Ubuntu", + WtVersion: "development", + UIVersion: "development", + }, + } + account.Peers[peer4.ID] = peer4 + + ips = account.getTakenIPs() + peer5IP, err := AllocatePeerIP(account.Network.Net, ips) + if err != nil { + return nil, err + } + + peer5 := &Peer{ + IP: peer5IP, + ID: peer5ID, + Key: peer5Key, + Name: "test-host4@netbird.io", + UserID: userID, + Meta: PeerSystemMeta{ + Hostname: "test-host4@netbird.io", + GoOS: "linux", + Kernel: "Linux", + Core: "21.04", + Platform: "x86_64", + OS: "Ubuntu", + WtVersion: "development", + UIVersion: "development", + }, + } + account.Peers[peer5.ID] = peer5 + err = am.Store.SaveAccount(account) if err != nil { return nil, err @@ -770,24 +1144,57 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er if err != nil { return nil, err } - - newGroup := &Group{ - ID: routeGroup1, - Name: routeGroup1, - Peers: []string{peer1.ID}, + err = am.GroupAddPeer(accountID, groupAll.ID, peer3ID) + if err != nil { + return nil, err } - err = am.SaveGroup(accountID, userID, newGroup) + err = am.GroupAddPeer(accountID, groupAll.ID, peer4ID) if err != nil { return nil, err } - newGroup = &Group{ - ID: routeGroup2, - Name: routeGroup2, - Peers: []string{peer2.ID}, + newGroup := []*Group{ + { + ID: routeGroup1, + Name: routeGroup1, + Peers: []string{peer1.ID}, + }, + { + ID: routeGroup2, + Name: routeGroup2, + Peers: []string{peer2.ID}, + }, + { + ID: routeGroup3, + Name: routeGroup3, + Peers: []string{peer5.ID}, + }, + { + ID: routeGroup4, + Name: routeGroup4, + Peers: []string{peer5.ID}, + }, + { + ID: routeGroupHA1, + Name: routeGroupHA1, + Peers: []string{peer1.ID, peer2.ID, peer3.ID}, // we have one non Linux peer, see peer3 + }, + { + ID: routeGroupHA2, + Name: routeGroupHA2, + Peers: []string{peer1.ID, peer4.ID}, + }, } - err = am.SaveGroup(accountID, userID, newGroup) + for _, group := range newGroup { + err = am.SaveGroup(accountID, userID, group) + if err != nil { + return nil, err + } + } + + _, err = am.CreateRoute(account.Id, existingNetwork, "", []string{routeGroup3, routeGroup4}, + "", existingRouteID, false, 1000, []string{groupAll.ID}, true, userID) if err != nil { return nil, err } diff --git a/route/route.go b/route/route.go index 5c45e2cf5..eb7bcba2f 100644 --- a/route/route.go +++ b/route/route.go @@ -70,6 +70,7 @@ type Route struct { NetID string Description string Peer string + PeerGroups []string NetworkType NetworkType Masquerade bool Metric int @@ -79,7 +80,7 @@ type Route struct { // EventMeta returns activity event meta related to the route func (r *Route) EventMeta() map[string]any { - return map[string]any{"name": r.NetID, "network_range": r.Network.String(), "peer_id": r.Peer} + return map[string]any{"name": r.NetID, "network_range": r.Network.String(), "peer_id": r.Peer, "peer_groups": r.PeerGroups} } // Copy copies a route object @@ -91,12 +92,14 @@ func (r *Route) Copy() *Route { Network: r.Network, NetworkType: r.NetworkType, Peer: r.Peer, + PeerGroups: make([]string, len(r.PeerGroups)), Metric: r.Metric, Masquerade: r.Masquerade, Enabled: r.Enabled, Groups: make([]string, len(r.Groups)), } copy(route.Groups, r.Groups) + copy(route.PeerGroups, r.PeerGroups) return route } @@ -111,7 +114,8 @@ func (r *Route) IsEqual(other *Route) bool { other.Metric == r.Metric && other.Masquerade == r.Masquerade && other.Enabled == r.Enabled && - compareGroupsList(r.Groups, other.Groups) + compareList(r.Groups, other.Groups) && + compareList(r.PeerGroups, other.PeerGroups) } // ParseNetwork Parses a network prefix string and returns a netip.Prefix object and if is invalid, IPv4 or IPv6 @@ -134,7 +138,7 @@ func ParseNetwork(networkString string) (NetworkType, netip.Prefix, error) { return IPv4Network, masked, nil } -func compareGroupsList(list, other []string) bool { +func compareList(list, other []string) bool { if len(list) != len(other) { return false }