diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 2e4b9677a..1827cc533 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -75,13 +75,13 @@ jobs: - run: chmod +x *testing.bin - name: Run Iface tests in docker - run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/iface --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/iface-testing.bin + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/iface --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/iface-testing.bin -test.timeout 5m -test.parallel 1 - name: Run RouteManager tests in docker - run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1 - name: Run Engine tests in docker - run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1 - name: Run Peer tests in docker - run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin \ No newline at end of file + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1 \ No newline at end of file diff --git a/dns/dns.go b/dns/dns.go new file mode 100644 index 000000000..e34218554 --- /dev/null +++ b/dns/dns.go @@ -0,0 +1,6 @@ +// Package dns implement dns types and standard methods and functions +// to parse and normalize dns records and configuration +package dns + +// DefaultDNSPort well-known port number +const DefaultDNSPort = 53 diff --git a/dns/nameserver.go b/dns/nameserver.go new file mode 100644 index 000000000..4f9de57e7 --- /dev/null +++ b/dns/nameserver.go @@ -0,0 +1,183 @@ +package dns + +import ( + "fmt" + "net/netip" + "net/url" + "strconv" + "strings" +) + +const ( + // MaxGroupNameChar maximum group name size + MaxGroupNameChar = 40 + // InvalidNameServerType invalid nameserver type + InvalidNameServerType NameServerType = iota + // UDPNameServerType udp nameserver type + UDPNameServerType +) + +const ( + // InvalidNameServerTypeString invalid nameserver type as string + InvalidNameServerTypeString = "invalid" + // UDPNameServerTypeString udp nameserver type as string + UDPNameServerTypeString = "udp" +) + +// NameServerType nameserver type +type NameServerType int + +// String returns nameserver type string +func (n NameServerType) String() string { + switch n { + case UDPNameServerType: + return UDPNameServerTypeString + default: + return InvalidNameServerTypeString + } +} + +// ToNameServerType returns a nameserver type +func ToNameServerType(typeString string) NameServerType { + switch typeString { + case UDPNameServerTypeString: + return UDPNameServerType + default: + return InvalidNameServerType + } +} + +// NameServerGroup group of nameservers and with group ids +type NameServerGroup struct { + // ID identifier of group + ID string + // Name group name + Name string + // Description group description + Description string + // NameServers list of nameservers + NameServers []NameServer + // Groups list of peer group IDs to distribute the nameservers information + Groups []string + // Enabled group status + Enabled bool +} + +// NameServer represents a DNS nameserver +type NameServer struct { + // IP address of nameserver + IP netip.Addr + // NSType nameserver type + NSType NameServerType + // Port nameserver listening port + Port int +} + +// Copy copies a nameserver object +func (n *NameServer) Copy() *NameServer { + return &NameServer{ + IP: n.IP, + NSType: n.NSType, + Port: n.Port, + } +} + +// IsEqual compares one nameserver with the other +func (n *NameServer) IsEqual(other *NameServer) bool { + return other.IP == n.IP && + other.NSType == n.NSType && + other.Port == n.Port +} + +// ParseNameServerURL parses a nameserver url in the format ://:, e.g., udp://1.1.1.1:53 +func ParseNameServerURL(nsURL string) (NameServer, error) { + parsedURL, err := url.Parse(nsURL) + if err != nil { + return NameServer{}, err + } + var ns NameServer + parsedScheme := strings.ToLower(parsedURL.Scheme) + nsType := ToNameServerType(parsedScheme) + if nsType == InvalidNameServerType { + return NameServer{}, fmt.Errorf("invalid nameserver url schema type, got %s", parsedScheme) + } + ns.NSType = nsType + + parsedPort, err := strconv.Atoi(parsedURL.Port()) + if err != nil { + return NameServer{}, fmt.Errorf("invalid nameserver url port, got %s", parsedURL.Port()) + } + ns.Port = parsedPort + + parsedAddr, err := netip.ParseAddr(parsedURL.Hostname()) + if err != nil { + return NameServer{}, fmt.Errorf("invalid nameserver url IP, got %s", parsedURL.Hostname()) + } + + ns.IP = parsedAddr + + return ns, nil +} + +// Copy copies a nameserver group object +func (g *NameServerGroup) Copy() *NameServerGroup { + return &NameServerGroup{ + ID: g.ID, + Name: g.Name, + Description: g.Description, + NameServers: g.NameServers, + Groups: g.Groups, + } +} + +// IsEqual compares one nameserver group with the other +func (g *NameServerGroup) IsEqual(other *NameServerGroup) bool { + return other.ID == g.ID && + other.Name == g.Name && + other.Description == g.Description && + compareNameServerList(g.NameServers, other.NameServers) && + compareGroupsList(g.Groups, other.Groups) +} + +func compareNameServerList(list, other []NameServer) bool { + if len(list) != len(other) { + return false + } + + for _, ns := range list { + if !containsNameServer(ns, other) { + return false + } + } + + return true +} + +func containsNameServer(element NameServer, list []NameServer) bool { + for _, ns := range list { + if ns.IsEqual(&element) { + return true + } + } + return false +} + +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 +} diff --git a/management/proto/generate.sh b/management/proto/generate.sh index a07eebc4e..64aef891e 100755 --- a/management/proto/generate.sh +++ b/management/proto/generate.sh @@ -1,4 +1,17 @@ #!/bin/bash +set -e + +if ! which realpath > /dev/null 2>&1 +then + echo realpath is not installed + echo run: brew install coreutils + exit 1 +fi + +old_pwd=$(pwd) +script_path=$(dirname $(realpath "$0")) +cd "$script_path" go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26 go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1 -protoc -I proto/ proto/management.proto --go_out=. --go-grpc_out=. \ No newline at end of file +protoc -I ./ ./management.proto --go_out=../ --go-grpc_out=../ +cd "$old_pwd" \ No newline at end of file diff --git a/management/server/account.go b/management/server/account.go index e84ae6e09..408898bcf 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/eko/gocache/v2/cache" cacheStore "github.com/eko/gocache/v2/store" + nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/route" @@ -40,6 +41,7 @@ type AccountManager interface { autoGroups []string, ) (*SetupKey, error) SaveSetupKey(accountID string, key *SetupKey) (*SetupKey, error) + ListSetupKeys(accountID string) ([]*SetupKey, error) SaveUser(accountID string, key *User) (*UserInfo, error) GetSetupKey(accountID, keyID string) (*SetupKey, error) GetAccountById(accountId string) (*Account, error) @@ -78,7 +80,12 @@ type AccountManager interface { UpdateRoute(accountID string, routeID string, operations []RouteUpdateOperation) (*route.Route, error) DeleteRoute(accountID, routeID string) error ListRoutes(accountID string) ([]*route.Route, error) - ListSetupKeys(accountID string) ([]*SetupKey, error) + GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) + CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) + SaveNameServerGroup(accountID string, nsGroupToSave *nbdns.NameServerGroup) error + UpdateNameServerGroup(accountID, nsGroupID string, operations []NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) + DeleteNameServerGroup(accountID, nsGroupID string) error + ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error) } type DefaultAccountManager struct { @@ -106,6 +113,7 @@ type Account struct { Groups map[string]*Group Rules map[string]*Rule Routes map[string]*route.Route + NameServerGroups map[string]*nbdns.NameServerGroup } type UserInfo struct { @@ -142,15 +150,27 @@ func (a *Account) Copy() *Account { rules[id] = rule.Copy() } + routes := map[string]*route.Route{} + for id, route := range a.Routes { + routes[id] = route.Copy() + } + + nsGroups := map[string]*nbdns.NameServerGroup{} + for id, nsGroup := range a.NameServerGroups { + nsGroups[id] = nsGroup.Copy() + } + return &Account{ - Id: a.Id, - CreatedBy: a.CreatedBy, - SetupKeys: setupKeys, - Network: a.Network.Copy(), - Peers: peers, - Users: users, - Groups: groups, - Rules: rules, + Id: a.Id, + CreatedBy: a.CreatedBy, + SetupKeys: setupKeys, + Network: a.Network.Copy(), + Peers: peers, + Users: users, + Groups: groups, + Rules: rules, + Routes: routes, + NameServerGroups: nsGroups, } } @@ -583,18 +603,20 @@ func newAccountWithId(accountId, userId, domain string) *Account { peers := make(map[string]*Peer) users := make(map[string]*User) routes := make(map[string]*route.Route) + nameServersGroups := make(map[string]*nbdns.NameServerGroup) users[userId] = NewAdminUser(userId) log.Debugf("created new account %s with setup key %s", accountId, defaultKey.Key) acc := &Account{ - Id: accountId, - SetupKeys: setupKeys, - Network: network, - Peers: peers, - Users: users, - CreatedBy: userId, - Domain: domain, - Routes: routes, + Id: accountId, + SetupKeys: setupKeys, + Network: network, + Peers: peers, + Users: users, + CreatedBy: userId, + Domain: domain, + Routes: routes, + NameServerGroups: nameServersGroups, } addAllGroup(acc) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 501567c99..099e696a9 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -1,6 +1,7 @@ package mock_server import ( + nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/route" @@ -53,6 +54,12 @@ type MockAccountManager struct { SaveSetupKeyFunc func(accountID string, key *server.SetupKey) (*server.SetupKey, error) ListSetupKeysFunc func(accountID string) ([]*server.SetupKey, error) SaveUserFunc func(accountID string, user *server.User) (*server.UserInfo, error) + GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) + CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) + SaveNameServerGroupFunc func(accountID string, nsGroupToSave *nbdns.NameServerGroup) error + UpdateNameServerGroupFunc func(accountID, nsGroupID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) + DeleteNameServerGroupFunc func(accountID, nsGroupID string) error + ListNameServerGroupsFunc func(accountID string) ([]*nbdns.NameServerGroup, error) } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -430,3 +437,51 @@ func (am *MockAccountManager) SaveUser(accountID string, user *server.User) (*se } return nil, status.Errorf(codes.Unimplemented, "method SaveUser is not implemented") } + +// GetNameServerGroup mocks GetNameServerGroup of the AccountManager interface +func (am *MockAccountManager) GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) { + if am.GetNameServerGroupFunc != nil { + return am.GetNameServerGroupFunc(accountID, nsGroupID) + } + return nil, nil +} + +// CreateNameServerGroup mocks CreateNameServerGroup of the AccountManager interface +func (am *MockAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, error) { + if am.CreateNameServerGroupFunc != nil { + return am.CreateNameServerGroupFunc(accountID, name, description, nameServerList, groups, enabled) + } + return nil, nil +} + +// SaveNameServerGroup mocks SaveNameServerGroup of the AccountManager interface +func (am *MockAccountManager) SaveNameServerGroup(accountID string, nsGroupToSave *nbdns.NameServerGroup) error { + if am.SaveNameServerGroupFunc != nil { + return am.SaveNameServerGroupFunc(accountID, nsGroupToSave) + } + return nil +} + +// UpdateNameServerGroup mocks UpdateNameServerGroup of the AccountManager interface +func (am *MockAccountManager) UpdateNameServerGroup(accountID, nsGroupID string, operations []server.NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, error) { + if am.UpdateNameServerGroupFunc != nil { + return am.UpdateNameServerGroupFunc(accountID, nsGroupID, operations) + } + return nil, nil +} + +// DeleteNameServerGroup mocks DeleteNameServerGroup of the AccountManager interface +func (am *MockAccountManager) DeleteNameServerGroup(accountID, nsGroupID string) error { + if am.DeleteNameServerGroupFunc != nil { + return am.DeleteNameServerGroupFunc(accountID, nsGroupID) + } + return nil +} + +// ListNameServerGroups mocks ListNameServerGroups of the AccountManager interface +func (am *MockAccountManager) ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error) { + if am.ListNameServerGroupsFunc != nil { + return am.ListNameServerGroupsFunc(accountID) + } + return nil, nil +} diff --git a/management/server/nameserver.go b/management/server/nameserver.go new file mode 100644 index 000000000..9a0468b05 --- /dev/null +++ b/management/server/nameserver.go @@ -0,0 +1,333 @@ +package server + +import ( + nbdns "github.com/netbirdio/netbird/dns" + "github.com/rs/xid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "strconv" + "unicode/utf8" +) + +const ( + // UpdateNameServerGroupName indicates a nameserver group name update operation + UpdateNameServerGroupName NameServerGroupUpdateOperationType = iota + // UpdateNameServerGroupDescription indicates a nameserver group description update operation + UpdateNameServerGroupDescription + // UpdateNameServerGroupNameServers indicates a nameserver group nameservers list update operation + UpdateNameServerGroupNameServers + // UpdateNameServerGroupGroups indicates a nameserver group' groups update operation + UpdateNameServerGroupGroups + // UpdateNameServerGroupEnabled indicates a nameserver group status update operation + UpdateNameServerGroupEnabled +) + +// NameServerGroupUpdateOperationType operation type +type NameServerGroupUpdateOperationType int + +func (t NameServerGroupUpdateOperationType) String() string { + switch t { + case UpdateNameServerGroupDescription: + return "UpdateNameServerGroupDescription" + case UpdateNameServerGroupName: + return "UpdateNameServerGroupName" + case UpdateNameServerGroupNameServers: + return "UpdateNameServerGroupNameServers" + case UpdateNameServerGroupGroups: + return "UpdateNameServerGroupGroups" + case UpdateNameServerGroupEnabled: + return "UpdateNameServerGroupEnabled" + default: + return "InvalidOperation" + } +} + +// NameServerGroupUpdateOperation operation object with type and values to be applied +type NameServerGroupUpdateOperation struct { + Type NameServerGroupUpdateOperationType + Values []string +} + +// GetNameServerGroup gets a nameserver group object from account and nameserver group IDs +func (am *DefaultAccountManager) GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, 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") + } + + nsGroup, found := account.NameServerGroups[nsGroupID] + if found { + return nsGroup.Copy(), nil + } + + return nil, status.Errorf(codes.NotFound, "nameserver group with ID %s not found", nsGroupID) +} + +// CreateNameServerGroup creates and saves a new nameserver group +func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, enabled bool) (*nbdns.NameServerGroup, 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") + } + + newNSGroup := &nbdns.NameServerGroup{ + ID: xid.New().String(), + Name: name, + Description: description, + NameServers: nameServerList, + Groups: groups, + Enabled: enabled, + } + + err = validateNameServerGroup(false, newNSGroup, account) + if err != nil { + return nil, err + } + + if account.NameServerGroups == nil { + account.NameServerGroups = make(map[string]*nbdns.NameServerGroup) + } + + account.NameServerGroups[newNSGroup.ID] = newNSGroup + + account.Network.IncSerial() + err = am.Store.SaveAccount(account) + if err != nil { + return nil, err + } + + return newNSGroup.Copy(), nil +} + +// SaveNameServerGroup saves nameserver group +func (am *DefaultAccountManager) SaveNameServerGroup(accountID string, nsGroupToSave *nbdns.NameServerGroup) error { + am.mux.Lock() + defer am.mux.Unlock() + + if nsGroupToSave == nil { + return status.Errorf(codes.InvalidArgument, "nameserver group provided is nil") + } + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return status.Errorf(codes.NotFound, "account not found") + } + + err = validateNameServerGroup(true, nsGroupToSave, account) + if err != nil { + return err + } + + account.NameServerGroups[nsGroupToSave.ID] = nsGroupToSave + + account.Network.IncSerial() + err = am.Store.SaveAccount(account) + if err != nil { + return err + } + + return nil +} + +// UpdateNameServerGroup updates existing nameserver group with set of operations +func (am *DefaultAccountManager) UpdateNameServerGroup(accountID, nsGroupID string, operations []NameServerGroupUpdateOperation) (*nbdns.NameServerGroup, 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") + } + + if len(operations) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "operations shouldn't be empty") + } + + nsGroupToUpdate, ok := account.NameServerGroups[nsGroupID] + if !ok { + return nil, status.Errorf(codes.NotFound, "nameserver group ID %s no longer exists", nsGroupID) + } + + newNSGroup := nsGroupToUpdate.Copy() + + for _, operation := range operations { + valuesCount := len(operation.Values) + if valuesCount < 1 { + return nil, status.Errorf(codes.InvalidArgument, "operation %s contains invalid number of values, it should be at least 1", operation.Type.String()) + } + + for _, value := range operation.Values { + if value == "" { + return nil, status.Errorf(codes.InvalidArgument, "operation %s contains invalid empty string value", operation.Type.String()) + } + } + switch operation.Type { + case UpdateNameServerGroupDescription: + newNSGroup.Description = operation.Values[0] + case UpdateNameServerGroupName: + if valuesCount > 1 { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse name values, expected 1 value got %d", valuesCount) + } + err = validateNSGroupName(operation.Values[0], nsGroupID, account.NameServerGroups) + if err != nil { + return nil, err + } + newNSGroup.Name = operation.Values[0] + case UpdateNameServerGroupNameServers: + var nsList []nbdns.NameServer + for _, url := range operation.Values { + ns, err := nbdns.ParseNameServerURL(url) + if err != nil { + return nil, err + } + nsList = append(nsList, ns) + } + err = validateNSList(nsList) + if err != nil { + return nil, err + } + newNSGroup.NameServers = nsList + case UpdateNameServerGroupGroups: + err = validateGroups(operation.Values, account.Groups) + if err != nil { + return nil, err + } + newNSGroup.Groups = operation.Values + case UpdateNameServerGroupEnabled: + 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]) + } + newNSGroup.Enabled = enabled + } + } + + account.NameServerGroups[nsGroupID] = newNSGroup + + account.Network.IncSerial() + err = am.Store.SaveAccount(account) + if err != nil { + return nil, err + } + + return newNSGroup.Copy(), nil +} + +// DeleteNameServerGroup deletes nameserver group with nsGroupID +func (am *DefaultAccountManager) DeleteNameServerGroup(accountID, nsGroupID 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.NameServerGroups, nsGroupID) + + account.Network.IncSerial() + err = am.Store.SaveAccount(account) + if err != nil { + return err + } + + return nil +} + +// ListNameServerGroups returns a list of nameserver groups from account +func (am *DefaultAccountManager) ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, 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") + } + + nsGroups := make([]*nbdns.NameServerGroup, 0, len(account.NameServerGroups)) + for _, item := range account.NameServerGroups { + nsGroups = append(nsGroups, item.Copy()) + } + + return nsGroups, nil +} + +func validateNameServerGroup(existingGroup bool, nameserverGroup *nbdns.NameServerGroup, account *Account) error { + nsGroupID := "" + if existingGroup { + nsGroupID = nameserverGroup.ID + _, found := account.NameServerGroups[nsGroupID] + if !found { + return status.Errorf(codes.NotFound, "nameserver group with ID %s was not found", nsGroupID) + } + } + + err := validateNSGroupName(nameserverGroup.Name, nsGroupID, account.NameServerGroups) + if err != nil { + return err + } + + err = validateNSList(nameserverGroup.NameServers) + if err != nil { + return err + } + + err = validateGroups(nameserverGroup.Groups, account.Groups) + if err != nil { + return err + } + + return nil +} + +func validateNSGroupName(name, nsGroupID string, nsGroupMap map[string]*nbdns.NameServerGroup) error { + if utf8.RuneCountInString(name) > nbdns.MaxGroupNameChar || name == "" { + return status.Errorf(codes.InvalidArgument, "nameserver group name should be between 1 and %d", nbdns.MaxGroupNameChar) + } + + for _, nsGroup := range nsGroupMap { + if name == nsGroup.Name && nsGroup.ID != nsGroupID { + return status.Errorf(codes.InvalidArgument, "a nameserver group with name %s already exist", name) + } + } + + return nil +} + +func validateNSList(list []nbdns.NameServer) error { + nsListLenght := len(list) + if nsListLenght == 0 || nsListLenght > 2 { + return status.Errorf(codes.InvalidArgument, "the list of nameservers should be 1 or 2, got %d", len(list)) + } + return nil +} + +func validateGroups(list []string, groups map[string]*Group) error { + if len(list) == 0 { + return status.Errorf(codes.InvalidArgument, "the list of group IDs should not be empty") + } + + for _, id := range list { + if id == "" { + return status.Errorf(codes.InvalidArgument, "group ID should not be empty string") + } + found := false + for groupID := range groups { + if id == groupID { + found = true + break + } + } + if !found { + return status.Errorf(codes.InvalidArgument, "group id %s not found", id) + } + } + + return nil +} diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go new file mode 100644 index 000000000..73512d898 --- /dev/null +++ b/management/server/nameserver_test.go @@ -0,0 +1,965 @@ +package server + +import ( + nbdns "github.com/netbirdio/netbird/dns" + "github.com/stretchr/testify/require" + "net/netip" + "testing" +) + +const ( + group1ID = "group1" + group2ID = "group2" + existingNSGroupName = "existing" + existingNSGroupID = "existingNSGroup" + nsGroupPeer1Key = "BhRPtynAAYRDy08+q4HTMsos8fs4plTP4NOSh7C1ry8=" + nsGroupPeer2Key = "/yF0+vCfv+mRR5k0dca0TrGdO/oiNeAI58gToZm5NyI=" +) + +func TestCreateNameServerGroup(t *testing.T) { + type input struct { + name string + description string + enabled bool + groups []string + nameServers []nbdns.NameServer + } + + testCases := []struct { + name string + inputArgs input + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedNSGroup *nbdns.NameServerGroup + }{ + { + name: "Create A NS Group", + inputArgs: input{ + name: "super", + description: "super", + groups: []string{group1ID}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedNSGroup: &nbdns.NameServerGroup{ + Name: "super", + Description: "super", + Groups: []string{group1ID}, + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + Enabled: true, + }, + }, + { + name: "Should Not Create If Name Exist", + inputArgs: input{ + name: existingNSGroupName, + description: "super", + groups: []string{group1ID}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Create If Name Is Small", + inputArgs: input{ + name: "", + description: "super", + groups: []string{group1ID}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Create If Name Is Large", + inputArgs: input{ + name: "1234567890123456789012345678901234567890extra", + description: "super", + groups: []string{group1ID}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Create A NS Group With No Nameservers Should Fail", + inputArgs: input{ + name: "super", + description: "super", + groups: []string{group1ID}, + nameServers: []nbdns.NameServer{}, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Create A NS Group With More Than 2 Nameservers Should Fail", + inputArgs: input{ + name: "super", + description: "super", + groups: []string{group1ID}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.3.3"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Create If Groups Is Empty", + inputArgs: input{ + name: "super", + description: "super", + groups: []string{}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Create If Group Doesn't Exist", + inputArgs: input{ + name: "super", + description: "super", + groups: []string{"missingGroup"}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Create If Group ID Is Invalid", + inputArgs: input{ + name: "super", + description: "super", + groups: []string{""}, + nameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + enabled: true, + }, + errFunc: require.Error, + shouldCreate: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + am, err := createNSManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestNSAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + outNSGroup, err := am.CreateNameServerGroup( + account.Id, + testCase.inputArgs.name, + testCase.inputArgs.description, + testCase.inputArgs.nameServers, + testCase.inputArgs.groups, + testCase.inputArgs.enabled, + ) + + testCase.errFunc(t, err) + + if !testCase.shouldCreate { + return + } + + // assign generated ID + testCase.expectedNSGroup.ID = outNSGroup.ID + + if !testCase.expectedNSGroup.IsEqual(outNSGroup) { + t.Errorf("new nameserver group didn't match expected ns group:\nGot %#v\nExpected:%#v\n", outNSGroup, testCase.expectedNSGroup) + } + }) + } +} + +func TestSaveNameServerGroup(t *testing.T) { + + existingNSGroup := &nbdns.NameServerGroup{ + ID: "testingNSGroup", + Name: "super", + Description: "super", + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + Groups: []string{group1ID}, + Enabled: true, + } + + validGroups := []string{group2ID} + invalidGroups := []string{"nonExisting"} + validNameServerList := []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + } + invalidNameServerListLarge := []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.3.3"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + } + invalidID := "doesntExist" + validName := "12345678901234567890qw" + invalidNameLarge := "12345678901234567890qwertyuiopqwertyuiop1" + invalidNameSmall := "" + invalidNameExisting := existingNSGroupName + + testCases := []struct { + name string + existingNSGroup *nbdns.NameServerGroup + newID *string + newName *string + newNSList []nbdns.NameServer + newGroups []string + skipCopying bool + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedNSGroup *nbdns.NameServerGroup + }{ + { + name: "Should Update Name Server Group", + existingNSGroup: existingNSGroup, + newName: &validName, + newGroups: validGroups, + newNSList: validNameServerList, + errFunc: require.NoError, + shouldCreate: true, + expectedNSGroup: &nbdns.NameServerGroup{ + ID: "testingNSGroup", + Name: validName, + Description: "super", + NameServers: validNameServerList, + Groups: validGroups, + Enabled: true, + }, + }, + { + name: "Should Not Update If Name Is Small", + existingNSGroup: existingNSGroup, + newName: &invalidNameSmall, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If Name Is Large", + existingNSGroup: existingNSGroup, + newName: &invalidNameLarge, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If Name Exists", + existingNSGroup: existingNSGroup, + newName: &invalidNameExisting, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If ID Don't Exist", + existingNSGroup: existingNSGroup, + newID: &invalidID, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If Nameserver List Is Small", + existingNSGroup: existingNSGroup, + newNSList: []nbdns.NameServer{}, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If Nameserver List Is Large", + existingNSGroup: existingNSGroup, + newNSList: invalidNameServerListLarge, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If Groups List Is Empty", + existingNSGroup: existingNSGroup, + newGroups: []string{}, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If Groups List Has Empty ID", + existingNSGroup: existingNSGroup, + newGroups: []string{""}, + errFunc: require.Error, + shouldCreate: false, + }, + { + name: "Should Not Update If Groups List Has Non Existing Group ID", + existingNSGroup: existingNSGroup, + newGroups: invalidGroups, + errFunc: require.Error, + shouldCreate: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + am, err := createNSManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestNSAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup + + err = am.Store.SaveAccount(account) + if err != nil { + t.Error("account should be saved") + } + + var nsGroupToSave *nbdns.NameServerGroup + + if !testCase.skipCopying { + nsGroupToSave = testCase.existingNSGroup.Copy() + + if testCase.newID != nil { + nsGroupToSave.ID = *testCase.newID + } + + if testCase.newName != nil { + nsGroupToSave.Name = *testCase.newName + } + + if testCase.newGroups != nil { + nsGroupToSave.Groups = testCase.newGroups + } + + if testCase.newNSList != nil { + nsGroupToSave.NameServers = testCase.newNSList + } + } + + err = am.SaveNameServerGroup(account.Id, nsGroupToSave) + + testCase.errFunc(t, err) + + if !testCase.shouldCreate { + return + } + + savedNSGroup, saved := account.NameServerGroups[testCase.expectedNSGroup.ID] + require.True(t, saved) + + if !testCase.expectedNSGroup.IsEqual(savedNSGroup) { + t.Errorf("new nameserver group didn't match expected group:\nGot %#v\nExpected:%#v\n", savedNSGroup, testCase.expectedNSGroup) + } + + }) + } +} + +func TestUpdateNameServerGroup(t *testing.T) { + nsGroupID := "testingNSGroup" + + existingNSGroup := &nbdns.NameServerGroup{ + ID: nsGroupID, + Name: "super", + Description: "super", + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + Groups: []string{group1ID}, + Enabled: true, + } + + testCases := []struct { + name string + existingNSGroup *nbdns.NameServerGroup + nsGroupID string + operations []NameServerGroupUpdateOperation + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedNSGroup *nbdns.NameServerGroup + }{ + { + name: "Should Update Single Property", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupName, + Values: []string{"superNew"}, + }, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedNSGroup: &nbdns.NameServerGroup{ + ID: nsGroupID, + Name: "superNew", + Description: "super", + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + Groups: []string{group1ID}, + Enabled: true, + }, + }, + { + name: "Should Update Multiple Properties", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupName, + Values: []string{"superNew"}, + }, + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupDescription, + Values: []string{"superDescription"}, + }, + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupNameServers, + Values: []string{"udp://127.0.0.1:53", "udp://8.8.8.8:53"}, + }, + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupGroups, + Values: []string{group1ID, group2ID}, + }, + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupEnabled, + Values: []string{"false"}, + }, + }, + errFunc: require.NoError, + shouldCreate: true, + expectedNSGroup: &nbdns.NameServerGroup{ + ID: nsGroupID, + Name: "superNew", + Description: "superDescription", + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("127.0.0.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("8.8.8.8"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + Groups: []string{group1ID, group2ID}, + Enabled: false, + }, + }, + { + name: "Should Not Update On Invalid ID", + existingNSGroup: existingNSGroup, + nsGroupID: "nonExistingNSGroup", + errFunc: require.Error, + }, + { + name: "Should Not Update On Empty Operations", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{}, + errFunc: require.Error, + }, + { + name: "Should Not Update On Empty Values", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupName, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Empty String", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupName, + Values: []string{""}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Invalid Name Large String", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupName, + Values: []string{"12345678901234567890qwertyuiopqwertyuiop1"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Invalid On Existing Name", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupName, + Values: []string{existingNSGroupName}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Invalid On Multiple Name Values", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupName, + Values: []string{"nameOne", "nameTwo"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Invalid Boolean", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupEnabled, + Values: []string{"yes"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Invalid Nameservers Wrong Schema", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupNameServers, + Values: []string{"https://127.0.0.1:53"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Invalid Nameservers Wrong IP", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupNameServers, + Values: []string{"udp://8.8.8.300:53"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Large Number Of Nameservers", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupNameServers, + Values: []string{"udp://127.0.0.1:53", "udp://8.8.8.8:53", "udp://8.8.4.4:53"}, + }, + }, + errFunc: require.Error, + }, + { + name: "Should Not Update On Invalid GroupID", + existingNSGroup: existingNSGroup, + nsGroupID: existingNSGroup.ID, + operations: []NameServerGroupUpdateOperation{ + NameServerGroupUpdateOperation{ + Type: UpdateNameServerGroupGroups, + Values: []string{"nonExistingGroupID"}, + }, + }, + errFunc: require.Error, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + am, err := createNSManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestNSAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + account.NameServerGroups[testCase.existingNSGroup.ID] = testCase.existingNSGroup + + err = am.Store.SaveAccount(account) + if err != nil { + t.Error("account should be saved") + } + + updatedRoute, err := am.UpdateNameServerGroup(account.Id, testCase.nsGroupID, testCase.operations) + testCase.errFunc(t, err) + + if !testCase.shouldCreate { + return + } + + testCase.expectedNSGroup.ID = updatedRoute.ID + + if !testCase.expectedNSGroup.IsEqual(updatedRoute) { + t.Errorf("new nameserver group didn't match expected group:\nGot %#v\nExpected:%#v\n", updatedRoute, testCase.expectedNSGroup) + } + + }) + } +} + +func TestDeleteNameServerGroup(t *testing.T) { + nsGroupID := "testingNSGroup" + + testingNSGroup := &nbdns.NameServerGroup{ + ID: nsGroupID, + Name: "super", + Description: "super", + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("1.1.1.1"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("1.1.2.2"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + Groups: []string{group1ID}, + Enabled: true, + } + + am, err := createNSManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestNSAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + account.NameServerGroups[testingNSGroup.ID] = testingNSGroup + + err = am.Store.SaveAccount(account) + if err != nil { + t.Error("failed to save account") + } + + err = am.DeleteNameServerGroup(account.Id, testingNSGroup.ID) + if err != nil { + t.Error("deleting nameserver group 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.NameServerGroups[testingNSGroup.ID] + if found { + t.Error("nameserver group shouldn't be found after delete") + } +} + +func TestGetNameServerGroup(t *testing.T) { + + am, err := createNSManager(t) + if err != nil { + t.Error("failed to create account manager") + } + + account, err := initTestNSAccount(t, am) + if err != nil { + t.Error("failed to init testing account") + } + + foundGroup, err := am.GetNameServerGroup(account.Id, existingNSGroupID) + if err != nil { + t.Error("getting existing nameserver group failed with error: ", err) + } + + if foundGroup == nil { + t.Error("got a nil group while getting nameserver group with ID") + } + + _, err = am.GetNameServerGroup(account.Id, "not existing") + if err == nil { + t.Error("getting not existing nameserver group should return error, got nil") + } +} + +func createNSManager(t *testing.T) (*DefaultAccountManager, error) { + store, err := createNSStore(t) + if err != nil { + return nil, err + } + return BuildManager(store, NewPeersUpdateManager(), nil) +} + +func createNSStore(t *testing.T) (Store, error) { + dataDir := t.TempDir() + store, err := NewStore(dataDir) + if err != nil { + return nil, err + } + + return store, nil +} + +func initTestNSAccount(t *testing.T, am *DefaultAccountManager) (*Account, error) { + peer1 := &Peer{ + Key: nsGroupPeer1Key, + 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: nsGroupPeer2Key, + 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", + }, + } + existingNSGroup := nbdns.NameServerGroup{ + ID: existingNSGroupID, + Name: existingNSGroupName, + Description: "", + NameServers: []nbdns.NameServer{ + { + IP: netip.MustParseAddr("8.8.8.8"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + { + IP: netip.MustParseAddr("8.8.4.4"), + NSType: nbdns.UDPNameServerType, + Port: nbdns.DefaultDNSPort, + }, + }, + Groups: []string{group1ID}, + Enabled: true, + } + + accountID := "testingAcc" + userID := "testingUser" + domain := "example.com" + + account := newAccountWithId(accountID, userID, domain) + + account.NameServerGroups[existingNSGroup.ID] = &existingNSGroup + + defaultGroup, err := account.GetGroupAll() + if err != nil { + return nil, err + } + newGroup1 := defaultGroup.Copy() + newGroup1.ID = group1ID + newGroup2 := defaultGroup.Copy() + newGroup2.ID = group2ID + + account.Groups[newGroup1.ID] = newGroup1 + account.Groups[newGroup2.ID] = newGroup2 + + 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 +}