From 5781ec7a8e49a9fc310d614356d24c630178eba8 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Fri, 6 Oct 2023 17:03:17 +0200 Subject: [PATCH 01/43] Use the PCKE flow in the UI (#1196) --- client/ui/client_ui.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index e6b4394e8..d2c18dc37 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -202,9 +202,10 @@ func (s *serviceClient) getSettingsForm() *widget.Form { } _, err = client.Login(s.ctx, &proto.LoginRequest{ - ManagementUrl: s.iMngURL.Text, - AdminURL: s.iAdminURL.Text, - PreSharedKey: s.iPreSharedKey.Text, + ManagementUrl: s.iMngURL.Text, + AdminURL: s.iAdminURL.Text, + PreSharedKey: s.iPreSharedKey.Text, + IsLinuxDesktopClient: runtime.GOOS == "linux", }) if err != nil { log.Errorf("login to management URL: %v", err) From af6fdd3af2ce52f41d97277aaf12a081c631a021 Mon Sep 17 00:00:00 2001 From: Misha Bragin Date: Fri, 6 Oct 2023 18:28:46 +0200 Subject: [PATCH 02/43] Always call Login from UI with the desktop flag (#1200) --- client/ui/build-ui-linux.sh | 5 +++++ client/ui/client_ui.go | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 client/ui/build-ui-linux.sh diff --git a/client/ui/build-ui-linux.sh b/client/ui/build-ui-linux.sh new file mode 100644 index 000000000..eab08214d --- /dev/null +++ b/client/ui/build-ui-linux.sh @@ -0,0 +1,5 @@ +#!/bin/bash +sudo apt update +sudo apt remove gir1.2-appindicator3-0.1 +sudo apt install -y libayatana-appindicator3-dev +go build \ No newline at end of file diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index d2c18dc37..9c7685db0 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -234,7 +234,9 @@ func (s *serviceClient) login() error { return err } - loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{}) + loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{ + IsLinuxDesktopClient: runtime.GOOS == "linux", + }) if err != nil { log.Errorf("login to management URL with: %v", err) return err From f7e6cdcbf0edb547c2ef3d7b0a41f5032995cbc9 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Sat, 7 Oct 2023 21:45:46 +0200 Subject: [PATCH 03/43] Run linter action on MacOS and Windows (#1198) --- .gitattributes | 1 + .github/workflows/golangci-lint.yml | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..d207b1802 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 2a5c51c8a..4e584ecc2 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -1,12 +1,23 @@ name: golangci-lint on: [pull_request] + +permissions: + contents: read + pull-requests: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }} cancel-in-progress: true + jobs: golangci: + strategy: + fail-fast: false + matrix: + os: [macos-latest, windows-latest, ubuntu-latest] name: lint - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + timeout-minutes: 15 steps: - name: Checkout code uses: actions/checkout@v3 @@ -14,7 +25,12 @@ jobs: uses: actions/setup-go@v4 with: go-version: "1.20.x" + cache: false - name: Install dependencies + if: matrix.os == 'ubuntu-latest' run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev - name: golangci-lint - uses: golangci/golangci-lint-action@v3 \ No newline at end of file + uses: golangci/golangci-lint-action@v3 + with: + version: latest + args: --timeout=12m \ No newline at end of file From 3c485dc7a111b7b87b07408c3654f60725cd76d5 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Mon, 9 Oct 2023 14:39:41 +0200 Subject: [PATCH 04/43] Fix routing groups expand and filtering (#1203) This PR fixes an issue were only one route containing routing groups was being synced to peers. It also prevents sending routes for peers that aren't connect via ACL. Moved all checks to Account.getEnabledAndDisabledRoutesByPeer. Co-authored-by: Yury Gargay Co-authored-by: braginini --- management/server/account.go | 104 +++++++++++------------------- management/server/account_test.go | 2 +- management/server/route.go | 2 +- management/server/route_test.go | 104 +++++++++++++++++++----------- 4 files changed, 107 insertions(+), 105 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index ab79a6789..1f8fc497b 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -200,7 +200,7 @@ type UserInfo struct { // from the ACL peers that have distribution groups associated with the peer ID. // Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID. func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Route { - routes, peerDisabledRoutes := a.getEnabledAndDisabledRoutesByPeer(peerID) + routes, peerDisabledRoutes := a.getRoutingPeerRoutes(peerID) peerRoutesMembership := make(lookupMap) for _, r := range append(routes, peerDisabledRoutes...) { peerRoutesMembership[route.GetHAUniqueID(r)] = struct{}{} @@ -208,7 +208,7 @@ func (a *Account) getRoutesToSync(peerID string, aclPeers []*Peer) []*route.Rout groupListMap := a.getPeerGroups(peerID) for _, peer := range aclPeers { - activeRoutes, _ := a.getEnabledAndDisabledRoutesByPeer(peer.ID) + activeRoutes, _ := a.getRoutingPeerRoutes(peer.ID) groupFilteredRoutes := a.filterRoutesByGroups(activeRoutes, groupListMap) filteredRoutes := a.filterRoutesFromPeersOfSameHAGroup(groupFilteredRoutes, peerRoutesMembership) routes = append(routes, filteredRoutes...) @@ -244,20 +244,32 @@ func (a *Account) filterRoutesByGroups(routes []*route.Route, groupListMap looku return filteredRoutes } -// getEnabledAndDisabledRoutesByPeer returns the enabled and disabled lists of routes that belong to a peer. +// getRoutingPeerRoutes returns the enabled and disabled lists of routes that the given routing peer serves // Please mind, that the returned route.Route objects will contain Peer.Key instead of Peer.ID. -func (a *Account) getEnabledAndDisabledRoutesByPeer(peerID string) ([]*route.Route, []*route.Route) { - var enabledRoutes []*route.Route - var disabledRoutes []*route.Route +// If the given is not a routing peer, then the lists are empty. +func (a *Account) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Route, disabledRoutes []*route.Route) { + + peer := a.GetPeer(peerID) + if peer == nil { + log.Errorf("peer %s that doesn't exist under account %s", peerID, a.Id) + return enabledRoutes, disabledRoutes + } + + // currently we support only linux routing peers + if peer.Meta.GoOS != "linux" { + return enabledRoutes, disabledRoutes + } + + seenRoute := make(map[string]struct{}) 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) + if _, ok := seenRoute[r.ID]; ok { return } + seenRoute[r.ID] = struct{}{} if r.Enabled { + r.Peer = peer.Key enabledRoutes = append(enabledRoutes, r) return } @@ -265,25 +277,30 @@ func (a *Account) getEnabledAndDisabledRoutesByPeer(peerID string) ([]*route.Rou } 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) + 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 { continue } - for _, id := range group.Peers { - if id == peerID { - takeRoute(r, id) - break - } - } + + newPeerRoute := r.Copy() + newPeerRoute.Peer = id + newPeerRoute.PeerGroups = nil + newPeerRoute.ID = r.ID + ":" + id // we have to provide unique route id when distribute network map + takeRoute(newPeerRoute, id) + break } } if r.Peer == peerID { - takeRoute(r, peerID) + takeRoute(r.Copy(), peerID) } } + return enabledRoutes, disabledRoutes } @@ -319,50 +336,7 @@ func (a *Account) GetPeerNetworkMap(peerID, dnsDomain string) *NetworkMap { peersToConnect = append(peersToConnect, p) } - 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 - } - } - } - } + routesUpdate := a.getRoutesToSync(peerID, peersToConnect) dnsManagementStatus := a.getPeerDNSManagementStatus(peerID) dnsUpdate := nbdns.Config{ diff --git a/management/server/account_test.go b/management/server/account_test.go index d55734685..331df2017 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1237,7 +1237,7 @@ func TestAccount_GetRoutesToSync(t *testing.T) { } account := &Account{ Peers: map[string]*Peer{ - "peer-1": {Key: "peer-1"}, "peer-2": {Key: "peer-2"}, "peer-3": {Key: "peer-1"}, + "peer-1": {Key: "peer-1", Meta: PeerSystemMeta{GoOS: "linux"}}, "peer-2": {Key: "peer-2", Meta: PeerSystemMeta{GoOS: "linux"}}, "peer-3": {Key: "peer-1", Meta: PeerSystemMeta{GoOS: "linux"}}, }, Groups: map[string]*Group{"group1": {ID: "group1", Peers: []string{"peer-1", "peer-2"}}}, Routes: map[string]*route.Route{ diff --git a/management/server/route.go b/management/server/route.go index 79c207c9b..6b5aa982d 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -98,7 +98,7 @@ func (am *DefaultAccountManager) checkRoutePrefixExistsForPeers(account *Account // 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) + peer := account.GetPeer(id) if peer == nil { return status.Errorf(status.InvalidArgument, "peer with ID %s not found", peerID) } diff --git a/management/server/route_test.go b/management/server/route_test.go index 32f15843b..00ef3e93a 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/rs/xid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/management/server/activity" @@ -48,11 +49,12 @@ func TestCreateRoute(t *testing.T) { } testCases := []struct { - name string - inputArgs input - shouldCreate bool - errFunc require.ErrorAssertionFunc - expectedRoute *route.Route + name string + inputArgs input + createInitRoute bool + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedRoute *route.Route }{ { name: "Happy Path", @@ -164,8 +166,9 @@ func TestCreateRoute(t *testing.T) { enabled: true, groups: []string{routeGroup1}, }, - errFunc: require.Error, - shouldCreate: false, + createInitRoute: true, + errFunc: require.Error, + shouldCreate: false, }, { name: "Bad Peers Group already has this route", @@ -179,8 +182,9 @@ func TestCreateRoute(t *testing.T) { enabled: true, groups: []string{routeGroup1}, }, - errFunc: require.Error, - shouldCreate: false, + createInitRoute: true, + errFunc: require.Error, + shouldCreate: false, }, { name: "Empty Peer Should Create", @@ -326,6 +330,18 @@ func TestCreateRoute(t *testing.T) { t.Errorf("failed to init testing account: %s", err) } + if testCase.createInitRoute { + groupAll, errInit := account.GetGroupAll() + if errInit != nil { + t.Errorf("failed to get group all: %s", errInit) + } + _, errInit = am.CreateRoute(account.Id, existingNetwork, "", []string{routeGroup3, routeGroup4}, + "", existingRouteID, false, 1000, []string{groupAll.ID}, true, userID) + if errInit != nil { + t.Errorf("failed to create init route: %s", errInit) + } + } + outRoute, err := am.CreateRoute( account.Id, testCase.inputArgs.network, @@ -370,17 +386,18 @@ func TestSaveRoute(t *testing.T) { validGroupHA2 := routeGroupHA2 testCases := []struct { - name string - existingRoute *route.Route - newPeer *string - newPeerGroups []string - newMetric *int - newPrefix *netip.Prefix - newGroups []string - skipCopying bool - shouldCreate bool - errFunc require.ErrorAssertionFunc - expectedRoute *route.Route + name string + existingRoute *route.Route + createInitRoute bool + newPeer *string + newPeerGroups []string + newMetric *int + newPrefix *netip.Prefix + newGroups []string + skipCopying bool + shouldCreate bool + errFunc require.ErrorAssertionFunc + expectedRoute *route.Route }{ { name: "Happy Path", @@ -645,8 +662,9 @@ func TestSaveRoute(t *testing.T) { Enabled: true, Groups: []string{routeGroup1}, }, - newPeer: &validUsedPeer, - errFunc: require.Error, + createInitRoute: true, + newPeer: &validUsedPeer, + errFunc: require.Error, }, { name: "Do not allow to modify existing route with a peers group from another route", @@ -662,8 +680,9 @@ func TestSaveRoute(t *testing.T) { Enabled: true, Groups: []string{routeGroup1}, }, - newPeerGroups: []string{routeGroup4}, - errFunc: require.Error, + createInitRoute: true, + newPeerGroups: []string{routeGroup4}, + errFunc: require.Error, }, } for _, testCase := range testCases { @@ -678,6 +697,21 @@ func TestSaveRoute(t *testing.T) { t.Error("failed to init testing account") } + if testCase.createInitRoute { + account.Routes["initRoute"] = &route.Route{ + ID: "initRoute", + Network: netip.MustParsePrefix(existingNetwork), + NetID: existingRouteID, + NetworkType: route.IPv4Network, + PeerGroups: []string{routeGroup4}, + Description: "super", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{routeGroup1}, + } + } + account.Routes[testCase.existingRoute.ID] = testCase.existingRoute err = am.Store.SaveAccount(account) @@ -811,15 +845,15 @@ func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) { peer1Routes, err := am.GetNetworkMap(peer1ID) require.NoError(t, err) - require.Len(t, peer1Routes.Routes, 3, "HA route should have more than 1 routes") + assert.Len(t, peer1Routes.Routes, 1, "HA route should have 1 server route") peer2Routes, err := am.GetNetworkMap(peer2ID) require.NoError(t, err) - require.Len(t, peer2Routes.Routes, 3, "HA route should have more than 1 routes") + assert.Len(t, peer2Routes.Routes, 1, "HA route should have 1 server route") peer4Routes, err := am.GetNetworkMap(peer4ID) require.NoError(t, err) - require.Len(t, peer4Routes.Routes, 3, "HA route should have more than 1 routes") + assert.Len(t, peer4Routes.Routes, 1, "HA route should have 1 server route") groups, err := am.ListGroups(account.Id) require.NoError(t, err) @@ -838,32 +872,32 @@ func TestGetNetworkMap_RouteSyncPeerGroups(t *testing.T) { peer2RoutesAfterDelete, err := am.GetNetworkMap(peer2ID) require.NoError(t, err) - require.Len(t, peer2RoutesAfterDelete.Routes, 2, "after peer deletion group should have only 2 route") + assert.Len(t, peer2RoutesAfterDelete.Routes, 2, "after peer deletion group should have 2 client routes") 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") + assert.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") + assert.Len(t, peer1RoutesAfterAdd.Routes, 1, "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") + assert.Len(t, peer2RoutesAfterAdd.Routes, 2, "HA route should have 2 client routes") 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") + assert.Len(t, peer1DeletedRoute.Routes, 0, "we should receive one route for peer1") } func TestGetNetworkMap_RouteSync(t *testing.T) { @@ -1193,11 +1227,5 @@ func initTestRouteAccount(t *testing.T, am *DefaultAccountManager) (*Account, er } } - _, err = am.CreateRoute(account.Id, existingNetwork, "", []string{routeGroup3, routeGroup4}, - "", existingRouteID, false, 1000, []string{groupAll.ID}, true, userID) - if err != nil { - return nil, err - } - return am.Store.GetAccount(account.Id) } From 4ad14cb46b3e3f0b3ebac54e568bff82e8d50c2d Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 11 Oct 2023 17:09:30 +0300 Subject: [PATCH 05/43] Add Pagination for IdP Users Fetch (#1210) * Retrieve all workspace users via pagination, excluding custom user attributes * Retrieve all authentik users via pagination * Retrieve all Azure AD users via pagination * Simplify user data appending operation Reduced unnecessary iteration and used an efficient way to append all users to 'indexedUsers' * Fix ineffectual assignment to reqURL * Retrieve all Okta users via pagination * Add missing GetAccount metrics * Refactor * minimize memory allocation Refactored the memory allocation for the 'users' slice in the Okta IDP code. Previously, the slice was only initialized but not given a size. Now the size of userList is utilized to optimize memory allocation, reducing potential slice resizing and memory re-allocation costs while appending users. * Add logging for entries received from IdP management Added informative and debug logging statements in account.go file. Logging has been added to identify the number of entries received from Identity Provider (IdP) management. This will aid in tracking and debugging any potential data ingestion issues. --- management/server/account.go | 2 + management/server/idp/authentik.go | 84 ++++++++++++----------- management/server/idp/azure.go | 84 ++++++++++++++--------- management/server/idp/google_workspace.go | 78 ++++++++++++++------- management/server/idp/okta.go | 65 +++++++++++------- 5 files changed, 191 insertions(+), 122 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 1f8fc497b..d35ad2566 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -946,6 +946,7 @@ func (am *DefaultAccountManager) warmupIDPCache() error { if err != nil { return err } + log.Infof("%d entries received from IdP management", len(userData)) // If the Identity Provider does not support writing AppMetadata, // in cases like this, we expect it to return all users in an "unset" field. @@ -1045,6 +1046,7 @@ func (am *DefaultAccountManager) loadAccount(_ context.Context, accountID interf if err != nil { return nil, err } + log.Debugf("%d entries received from IdP management", len(userData)) dataMap := make(map[string]*idp.UserData, len(userData)) for _, datum := range userData { diff --git a/management/server/idp/authentik.go b/management/server/idp/authentik.go index ca995b299..4bbf09404 100644 --- a/management/server/idp/authentik.go +++ b/management/server/idp/authentik.go @@ -251,34 +251,18 @@ func (am *AuthentikManager) GetUserDataByID(userID string, appMetadata AppMetada // GetAccount returns all the users for a given profile. func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) { - ctx, err := am.authenticationContext() + users, err := am.getAllUsers() if err != nil { return nil, err } - userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Execute() - if err != nil { - return nil, err - } - defer resp.Body.Close() - if am.appMetrics != nil { am.appMetrics.IDPMetrics().CountGetAccount() } - if resp.StatusCode != http.StatusOK { - if am.appMetrics != nil { - am.appMetrics.IDPMetrics().CountRequestStatusError() - } - return nil, fmt.Errorf("unable to get account %s users, statusCode %d", accountID, resp.StatusCode) - } - - users := make([]*UserData, 0) - for _, user := range userList.Results { - userData := parseAuthentikUser(user) - userData.AppMetadata.WTAccountID = accountID - - users = append(users, userData) + for index, user := range users { + user.AppMetadata.WTAccountID = accountID + users[index] = user } return users, nil @@ -287,37 +271,59 @@ func (am *AuthentikManager) GetAccount(accountID string) ([]*UserData, error) { // GetAllAccounts gets all registered accounts with corresponding user data. // It returns a list of users indexed by accountID. func (am *AuthentikManager) GetAllAccounts() (map[string][]*UserData, error) { - ctx, err := am.authenticationContext() + users, err := am.getAllUsers() if err != nil { return nil, err } - userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Execute() - if err != nil { - return nil, err - } - defer resp.Body.Close() + indexedUsers := make(map[string][]*UserData) + indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...) if am.appMetrics != nil { am.appMetrics.IDPMetrics().CountGetAllAccounts() } - if resp.StatusCode != http.StatusOK { - if am.appMetrics != nil { - am.appMetrics.IDPMetrics().CountRequestStatusError() - } - return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode) - } - - indexedUsers := make(map[string][]*UserData) - for _, user := range userList.Results { - userData := parseAuthentikUser(user) - indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData) - } - return indexedUsers, nil } +// getAllUsers returns all users in a Authentik account. +func (am *AuthentikManager) getAllUsers() ([]*UserData, error) { + users := make([]*UserData, 0) + + page := int32(1) + for { + ctx, err := am.authenticationContext() + if err != nil { + return nil, err + } + + userList, resp, err := am.apiClient.CoreApi.CoreUsersList(ctx).Page(page).Execute() + if err != nil { + return nil, err + } + _ = resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if am.appMetrics != nil { + am.appMetrics.IDPMetrics().CountRequestStatusError() + } + return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode) + } + + for _, user := range userList.Results { + users = append(users, parseAuthentikUser(user)) + } + + page = int32(userList.GetPagination().Next) + if userList.GetPagination().Next == 0 { + break + } + + } + + return users, nil +} + // CreateUser creates a new user in authentik Idp and sends an invitation. func (am *AuthentikManager) CreateUser(_, _, _, _ string) (*UserData, error) { return nil, fmt.Errorf("method CreateUser not implemented") diff --git a/management/server/idp/azure.go b/management/server/idp/azure.go index e4224c26d..706e4d330 100644 --- a/management/server/idp/azure.go +++ b/management/server/idp/azure.go @@ -266,10 +266,7 @@ func (am *AzureManager) GetUserByEmail(email string) ([]*UserData, error) { // GetAccount returns all the users for a given profile. func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) { - q := url.Values{} - q.Add("$select", profileFields) - - body, err := am.get("users", q) + users, err := am.getAllUsers() if err != nil { return nil, err } @@ -278,18 +275,9 @@ func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) { am.appMetrics.IDPMetrics().CountGetAccount() } - var profiles struct{ Value []azureProfile } - err = am.helper.Unmarshal(body, &profiles) - if err != nil { - return nil, err - } - - users := make([]*UserData, 0) - for _, profile := range profiles.Value { - userData := profile.userData() - userData.AppMetadata.WTAccountID = accountID - - users = append(users, userData) + for index, user := range users { + user.AppMetadata.WTAccountID = accountID + users[index] = user } return users, nil @@ -298,28 +286,16 @@ func (am *AzureManager) GetAccount(accountID string) ([]*UserData, error) { // GetAllAccounts gets all registered accounts with corresponding user data. // It returns a list of users indexed by accountID. func (am *AzureManager) GetAllAccounts() (map[string][]*UserData, error) { - q := url.Values{} - q.Add("$select", profileFields) - - body, err := am.get("users", q) - if err != nil { - return nil, err - } - - if am.appMetrics != nil { - am.appMetrics.IDPMetrics().CountGetAllAccounts() - } - - var profiles struct{ Value []azureProfile } - err = am.helper.Unmarshal(body, &profiles) + users, err := am.getAllUsers() if err != nil { return nil, err } indexedUsers := make(map[string][]*UserData) - for _, profile := range profiles.Value { - userData := profile.userData() - indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData) + indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...) + + if am.appMetrics != nil { + am.appMetrics.IDPMetrics().CountGetAllAccounts() } return indexedUsers, nil @@ -373,6 +349,39 @@ func (am *AzureManager) DeleteUser(userID string) error { return nil } +// getAllUsers returns all users in an Azure AD account. +func (am *AzureManager) getAllUsers() ([]*UserData, error) { + users := make([]*UserData, 0) + + q := url.Values{} + q.Add("$select", profileFields) + q.Add("$top", "500") + + for nextLink := "users"; nextLink != ""; { + body, err := am.get(nextLink, q) + if err != nil { + return nil, err + } + + var profiles struct { + Value []azureProfile + NextLink string `json:"@odata.nextLink"` + } + err = am.helper.Unmarshal(body, &profiles) + if err != nil { + return nil, err + } + + for _, profile := range profiles.Value { + users = append(users, profile.userData()) + } + + nextLink = profiles.NextLink + } + + return users, nil +} + // get perform Get requests. func (am *AzureManager) get(resource string, q url.Values) ([]byte, error) { jwtToken, err := am.credentials.Authenticate() @@ -380,7 +389,14 @@ func (am *AzureManager) get(resource string, q url.Values) ([]byte, error) { return nil, err } - reqURL := fmt.Sprintf("%s/%s?%s", am.GraphAPIEndpoint, resource, q.Encode()) + var reqURL string + if strings.HasPrefix(resource, "https") { + // Already an absolute URL for paging + reqURL = resource + } else { + reqURL = fmt.Sprintf("%s/%s?%s", am.GraphAPIEndpoint, resource, q.Encode()) + } + req, err := http.NewRequest(http.MethodGet, reqURL, nil) if err != nil { return nil, err diff --git a/management/server/idp/google_workspace.go b/management/server/idp/google_workspace.go index ed2de9a42..896fb707b 100644 --- a/management/server/idp/google_workspace.go +++ b/management/server/idp/google_workspace.go @@ -96,7 +96,7 @@ func (gm *GoogleWorkspaceManager) UpdateUserAppMetadata(_ string, _ AppMetadata) // GetUserDataByID requests user data from Google Workspace via ID. func (gm *GoogleWorkspaceManager) GetUserDataByID(userID string, appMetadata AppMetadata) (*UserData, error) { - user, err := gm.usersService.Get(userID).Projection("full").Do() + user, err := gm.usersService.Get(userID).Do() if err != nil { return nil, err } @@ -113,43 +113,69 @@ func (gm *GoogleWorkspaceManager) GetUserDataByID(userID string, appMetadata App // GetAccount returns all the users for a given profile. func (gm *GoogleWorkspaceManager) GetAccount(accountID string) ([]*UserData, error) { - usersList, err := gm.usersService.List().Customer(gm.CustomerID).Projection("full").Do() - if err != nil { - return nil, err - } - - usersData := make([]*UserData, 0) - for _, user := range usersList.Users { - userData := parseGoogleWorkspaceUser(user) - userData.AppMetadata.WTAccountID = accountID - - usersData = append(usersData, userData) - } - - return usersData, nil -} - -// GetAllAccounts gets all registered accounts with corresponding user data. -// It returns a list of users indexed by accountID. -func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, error) { - usersList, err := gm.usersService.List().Customer(gm.CustomerID).Projection("full").Do() + users, err := gm.getAllUsers() if err != nil { return nil, err } if gm.appMetrics != nil { - gm.appMetrics.IDPMetrics().CountGetAllAccounts() + gm.appMetrics.IDPMetrics().CountGetAccount() + } + + for index, user := range users { + user.AppMetadata.WTAccountID = accountID + users[index] = user + } + + return users, nil +} + +// GetAllAccounts gets all registered accounts with corresponding user data. +// It returns a list of users indexed by accountID. +func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, error) { + users, err := gm.getAllUsers() + if err != nil { + return nil, err } indexedUsers := make(map[string][]*UserData) - for _, user := range usersList.Users { - userData := parseGoogleWorkspaceUser(user) - indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData) + indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...) + + if gm.appMetrics != nil { + gm.appMetrics.IDPMetrics().CountGetAllAccounts() } return indexedUsers, nil } +// getAllUsers returns all users in a Google Workspace account filtered by customer ID. +func (gm *GoogleWorkspaceManager) getAllUsers() ([]*UserData, error) { + users := make([]*UserData, 0) + pageToken := "" + for { + call := gm.usersService.List().Customer(gm.CustomerID).MaxResults(500) + if pageToken != "" { + call.PageToken(pageToken) + } + + resp, err := call.Do() + if err != nil { + return nil, err + } + + for _, user := range resp.Users { + users = append(users, parseGoogleWorkspaceUser(user)) + } + + pageToken = resp.NextPageToken + if pageToken == "" { + break + } + } + + return users, nil +} + // CreateUser creates a new user in Google Workspace and sends an invitation. func (gm *GoogleWorkspaceManager) CreateUser(_, _, _, _ string) (*UserData, error) { return nil, fmt.Errorf("method CreateUser not implemented") @@ -158,7 +184,7 @@ func (gm *GoogleWorkspaceManager) CreateUser(_, _, _, _ string) (*UserData, erro // GetUserByEmail searches users with a given email. // If no users have been found, this function returns an empty list. func (gm *GoogleWorkspaceManager) GetUserByEmail(email string) ([]*UserData, error) { - user, err := gm.usersService.Get(email).Projection("full").Do() + user, err := gm.usersService.Get(email).Do() if err != nil { return nil, err } diff --git a/management/server/idp/okta.go b/management/server/idp/okta.go index 3e7b9357e..67341a26f 100644 --- a/management/server/idp/okta.go +++ b/management/server/idp/okta.go @@ -9,6 +9,7 @@ import ( "time" "github.com/okta/okta-sdk-golang/v2/okta" + "github.com/okta/okta-sdk-golang/v2/okta/query" "github.com/netbirdio/netbird/management/server/telemetry" ) @@ -160,7 +161,7 @@ func (om *OktaManager) GetUserByEmail(email string) ([]*UserData, error) { // GetAccount returns all the users for a given profile. func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) { - users, resp, err := om.client.User.ListUsers(context.Background(), nil) + users, err := om.getAllUsers() if err != nil { return nil, err } @@ -169,39 +170,40 @@ func (om *OktaManager) GetAccount(accountID string) ([]*UserData, error) { om.appMetrics.IDPMetrics().CountGetAccount() } - if resp.StatusCode != http.StatusOK { - if om.appMetrics != nil { - om.appMetrics.IDPMetrics().CountRequestStatusError() - } - return nil, fmt.Errorf("unable to get account, statusCode %d", resp.StatusCode) + for index, user := range users { + user.AppMetadata.WTAccountID = accountID + users[index] = user } - list := make([]*UserData, 0) - for _, user := range users { - userData, err := parseOktaUser(user) - if err != nil { - return nil, err - } - userData.AppMetadata.WTAccountID = accountID - - list = append(list, userData) - } - - return list, nil + return users, nil } // GetAllAccounts gets all registered accounts with corresponding user data. // It returns a list of users indexed by accountID. func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) { - users, resp, err := om.client.User.ListUsers(context.Background(), nil) + users, err := om.getAllUsers() if err != nil { return nil, err } + indexedUsers := make(map[string][]*UserData) + indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], users...) + if om.appMetrics != nil { om.appMetrics.IDPMetrics().CountGetAllAccounts() } + return indexedUsers, nil +} + +// getAllUsers returns all users in an Okta account. +func (om *OktaManager) getAllUsers() ([]*UserData, error) { + qp := query.NewQueryParams(query.WithLimit(200)) + userList, resp, err := om.client.User.ListUsers(context.Background(), qp) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { if om.appMetrics != nil { om.appMetrics.IDPMetrics().CountRequestStatusError() @@ -209,17 +211,34 @@ func (om *OktaManager) GetAllAccounts() (map[string][]*UserData, error) { return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode) } - indexedUsers := make(map[string][]*UserData) - for _, user := range users { + for resp.HasNextPage() { + paginatedUsers := make([]*okta.User, 0) + resp, err = resp.Next(context.Background(), &paginatedUsers) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + if om.appMetrics != nil { + om.appMetrics.IDPMetrics().CountRequestStatusError() + } + return nil, fmt.Errorf("unable to get all accounts, statusCode %d", resp.StatusCode) + } + + userList = append(userList, paginatedUsers...) + } + + users := make([]*UserData, 0, len(userList)) + for _, user := range userList { userData, err := parseOktaUser(user) if err != nil { return nil, err } - indexedUsers[UnsetAccountID] = append(indexedUsers[UnsetAccountID], userData) + users = append(users, userData) } - return indexedUsers, nil + return users, nil } // UpdateUserAppMetadata updates user app metadata based on userID and metadata map. From 659110f0d5491936e4cd5bb7dc14d2ff543ddeda Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Wed, 11 Oct 2023 18:11:45 +0200 Subject: [PATCH 06/43] Rework peer connection status based on the update channel existence (#1213) With this change, we don't need to update all peers on startup. We will check the existence of an update channel when returning a list or single peer on API. Then after restarting of server consumers of API will see peer not connected status till the creation of an updated channel which indicates peer successful connection. --- management/server/account.go | 6 ++ management/server/file_store.go | 4 -- management/server/http/peers_handler.go | 33 ++++++++++- management/server/http/peers_handler_test.go | 56 +++++++++++++++++-- management/server/mock_server/account_mock.go | 9 +++ 5 files changed, 97 insertions(+), 11 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index d35ad2566..8e453a1fe 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -103,6 +103,7 @@ type AccountManager interface { UpdateAccountSettings(accountID, userID string, newSettings *Settings) (*Account, error) LoginPeer(login PeerLogin) (*Peer, *NetworkMap, error) // used by peer gRPC API SyncPeer(sync PeerSync) (*Peer, *NetworkMap, error) // used by peer gRPC API + GetAllConnectedPeers() (map[string]struct{}, error) } type DefaultAccountManager struct { @@ -1558,6 +1559,11 @@ func (am *DefaultAccountManager) getAccountWithAuthorizationClaims(claims jwtcla } } +// GetAllConnectedPeers returns connected peers based on peersUpdateManager.GetAllConnectedPeers() +func (am *DefaultAccountManager) GetAllConnectedPeers() (map[string]struct{}, error) { + return am.peersUpdateManager.GetAllConnectedPeers(), nil +} + func isDomainValid(domain string) bool { re := regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`) return re.Match([]byte(domain)) diff --git a/management/server/file_store.go b/management/server/file_store.go index ecd02ba99..b90b1d607 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -111,10 +111,6 @@ func restore(file string) (*FileStore, error) { for _, peer := range account.Peers { store.PeerKeyID2AccountID[peer.Key] = accountID store.PeerID2AccountID[peer.ID] = accountID - // reset all peers to status = Disconnected - if peer.Status != nil && peer.Status.Connected { - peer.Status.Connected = false - } } for _, user := range account.Users { store.UserID2AccountID[user.Id] = accountID diff --git a/management/server/http/peers_handler.go b/management/server/http/peers_handler.go index adf4a9721..a485d6ccf 100644 --- a/management/server/http/peers_handler.go +++ b/management/server/http/peers_handler.go @@ -31,6 +31,24 @@ func NewPeersHandler(accountManager server.AccountManager, authCfg AuthCfg) *Pee } } +func (h *PeersHandler) checkPeerStatus(peer *server.Peer) (*server.Peer, error) { + peerToReturn := peer.Copy() + if peer.Status.Connected { + statuses, err := h.accountManager.GetAllConnectedPeers() + if err != nil { + return peerToReturn, err + } + + // Although we have online status in store we do not yet have an updated channel so have to show it as disconnected + // This may happen after server restart when not all peers are yet connected + if _, connected := statuses[peerToReturn.ID]; !connected { + peerToReturn.Status.Connected = false + } + } + + return peerToReturn, nil +} + func (h *PeersHandler) getPeer(account *server.Account, peerID, userID string, w http.ResponseWriter) { peer, err := h.accountManager.GetPeer(account.Id, peerID, userID) if err != nil { @@ -38,7 +56,13 @@ func (h *PeersHandler) getPeer(account *server.Account, peerID, userID string, w return } - util.WriteJSONObject(w, toPeerResponse(peer, account, h.accountManager.GetDNSDomain())) + peerToReturn, err := h.checkPeerStatus(peer) + if err != nil { + util.WriteError(err, w) + return + } + + util.WriteJSONObject(w, toPeerResponse(peerToReturn, account, h.accountManager.GetDNSDomain())) } func (h *PeersHandler) updatePeer(account *server.Account, user *server.User, peerID string, w http.ResponseWriter, r *http.Request) { @@ -120,7 +144,12 @@ func (h *PeersHandler) GetAllPeers(w http.ResponseWriter, r *http.Request) { respBody := []*api.Peer{} for _, peer := range peers { - respBody = append(respBody, toPeerResponse(peer, account, dnsDomain)) + peerToReturn, err := h.checkPeerStatus(peer) + if err != nil { + util.WriteError(err, w) + return + } + respBody = append(respBody, toPeerResponse(peerToReturn, account, dnsDomain)) } util.WriteJSONObject(w, respBody) return diff --git a/management/server/http/peers_handler_test.go b/management/server/http/peers_handler_test.go index 7fe732f2f..1856861d5 100644 --- a/management/server/http/peers_handler_test.go +++ b/management/server/http/peers_handler_test.go @@ -3,6 +3,7 @@ package http import ( "bytes" "encoding/json" + "fmt" "io" "net" "net/http" @@ -23,19 +24,33 @@ import ( ) const testPeerID = "test_peer" +const noUpdateChannelTestPeerID = "no-update-channel" func initTestMetaData(peers ...*server.Peer) *PeersHandler { return &PeersHandler{ accountManager: &mock_server.MockAccountManager{ UpdatePeerFunc: func(accountID, userID string, update *server.Peer) (*server.Peer, error) { - p := peers[0].Copy() + var p *server.Peer + for _, peer := range peers { + if update.ID == peer.ID { + p = peer.Copy() + break + } + } p.SSHEnabled = update.SSHEnabled p.LoginExpirationEnabled = update.LoginExpirationEnabled p.Name = update.Name return p, nil }, GetPeerFunc: func(accountID, peerID, userID string) (*server.Peer, error) { - return peers[0], nil + var p *server.Peer + for _, peer := range peers { + if peerID == peer.ID { + p = peer.Copy() + break + } + } + return p, nil }, GetPeersFunc: func(accountID, userID string) ([]*server.Peer, error) { return peers, nil @@ -57,6 +72,16 @@ func initTestMetaData(peers ...*server.Peer) *PeersHandler { }, }, user, nil }, + GetAllConnectedPeersFunc: func() (map[string]struct{}, error) { + statuses := make(map[string]struct{}) + for _, peer := range peers { + if peer.ID == noUpdateChannelTestPeerID { + break + } + statuses[peer.ID] = struct{}{} + } + return statuses, nil + }, }, claimsExtractor: jwtclaims.NewClaimsExtractor( jwtclaims.WithFromRequestContext(func(r *http.Request) jwtclaims.AuthorizationClaims { @@ -79,7 +104,7 @@ func TestGetPeers(t *testing.T) { Key: "key", SetupKey: "setupkey", IP: net.ParseIP("100.64.0.1"), - Status: &server.PeerStatus{}, + Status: &server.PeerStatus{Connected: true}, Name: "PeerName", LoginExpirationEnabled: false, Meta: server.PeerSystemMeta{ @@ -93,11 +118,17 @@ func TestGetPeers(t *testing.T) { }, } + peer1 := peer.Copy() + peer1.ID = noUpdateChannelTestPeerID + expectedUpdatedPeer := peer.Copy() expectedUpdatedPeer.LoginExpirationEnabled = true expectedUpdatedPeer.SSHEnabled = true expectedUpdatedPeer.Name = "New Name" + expectedPeer1 := peer1.Copy() + expectedPeer1.Status.Connected = false + tt := []struct { name string expectedStatus int @@ -116,13 +147,21 @@ func TestGetPeers(t *testing.T) { expectedPeer: peer, }, { - name: "GetPeer", + name: "GetPeer with update channel", requestType: http.MethodGet, requestPath: "/api/peers/" + testPeerID, expectedStatus: http.StatusOK, expectedArray: false, expectedPeer: peer, }, + { + name: "GetPeer with no update channel", + requestType: http.MethodGet, + requestPath: "/api/peers/" + peer1.ID, + expectedStatus: http.StatusOK, + expectedArray: false, + expectedPeer: expectedPeer1, + }, { name: "PutPeer", requestType: http.MethodPut, @@ -136,7 +175,7 @@ func TestGetPeers(t *testing.T) { rr := httptest.NewRecorder() - p := initTestMetaData(peer) + p := initTestMetaData(peer, peer1) for _, tc := range tt { t.Run(tc.name, func(t *testing.T) { @@ -171,6 +210,10 @@ func TestGetPeers(t *testing.T) { t.Fatalf("Sent content is not in correct json format; %v", err) } + // hardcode this check for now as we only have two peers in this suite + assert.Equal(t, len(respBody), 2) + assert.Equal(t, respBody[1].Connected, false) + got = respBody[0] } else { got = &api.Peer{} @@ -180,12 +223,15 @@ func TestGetPeers(t *testing.T) { } } + fmt.Println(got) + assert.Equal(t, got.Name, tc.expectedPeer.Name) assert.Equal(t, got.Version, tc.expectedPeer.Meta.WtVersion) assert.Equal(t, got.Ip, tc.expectedPeer.IP.String()) assert.Equal(t, got.Os, "OS core") assert.Equal(t, got.LoginExpirationEnabled, tc.expectedPeer.LoginExpirationEnabled) assert.Equal(t, got.SshEnabled, tc.expectedPeer.SSHEnabled) + assert.Equal(t, got.Connected, tc.expectedPeer.Status.Connected) }) } } diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 5432b201b..ab3748c01 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -75,6 +75,7 @@ type MockAccountManager struct { LoginPeerFunc func(login server.PeerLogin) (*server.Peer, *server.NetworkMap, error) SyncPeerFunc func(sync server.PeerSync) (*server.Peer, *server.NetworkMap, error) InviteUserFunc func(accountID string, initiatorUserID string, targetUserEmail string) error + GetAllConnectedPeersFunc func() (map[string]struct{}, error) } // GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface @@ -583,3 +584,11 @@ func (am *MockAccountManager) SyncPeer(sync server.PeerSync) (*server.Peer, *ser } return nil, nil, status.Errorf(codes.Unimplemented, "method SyncPeer is not implemented") } + +// GetAllConnectedPeers mocks GetAllConnectedPeers of the AccountManager interface +func (am *MockAccountManager) GetAllConnectedPeers() (map[string]struct{}, error) { + if am.GetAllConnectedPeersFunc != nil { + return am.GetAllConnectedPeersFunc() + } + return nil, status.Errorf(codes.Unimplemented, "method GetAllConnectedPeers is not implemented") +} From b8599f634c06617092e1f2b1373f1b7d4ff6cf0d Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 11 Oct 2023 23:00:56 +0200 Subject: [PATCH 07/43] Fix nil pointer exception in group delete (#1211) Fix group delete panic In case if in the db the DNSSettings is null then can cause panic in delete group function because this field is pointer and it was not checked. Because of in the future implementation this variable will be filled in any case then make no sense to keep the pointer type. Fix DNSSettings copy function --- management/server/account.go | 21 +++++-------- management/server/account_test.go | 2 +- management/server/dns.go | 30 +++++-------------- management/server/dns_test.go | 2 +- .../server/http/dns_settings_handler_test.go | 4 +-- 5 files changed, 19 insertions(+), 40 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 8e453a1fe..0e583e17f 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -180,7 +180,7 @@ type Account struct { Policies []*Policy Routes map[string]*route.Route NameServerGroups map[string]*nbdns.NameServerGroup - DNSSettings *DNSSettings + DNSSettings DNSSettings // Settings is a dictionary of Account settings Settings *Settings } @@ -513,13 +513,11 @@ func (a *Account) getUserGroups(userID string) ([]string, error) { func (a *Account) getPeerDNSManagementStatus(peerID string) bool { peerGroups := a.getPeerGroups(peerID) enabled := true - if a.DNSSettings != nil { - for _, groupID := range a.DNSSettings.DisabledManagementGroups { - _, found := peerGroups[groupID] - if found { - enabled = false - break - } + for _, groupID := range a.DNSSettings.DisabledManagementGroups { + _, found := peerGroups[groupID] + if found { + enabled = false + break } } return enabled @@ -606,10 +604,7 @@ func (a *Account) Copy() *Account { nsGroups[id] = nsGroup.Copy() } - var dnsSettings *DNSSettings - if a.DNSSettings != nil { - dnsSettings = a.DNSSettings.Copy() - } + dnsSettings := a.DNSSettings.Copy() var settings *Settings if a.Settings != nil { @@ -1618,7 +1613,7 @@ func newAccountWithId(accountID, userID, domain string) *Account { setupKeys := map[string]*SetupKey{} nameServersGroups := make(map[string]*nbdns.NameServerGroup) users[userID] = NewAdminUser(userID) - dnsSettings := &DNSSettings{ + dnsSettings := DNSSettings{ DisabledManagementGroups: make([]string, 0), } log.Debugf("created new account %s", accountID) diff --git a/management/server/account_test.go b/management/server/account_test.go index 331df2017..e47b3b854 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1374,7 +1374,7 @@ func TestAccount_Copy(t *testing.T) { NameServers: []nbdns.NameServer{}, }, }, - DNSSettings: &DNSSettings{DisabledManagementGroups: []string{}}, + DNSSettings: DNSSettings{DisabledManagementGroups: []string{}}, Settings: &Settings{}, } err := hasNilField(account) diff --git a/management/server/dns.go b/management/server/dns.go index 252782aea..9707cc372 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -24,19 +24,11 @@ type DNSSettings struct { } // Copy returns a copy of the DNS settings -func (d *DNSSettings) Copy() *DNSSettings { - settings := &DNSSettings{ - DisabledManagementGroups: make([]string, 0), +func (d DNSSettings) Copy() DNSSettings { + settings := DNSSettings{ + DisabledManagementGroups: make([]string, len(d.DisabledManagementGroups)), } - - if d == nil { - return settings - } - - if d.DisabledManagementGroups != nil && len(d.DisabledManagementGroups) > 0 { - settings.DisabledManagementGroups = d.DisabledManagementGroups[:] - } - + copy(settings.DisabledManagementGroups, d.DisabledManagementGroups) return settings } @@ -58,12 +50,8 @@ func (am *DefaultAccountManager) GetDNSSettings(accountID string, userID string) if !user.IsAdmin() { return nil, status.Errorf(status.PermissionDenied, "only admins are allowed to view DNS settings") } - - if account.DNSSettings == nil { - return &DNSSettings{}, nil - } - - return account.DNSSettings.Copy(), nil + dnsSettings := account.DNSSettings.Copy() + return &dnsSettings, nil } // SaveDNSSettings validates a user role and updates the account's DNS settings @@ -96,11 +84,7 @@ func (am *DefaultAccountManager) SaveDNSSettings(accountID string, userID string } } - oldSettings := &DNSSettings{} - if account.DNSSettings != nil { - oldSettings = account.DNSSettings.Copy() - } - + oldSettings := account.DNSSettings.Copy() account.DNSSettings = dnsSettingsToSave.Copy() account.Network.IncSerial() diff --git a/management/server/dns_test.go b/management/server/dns_test.go index b089949b2..8c979c2a6 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -42,7 +42,7 @@ func TestGetDNSSettings(t *testing.T) { t.Fatal("DNS settings for new accounts shouldn't return nil") } - account.DNSSettings = &DNSSettings{ + account.DNSSettings = DNSSettings{ DisabledManagementGroups: []string{group1ID}, } diff --git a/management/server/http/dns_settings_handler_test.go b/management/server/http/dns_settings_handler_test.go index c7d135fd1..a2f65a521 100644 --- a/management/server/http/dns_settings_handler_test.go +++ b/management/server/http/dns_settings_handler_test.go @@ -26,7 +26,7 @@ const ( testDNSSettingsUserID = "test_user" ) -var baseExistingDNSSettings = &server.DNSSettings{ +var baseExistingDNSSettings = server.DNSSettings{ DisabledManagementGroups: []string{testDNSSettingsExistingGroup}, } @@ -43,7 +43,7 @@ func initDNSSettingsTestData() *DNSSettingsHandler { return &DNSSettingsHandler{ accountManager: &mock_server.MockAccountManager{ GetDNSSettingsFunc: func(accountID string, userID string) (*server.DNSSettings, error) { - return testingDNSSettingsAccount.DNSSettings, nil + return &testingDNSSettingsAccount.DNSSettings, nil }, SaveDNSSettingsFunc: func(accountID string, userID string, dnsSettingsToSave *server.DNSSettings) error { if dnsSettingsToSave != nil { From 2b90ff8c2486a626e08fb495db6772ddfd5bcd2b Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 11 Oct 2023 23:01:49 +0200 Subject: [PATCH 08/43] Fix/key backup in config script (#1206) Because we provide the option to regenerate the config files, the encryption key could be lost. - The configure.sh read the existing key and write it back during the config generation - Backup the previously generated config files before overwrite it - Fix invalid json output in the Extras field - Reduce the error logs in case if the encryption key is invalid - Response in the events API with valid user info in any cases - Add extra error handling to the configure.sh. I.e. handle the invalid OpenID urls --- infrastructure_files/configure.sh | 28 +++++++++++++++++++-- infrastructure_files/management.json.tmpl | 1 + management/server/activity/sqlite/sqlite.go | 22 +++++++++++----- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh index 3db799068..09152d788 100755 --- a/infrastructure_files/configure.sh +++ b/infrastructure_files/configure.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e if ! which curl >/dev/null 2>&1; then echo "This script uses curl fetch OpenID configuration from IDP." @@ -154,6 +155,8 @@ if [ -n "$NETBIRD_MGMT_IDP" ]; then export NETBIRD_IDP_MGMT_CLIENT_ID export NETBIRD_IDP_MGMT_CLIENT_SECRET export NETBIRD_IDP_MGMT_EXTRA_CONFIG=$EXTRA_CONFIG +else + export NETBIRD_IDP_MGMT_EXTRA_CONFIG={} fi IFS=',' read -r -a REDIRECT_URL_PORTS <<< "$NETBIRD_AUTH_PKCE_REDIRECT_URL_PORTS" @@ -170,8 +173,29 @@ if [ "$NETBIRD_DASH_AUTH_USE_AUDIENCE" = "false" ]; then export NETBIRD_AUTH_PKCE_AUDIENCE= fi +# Read the encryption key +if test -f 'management.json'; then + encKey=$(jq -r ".DataStoreEncryptionKey" management.json) + if [[ "$encKey" != "null" ]]; then + export NETBIRD_DATASTORE_ENC_KEY=$encKey + + fi +fi + env | grep NETBIRD +bkp_postfix="$(date +%s)" +if test -f 'docker-compose.yml'; then + cp docker-compose.yml "docker-compose.yml.bkp.${bkp_postfix}" +fi + +if test -f 'management.json'; then + cp management.json "management.json.bkp.${bkp_postfix}" +fi + +if test -f 'turnserver.conf'; then + cp turnserver.conf "turnserver.conf.bpk.${bkp_postfix}" +fi envsubst docker-compose.yml -envsubst management.json -envsubst turnserver.conf \ No newline at end of file +envsubst management.json +envsubst turnserver.conf diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index e185faa6e..847ce6222 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -27,6 +27,7 @@ "Password": null }, "Datadir": "", + "DataStoreEncryptionKey": "$NETBIRD_DATASTORE_ENC_KEY", "HttpConfig": { "Address": "0.0.0.0:$NETBIRD_MGMT_API_PORT", "AuthIssuer": "$NETBIRD_AUTH_AUTHORITY", diff --git a/management/server/activity/sqlite/sqlite.go b/management/server/activity/sqlite/sqlite.go index 6af4d4d8d..a5130b0c5 100644 --- a/management/server/activity/sqlite/sqlite.go +++ b/management/server/activity/sqlite/sqlite.go @@ -45,6 +45,9 @@ const ( "VALUES(?, ?, ?, ?, ?, ?)" insertDeleteUserQuery = `INSERT INTO deleted_users(id, email, name) VALUES(?, ?, ?)` + + fallbackName = "unknown" + fallbackEmail = "unknown@unknown.com" ) // Store is the implementation of the activity.Store interface backed by SQLite @@ -128,6 +131,7 @@ func NewSQLiteStore(dataDir string, encryptionKey string) (*Store, error) { func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) { events := make([]*activity.Event, 0) + var cryptErr error for result.Next() { var id int64 var operation activity.Activity @@ -156,8 +160,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) { if targetUserName != nil { name, err := store.fieldEncrypt.Decrypt(*targetUserName) if err != nil { - log.Errorf("failed to decrypt username for target id: %s", target) - meta["username"] = "" + cryptErr = fmt.Errorf("failed to decrypt username for target id: %s", target) + meta["username"] = fallbackName } else { meta["username"] = name } @@ -166,8 +170,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) { if targetEmail != nil { email, err := store.fieldEncrypt.Decrypt(*targetEmail) if err != nil { - log.Errorf("failed to decrypt email address for target id: %s", target) - meta["email"] = "" + cryptErr = fmt.Errorf("failed to decrypt email address for target id: %s", target) + meta["email"] = fallbackEmail } else { meta["email"] = email } @@ -186,7 +190,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) { if initiatorName != nil { name, err := store.fieldEncrypt.Decrypt(*initiatorName) if err != nil { - log.Errorf("failed to decrypt username of initiator: %s", initiator) + cryptErr = fmt.Errorf("failed to decrypt username of initiator: %s", initiator) + event.InitiatorName = fallbackName } else { event.InitiatorName = name } @@ -195,7 +200,8 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) { if initiatorEmail != nil { email, err := store.fieldEncrypt.Decrypt(*initiatorEmail) if err != nil { - log.Errorf("failed to decrypt email address of initiator: %s", initiator) + cryptErr = fmt.Errorf("failed to decrypt email address of initiator: %s", initiator) + event.InitiatorEmail = fallbackEmail } else { event.InitiatorEmail = email } @@ -204,6 +210,10 @@ func (store *Store) processResult(result *sql.Rows) ([]*activity.Event, error) { events = append(events, event) } + if cryptErr != nil { + log.Warnf("%s", cryptErr) + } + return events, nil } From 32880c56a41acd604615604e538a8f1054bef11a Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Thu, 12 Oct 2023 15:42:36 +0200 Subject: [PATCH 09/43] Implement SQLite Store using gorm and relational approach (#1065) Restructure data handling for improved performance and flexibility. Introduce 'G'-prefixed fields to represent Gorm relations, simplifying resource management. Eliminate complexity in lookup tables for enhanced query and write speed. Enable independent operations on data structures, requiring adjustments in the Store interface and Account Manager. --- .github/workflows/golang-test-darwin.yml | 5 +- .github/workflows/golang-test-linux.yml | 18 +- .github/workflows/golang-test-windows.yml | 5 + .gitignore | 3 +- client/cmd/testutil.go | 2 +- client/internal/engine_test.go | 5 +- dns/nameserver.go | 10 +- go.mod | 6 +- go.sum | 12 +- management/client/client_test.go | 2 +- management/cmd/management.go | 2 +- management/cmd/migration_down.go | 66 +++ management/cmd/migration_up.go | 66 +++ management/cmd/root.go | 14 + management/server/account.go | 35 +- management/server/account_test.go | 21 +- management/server/config.go | 2 + management/server/dns.go | 2 +- management/server/dns_test.go | 2 +- management/server/file_store.go | 24 + management/server/file_store_test.go | 2 +- management/server/group.go | 5 +- management/server/group_test.go | 5 + management/server/management_proto_test.go | 2 +- management/server/management_test.go | 3 +- management/server/metrics/selfhosted.go | 2 + management/server/metrics/selfhosted_test.go | 9 + management/server/nameserver_test.go | 2 +- management/server/network.go | 24 +- management/server/peer.go | 13 +- management/server/peer_test.go | 4 +- management/server/personal_access_token.go | 4 +- management/server/policy.go | 21 +- management/server/route_test.go | 2 +- management/server/rule.go | 7 +- management/server/setupkey.go | 9 +- management/server/sqlite_store.go | 457 +++++++++++++++++++ management/server/sqlite_store_test.go | 229 ++++++++++ management/server/store.go | 67 ++- management/server/store_test.go | 88 ++++ management/server/testdata/store.json | 63 ++- management/server/user.go | 10 +- management/server/user_test.go | 6 + route/route.go | 10 +- 44 files changed, 1239 insertions(+), 107 deletions(-) create mode 100644 management/cmd/migration_down.go create mode 100644 management/cmd/migration_up.go create mode 100644 management/server/sqlite_store.go create mode 100644 management/server/sqlite_store_test.go create mode 100644 management/server/store_test.go diff --git a/.github/workflows/golang-test-darwin.yml b/.github/workflows/golang-test-darwin.yml index 97fdeabe8..8cd28bcb4 100644 --- a/.github/workflows/golang-test-darwin.yml +++ b/.github/workflows/golang-test-darwin.yml @@ -12,6 +12,9 @@ concurrency: jobs: test: + strategy: + matrix: + store: ['JsonFile', 'Sqlite'] runs-on: macos-latest steps: - name: Install Go @@ -33,4 +36,4 @@ jobs: run: go mod tidy - name: Test - run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... + run: NETBIRD_STORE_KIND=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 13061f6eb..026779885 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -15,6 +15,7 @@ jobs: strategy: matrix: arch: ['386','amd64'] + store: ['JsonFile', 'Sqlite'] runs-on: ubuntu-latest steps: - name: Install Go @@ -41,17 +42,16 @@ jobs: run: go mod tidy - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_KIND=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... test_client_on_docker: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Install Go uses: actions/setup-go@v4 with: go-version: "1.20.x" - - name: Cache Go modules uses: actions/cache@v3 with: @@ -64,7 +64,7 @@ jobs: uses: actions/checkout@v3 - name: Install dependencies - run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib - name: Install modules run: go mod tidy @@ -82,7 +82,7 @@ jobs: run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/... - name: Generate Engine Test bin - run: CGO_ENABLED=0 go test -c -o engine-testing.bin ./client/internal + run: CGO_ENABLED=1 go test -c -o engine-testing.bin ./client/internal - name: Generate Peer Test bin run: CGO_ENABLED=0 go test -c -o peer-testing.bin ./client/internal/peer/... @@ -95,15 +95,17 @@ jobs: - 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 -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 -test.timeout 5m -test.parallel 1 - name: Run nftables Manager tests in docker run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-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 -test.timeout 5m -test.parallel 1 + - name: Run Engine tests in docker with file store + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_KIND="JsonFile" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1 + + - name: Run Engine tests in docker with sqlite store + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_KIND="Sqlite" --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 -test.timeout 5m -test.parallel 1 \ No newline at end of file diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index 6dd91666c..1fc84ff2a 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -14,6 +14,9 @@ concurrency: jobs: test: + strategy: + matrix: + store: ['JsonFile', 'Sqlite'] runs-on: windows-latest steps: - name: Checkout code @@ -40,6 +43,8 @@ jobs: - run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\' - run: choco install -y sysinternals + - run: choco install -y mingw + - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=C:\Users\runneradmin\go\pkg\mod - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=C:\Users\runneradmin\AppData\Local\go-build diff --git a/.gitignore b/.gitignore index dc62780ad..7edcc7087 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ client/.distfiles/ infrastructure_files/setup.env infrastructure_files/setup-*.env .vscode -.DS_Store \ No newline at end of file +.DS_Store +*.db diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index 6d47021dd..47ae9ddb4 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -65,7 +65,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste t.Fatal(err) } s := grpc.NewServer() - store, err := mgmt.NewFileStore(config.Datadir, nil) + store, err := mgmt.NewStoreFromJson(config.Datadir, nil) if err != nil { t.Fatal(err) } diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index ea4a23a8d..42012bd0a 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -1039,10 +1039,11 @@ func startManagement(dataDir string) (*grpc.Server, string, error) { return nil, "", err } s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)) - store, err := server.NewFileStore(config.Datadir, nil) + store, err := server.NewStoreFromJson(config.Datadir, nil) if err != nil { - log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) + return nil, "", err } + peersUpdateManager := server.NewPeersUpdateManager() eventStore := &activity.InMemoryEventStore{} if err != nil { diff --git a/dns/nameserver.go b/dns/nameserver.go index 7751f8e1c..f3ae2569d 100644 --- a/dns/nameserver.go +++ b/dns/nameserver.go @@ -50,19 +50,21 @@ func ToNameServerType(typeString string) NameServerType { // NameServerGroup group of nameservers and with group ids type NameServerGroup struct { // ID identifier of group - ID string + ID string `gorm:"primaryKey"` + // AccountID is a reference to Account that this object belongs + AccountID string `gorm:"index"` // Name group name Name string // Description group description Description string // NameServers list of nameservers - NameServers []NameServer + NameServers []NameServer `gorm:"serializer:json"` // Groups list of peer group IDs to distribute the nameservers information - Groups []string + Groups []string `gorm:"serializer:json"` // Primary indicates that the nameserver group is the primary resolver for any dns query Primary bool // Domains indicate the dns query domains to use with this nameserver group - Domains []string + Domains []string `gorm:"serializer:json"` // Enabled group status Enabled bool } diff --git a/go.mod b/go.mod index 8be159997..1f8eec24e 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/hashicorp/go-version v1.6.0 github.com/libp2p/go-netroute v0.2.0 github.com/magiconair/properties v1.8.5 - github.com/mattn/go-sqlite3 v1.14.16 + github.com/mattn/go-sqlite3 v1.14.17 github.com/mdlayher/socket v0.4.0 github.com/miekg/dns v1.1.43 github.com/mitchellh/hashstructure/v2 v2.0.2 @@ -74,6 +74,8 @@ require ( golang.org/x/term v0.8.0 google.golang.org/api v0.126.0 gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/sqlite v1.5.3 + gorm.io/gorm v1.25.4 ) require ( @@ -110,6 +112,8 @@ require ( github.com/googleapis/gax-go/v2 v2.10.0 // indirect github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/josharian/native v1.0.0 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect diff --git a/go.sum b/go.sum index 25182ca85..15e69283c 100644 --- a/go.sum +++ b/go.sum @@ -383,6 +383,10 @@ github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE= github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= @@ -441,8 +445,8 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -1189,6 +1193,10 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.5.3 h1:7/0dUgX28KAcopdfbRWWl68Rflh6osa4rDh+m51KL2g= +gorm.io/driver/sqlite v1.5.3/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4= +gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= +gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY= diff --git a/management/client/client_test.go b/management/client/client_test.go index 86c598adb..b66dacc73 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -53,7 +53,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) { t.Fatal(err) } s := grpc.NewServer() - store, err := mgmt.NewFileStore(config.Datadir, nil) + store, err := mgmt.NewStoreFromJson(config.Datadir, nil) if err != nil { t.Fatal(err) } diff --git a/management/cmd/management.go b/management/cmd/management.go index f85cf225e..fda16566c 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -126,7 +126,7 @@ var ( if err != nil { return err } - store, err := server.NewFileStore(config.Datadir, appMetrics) + store, err := server.NewStore(config.StoreKind, config.Datadir, appMetrics) if err != nil { return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err) } diff --git a/management/cmd/migration_down.go b/management/cmd/migration_down.go new file mode 100644 index 000000000..6d136ec1a --- /dev/null +++ b/management/cmd/migration_down.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "errors" + "flag" + "fmt" + "os" + "path" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var shortDown = "Rollback SQLite store to JSON file store. Please make a backup of the SQLite file before running this command." + +var downCmd = &cobra.Command{ + Use: "downgrade [--datadir directory] [--log-file console]", + Aliases: []string{"down"}, + Short: shortDown, + Long: shortDown + + "\n\n" + + "This command reads the content of {datadir}/store.db and migrates it to {datadir}/store.json that can be used by File store driver.", + RunE: func(cmd *cobra.Command, args []string) error { + flag.Parse() + err := util.InitLog(logLevel, logFile) + if err != nil { + return fmt.Errorf("failed initializing log %v", err) + } + + sqliteStorePath := path.Join(mgmtDataDir, "store.db") + if _, err := os.Stat(sqliteStorePath); errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("%s doesn't exist, couldn't continue the operation", sqliteStorePath) + } + + fileStorePath := path.Join(mgmtDataDir, "store.json") + if _, err := os.Stat(fileStorePath); err == nil { + return fmt.Errorf("%s already exists, couldn't continue the operation", fileStorePath) + } + + sqlstore, err := server.NewSqliteStore(mgmtDataDir, nil) + if err != nil { + return fmt.Errorf("failed creating file store: %s: %v", mgmtDataDir, err) + } + + sqliteStoreAccounts := len(sqlstore.GetAllAccounts()) + log.Infof("%d account will be migrated from sqlite store %s to file store %s", + sqliteStoreAccounts, sqliteStorePath, fileStorePath) + + store, err := server.NewFilestoreFromSqliteStore(sqlstore, mgmtDataDir, nil) + if err != nil { + return fmt.Errorf("failed creating file store: %s: %v", mgmtDataDir, err) + } + + fsStoreAccounts := len(store.GetAllAccounts()) + if fsStoreAccounts != sqliteStoreAccounts { + return fmt.Errorf("failed to migrate accounts from sqlite to file[]. Expected accounts: %d, got: %d", + sqliteStoreAccounts, fsStoreAccounts) + } + + log.Info("Migration finished successfully") + + return nil + }, +} diff --git a/management/cmd/migration_up.go b/management/cmd/migration_up.go new file mode 100644 index 000000000..5c7505cfc --- /dev/null +++ b/management/cmd/migration_up.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "errors" + "flag" + "fmt" + "os" + "path" + + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/util" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var shortUp = "Migrate JSON file store to SQLite store. Please make a backup of the JSON file before running this command." + +var upCmd = &cobra.Command{ + Use: "upgrade [--datadir directory] [--log-file console]", + Aliases: []string{"up"}, + Short: shortUp, + Long: shortUp + + "\n\n" + + "This command reads the content of {datadir}/store.json and migrates it to {datadir}/store.db that can be used by SQLite store driver.", + RunE: func(cmd *cobra.Command, args []string) error { + flag.Parse() + err := util.InitLog(logLevel, logFile) + if err != nil { + return fmt.Errorf("failed initializing log %v", err) + } + + fileStorePath := path.Join(mgmtDataDir, "store.json") + if _, err := os.Stat(fileStorePath); errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("%s doesn't exist, couldn't continue the operation", fileStorePath) + } + + sqlStorePath := path.Join(mgmtDataDir, "store.db") + if _, err := os.Stat(sqlStorePath); err == nil { + return fmt.Errorf("%s already exists, couldn't continue the operation", sqlStorePath) + } + + fstore, err := server.NewFileStore(mgmtDataDir, nil) + if err != nil { + return fmt.Errorf("failed creating file store: %s: %v", mgmtDataDir, err) + } + + fsStoreAccounts := len(fstore.GetAllAccounts()) + log.Infof("%d account will be migrated from file store %s to sqlite store %s", + fsStoreAccounts, fileStorePath, sqlStorePath) + + store, err := server.NewSqliteStoreFromFileStore(fstore, mgmtDataDir, nil) + if err != nil { + return fmt.Errorf("failed creating file store: %s: %v", mgmtDataDir, err) + } + + sqliteStoreAccounts := len(store.GetAllAccounts()) + if fsStoreAccounts != sqliteStoreAccounts { + return fmt.Errorf("failed to migrate accounts from file to sqlite. Expected accounts: %d, got: %d", + fsStoreAccounts, sqliteStoreAccounts) + } + + log.Info("Migration finished successfully") + + return nil + }, +} diff --git a/management/cmd/root.go b/management/cmd/root.go index 2080a6b29..d8a9da53f 100644 --- a/management/cmd/root.go +++ b/management/cmd/root.go @@ -34,6 +34,12 @@ var ( SilenceUsage: true, } + migrationCmd = &cobra.Command{ + Use: "sqlite-migration", + Short: "Contains sub-commands to perform JSON file store to SQLite store migration and rollback", + Long: "", + SilenceUsage: true, + } // Execution control channel for stopCh signal stopCh chan int ) @@ -63,6 +69,14 @@ func init() { rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "") rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the the log will be output to stdout") rootCmd.AddCommand(mgmtCmd) + + migrationCmd.PersistentFlags().StringVar(&mgmtDataDir, "datadir", defaultMgmtDataDir, "server data directory location") + migrationCmd.MarkFlagRequired("datadir") //nolint + + migrationCmd.AddCommand(upCmd) + migrationCmd.AddCommand(downCmd) + + rootCmd.AddCommand(migrationCmd) } // SetupCloseHandler handles SIGTERM signal and exits with success diff --git a/management/server/account.go b/management/server/account.go index 0e583e17f..f78530b44 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -165,24 +165,33 @@ func (s *Settings) Copy() *Settings { // Account represents a unique account of the system type Account struct { - Id string + // we have to name column to aid as it collides with Network.Id when work with associations + Id string `gorm:"primaryKey"` + // User.Id it was created by CreatedBy string - Domain string + Domain string `gorm:"index"` DomainCategory string IsDomainPrimaryAccount bool - SetupKeys map[string]*SetupKey - Network *Network - Peers map[string]*Peer - Users map[string]*User - Groups map[string]*Group - Rules map[string]*Rule - Policies []*Policy - Routes map[string]*route.Route - NameServerGroups map[string]*nbdns.NameServerGroup - DNSSettings DNSSettings + SetupKeys map[string]*SetupKey `gorm:"-"` + SetupKeysG []SetupKey `json:"-" gorm:"foreignKey:AccountID;references:id"` + Network *Network `gorm:"embedded;embeddedPrefix:network_"` + Peers map[string]*Peer `gorm:"-"` + PeersG []Peer `json:"-" gorm:"foreignKey:AccountID;references:id"` + Users map[string]*User `gorm:"-"` + UsersG []User `json:"-" gorm:"foreignKey:AccountID;references:id"` + Groups map[string]*Group `gorm:"-"` + GroupsG []Group `json:"-" gorm:"foreignKey:AccountID;references:id"` + Rules map[string]*Rule `gorm:"-"` + RulesG []Rule `json:"-" gorm:"foreignKey:AccountID;references:id"` + Policies []*Policy `gorm:"foreignKey:AccountID;references:id"` + Routes map[string]*route.Route `gorm:"-"` + RoutesG []route.Route `json:"-" gorm:"foreignKey:AccountID;references:id"` + NameServerGroups map[string]*nbdns.NameServerGroup `gorm:"-"` + NameServerGroupsG []nbdns.NameServerGroup `json:"-" gorm:"foreignKey:AccountID;references:id"` + DNSSettings DNSSettings `gorm:"embedded;embeddedPrefix:dns_settings_"` // Settings is a dictionary of Account settings - Settings *Settings + Settings *Settings `gorm:"embedded;embeddedPrefix:settings_"` } type UserInfo struct { diff --git a/management/server/account_test.go b/management/server/account_test.go index e47b3b854..181e1c3fe 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -198,11 +198,11 @@ func TestAccount_GetPeerNetworkMap(t *testing.T) { netIP := net.IP{100, 64, 0, 0} netMask := net.IPMask{255, 255, 0, 0} network := &Network{ - Id: "network", - Net: net.IPNet{IP: netIP, Mask: netMask}, - Dns: "netbird.selfhosted", - Serial: 0, - mu: sync.Mutex{}, + Identifier: "network", + Net: net.IPNet{IP: netIP, Mask: netMask}, + Dns: "netbird.selfhosted", + Serial: 0, + mu: sync.Mutex{}, } for _, testCase := range tt { @@ -476,7 +476,7 @@ func TestDefaultAccountManager_GetGroupsFromTheToken(t *testing.T) { // as initAccount was created without account id we have to take the id after account initialization // that happens inside the GetAccountByUserOrAccountID where the id is getting generated // it is important to set the id as it help to avoid creating additional account with empty Id and re-pointing indices to it - initAccount.Id = acc.Id + initAccount = acc claims := jwtclaims.AuthorizationClaims{ AccountId: accountID, // is empty as it is based on accountID right after initialization of initAccount @@ -1025,7 +1025,6 @@ func TestAccountManager_NetworkUpdates(t *testing.T) { wg.Wait() }) - t.Run("delete peer update", func(t *testing.T) { wg.Add(1) go func() { @@ -1309,7 +1308,7 @@ func TestAccount_Copy(t *testing.T) { }, }, Network: &Network{ - Id: "net1", + Identifier: "net1", }, Peers: map[string]*Peer{ "peer1": { @@ -1400,6 +1399,10 @@ func hasNilField(x interface{}) error { rv := reflect.ValueOf(x) rv = rv.Elem() for i := 0; i < rv.NumField(); i++ { + // skip gorm internal fields + if json, ok := rv.Type().Field(i).Tag.Lookup("json"); ok && json == "-" { + continue + } if f := rv.Field(i); f.IsValid() { k := f.Kind() switch k { @@ -2045,7 +2048,7 @@ func createManager(t *testing.T) (*DefaultAccountManager, error) { func createStore(t *testing.T) (Store, error) { dataDir := t.TempDir() - store, err := NewFileStore(dataDir, nil) + store, err := NewStoreFromJson(dataDir, nil) if err != nil { return nil, err } diff --git a/management/server/config.go b/management/server/config.go index 31c1cf45c..19a71ff7a 100644 --- a/management/server/config.go +++ b/management/server/config.go @@ -45,6 +45,8 @@ type Config struct { DeviceAuthorizationFlow *DeviceAuthorizationFlow PKCEAuthorizationFlow *PKCEAuthorizationFlow + + StoreKind StoreKind } // GetAuthAudiences returns the audience from the http config and device authorization flow config diff --git a/management/server/dns.go b/management/server/dns.go index 9707cc372..7b25e230f 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -20,7 +20,7 @@ type lookupMap map[string]struct{} // DNSSettings defines dns settings at the account level type DNSSettings struct { // DisabledManagementGroups groups whose DNS management is disabled - DisabledManagementGroups []string + DisabledManagementGroups []string `gorm:"serializer:json"` } // Copy returns a copy of the DNS settings diff --git a/management/server/dns_test.go b/management/server/dns_test.go index 8c979c2a6..a2c9d3aa2 100644 --- a/management/server/dns_test.go +++ b/management/server/dns_test.go @@ -196,7 +196,7 @@ func createDNSManager(t *testing.T) (*DefaultAccountManager, error) { func createDNSStore(t *testing.T) (Store, error) { dataDir := t.TempDir() - store, err := NewFileStore(dataDir, nil) + store, err := NewStoreFromJson(dataDir, nil) if err != nil { return nil, err } diff --git a/management/server/file_store.go b/management/server/file_store.go index b90b1d607..c8d24433f 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -54,6 +54,25 @@ func NewFileStore(dataDir string, metrics telemetry.AppMetrics) (*FileStore, err return fs, nil } +// NewFilestoreFromSqliteStore restores a store from Sqlite and stores to Filestore json in the file located in datadir +func NewFilestoreFromSqliteStore(sqlitestore *SqliteStore, dataDir string, metrics telemetry.AppMetrics) (*FileStore, error) { + store, err := NewFileStore(dataDir, metrics) + if err != nil { + return nil, err + } + + err = store.SaveInstallationID(sqlitestore.GetInstallationID()) + if err != nil { + return nil, err + } + + for _, account := range sqlitestore.GetAllAccounts() { + store.Accounts[account.Id] = account + } + + return store, store.persist(store.storeFile) +} + // restore the state of the store from the file. // Creates a new empty store file if doesn't exist func restore(file string) (*FileStore, error) { @@ -595,3 +614,8 @@ func (s *FileStore) Close() error { return s.persist(s.storeFile) } + +// GetStoreKind returns FileStoreKind +func (s *FileStore) GetStoreKind() StoreKind { + return FileStoreKind +} diff --git a/management/server/file_store_test.go b/management/server/file_store_test.go index e2f07acda..705e9f149 100644 --- a/management/server/file_store_test.go +++ b/management/server/file_store_test.go @@ -387,7 +387,7 @@ func TestFileStore_GetAccount(t *testing.T) { assert.Equal(t, expected.DomainCategory, account.DomainCategory) assert.Equal(t, expected.Domain, account.Domain) assert.Equal(t, expected.CreatedBy, account.CreatedBy) - assert.Equal(t, expected.Network.Id, account.Network.Id) + assert.Equal(t, expected.Network.Identifier, account.Network.Identifier) assert.Len(t, account.Peers, len(expected.Peers)) assert.Len(t, account.Users, len(expected.Users)) assert.Len(t, account.SetupKeys, len(expected.SetupKeys)) diff --git a/management/server/group.go b/management/server/group.go index a7502134a..28606e02d 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -23,6 +23,9 @@ type Group struct { // ID of the group ID string + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index"` + // Name visible in the UI Name string @@ -30,7 +33,7 @@ type Group struct { Issued string // Peers list of the group - Peers []string + Peers []string `gorm:"serializer:json"` } // EventMeta returns activity event meta related to the group diff --git a/management/server/group_test.go b/management/server/group_test.go index 3e2d6d3cc..e300fe7fb 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -80,6 +80,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { groupForRoute := &Group{ "grp-for-route", + "account-id", "Group for route", GroupIssuedAPI, make([]string, 0), @@ -87,6 +88,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { groupForNameServerGroups := &Group{ "grp-for-name-server-grp", + "account-id", "Group for name server groups", GroupIssuedAPI, make([]string, 0), @@ -94,6 +96,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { groupForPolicies := &Group{ "grp-for-policies", + "account-id", "Group for policies", GroupIssuedAPI, make([]string, 0), @@ -101,6 +104,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { groupForSetupKeys := &Group{ "grp-for-keys", + "account-id", "Group for setup keys", GroupIssuedAPI, make([]string, 0), @@ -108,6 +112,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { groupForUsers := &Group{ "grp-for-users", + "account-id", "Group for users", GroupIssuedAPI, make([]string, 0), diff --git a/management/server/management_proto_test.go b/management/server/management_proto_test.go index b4a527e46..06fc6669d 100644 --- a/management/server/management_proto_test.go +++ b/management/server/management_proto_test.go @@ -405,7 +405,7 @@ func startManagement(t *testing.T, config *Config) (*grpc.Server, string, error) return nil, "", err } s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)) - store, err := NewFileStore(config.Datadir, nil) + store, err := NewStoreFromJson(config.Datadir, nil) if err != nil { return nil, "", err } diff --git a/management/server/management_test.go b/management/server/management_test.go index fa35cfdef..375e7e634 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -393,6 +393,7 @@ var _ = Describe("Management service", func() { ipChannel := make(chan string, 20) for i := 0; i < initialPeers; i++ { go func() { + defer GinkgoRecover() key, _ := wgtypes.GenerateKey() loginPeerWithValidSetupKey(serverPubKey, key, client) encryptedBytes, err := encryption.EncryptMessage(serverPubKey, key, &mgmtProto.SyncRequest{}) @@ -496,7 +497,7 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { Expect(err).NotTo(HaveOccurred()) s := grpc.NewServer() - store, err := server.NewFileStore(config.Datadir, nil) + store, err := server.NewStoreFromJson(config.Datadir, nil) if err != nil { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } diff --git a/management/server/metrics/selfhosted.go b/management/server/metrics/selfhosted.go index 3b3db0baa..59364b940 100644 --- a/management/server/metrics/selfhosted.go +++ b/management/server/metrics/selfhosted.go @@ -48,6 +48,7 @@ type properties map[string]interface{} // DataSource metric data source type DataSource interface { GetAllAccounts() []*server.Account + GetStoreKind() server.StoreKind } // ConnManager peer connection manager that holds state for current active connections @@ -295,6 +296,7 @@ func (w *Worker) generateProperties() properties { metricsProperties["max_active_peer_version"] = maxActivePeerVersion metricsProperties["ui_clients"] = uiClient metricsProperties["idp_manager"] = w.idpManager + metricsProperties["store_kind"] = w.dataSource.GetStoreKind() for protocol, count := range rulesProtocol { metricsProperties["rules_protocol_"+protocol] = count diff --git a/management/server/metrics/selfhosted_test.go b/management/server/metrics/selfhosted_test.go index c61613fd2..f69c0f8f8 100644 --- a/management/server/metrics/selfhosted_test.go +++ b/management/server/metrics/selfhosted_test.go @@ -151,6 +151,11 @@ func (mockDatasource) GetAllAccounts() []*server.Account { } } +// GetStoreKind returns FileStoreKind +func (mockDatasource) GetStoreKind() server.StoreKind { + return server.FileStoreKind +} + // TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties func TestGenerateProperties(t *testing.T) { ds := mockDatasource{} @@ -236,4 +241,8 @@ func TestGenerateProperties(t *testing.T) { if properties["user_peers"] != 2 { t.Errorf("expected 2 user_peers, got %d", properties["user_peers"]) } + + if properties["store_kind"] != server.FileStoreKind { + t.Errorf("expected JsonFile, got %s", properties["store_kind"]) + } } diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 26977116b..8809dc8ad 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -749,7 +749,7 @@ func createNSManager(t *testing.T) (*DefaultAccountManager, error) { func createNSStore(t *testing.T) (Store, error) { dataDir := t.TempDir() - store, err := NewFileStore(dataDir, nil) + store, err := NewStoreFromJson(dataDir, nil) if err != nil { return nil, err } diff --git a/management/server/network.go b/management/server/network.go index 70f218f66..c5b165cae 100644 --- a/management/server/network.go +++ b/management/server/network.go @@ -34,14 +34,14 @@ type NetworkMap struct { } type Network struct { - Id string - Net net.IPNet - Dns string + Identifier string `json:"id"` + Net net.IPNet `gorm:"serializer:gob"` + Dns string // Serial is an ID that increments by 1 when any change to the network happened (e.g. new peer has been added). // Used to synchronize state to the client apps. Serial uint64 - mu sync.Mutex `json:"-"` + mu sync.Mutex `json:"-" gorm:"-"` } // NewNetwork creates a new Network initializing it with a Serial=0 @@ -56,10 +56,10 @@ func NewNetwork() *Network { intn := r.Intn(len(sub)) return &Network{ - Id: xid.New().String(), - Net: sub[intn].IPNet, - Dns: "", - Serial: 0} + Identifier: xid.New().String(), + Net: sub[intn].IPNet, + Dns: "", + Serial: 0} } // IncSerial increments Serial by 1 reflecting that the network state has been changed @@ -78,10 +78,10 @@ func (n *Network) CurrentSerial() uint64 { func (n *Network) Copy() *Network { return &Network{ - Id: n.Id, - Net: n.Net, - Dns: n.Dns, - Serial: n.Serial, + Identifier: n.Identifier, + Net: n.Net, + Dns: n.Dns, + Serial: n.Serial, } } diff --git a/management/server/peer.go b/management/server/peer.go index e5c6e39d6..f38e19e87 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -72,22 +72,24 @@ type PeerLogin struct { // The Peer is a WireGuard peer identified by a public key type Peer struct { // ID is an internal ID of the peer - ID string + ID string `gorm:"primaryKey"` + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index;uniqueIndex:idx_peers_account_id_ip"` // WireGuard public key - Key string + Key string `gorm:"index"` // A setup key this peer was registered with SetupKey string // IP address of the Peer - IP net.IP + IP net.IP `gorm:"uniqueIndex:idx_peers_account_id_ip"` // Meta is a Peer system meta data - Meta PeerSystemMeta + Meta PeerSystemMeta `gorm:"embedded;embeddedPrefix:meta_"` // Name is peer's name (machine name) Name string // DNSLabel is the parsed peer name for domain resolution. It is used to form an FQDN by appending the account's // domain to the peer label. e.g. peer-dns-label.netbird.cloud DNSLabel string // Status peer's management connection status - Status *PeerStatus + Status *PeerStatus `gorm:"embedded;embeddedPrefix:peer_status_"` // The user ID that registered the peer UserID string // SSHKey is a public SSH key of the peer @@ -116,6 +118,7 @@ func (p *Peer) Copy() *Peer { } return &Peer{ ID: p.ID, + AccountID: p.AccountID, Key: p.Key, SetupKey: p.SetupKey, IP: p.IP, diff --git a/management/server/peer_test.go b/management/server/peer_test.go index 36e96df43..9d5a8bfb9 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -369,8 +369,8 @@ func TestAccountManager_GetPeerNetwork(t *testing.T) { return } - if account.Network.Id != network.Id { - t.Errorf("expecting Account Networks ID to be equal, got %s expected %s", network.Id, account.Network.Id) + if account.Network.Identifier != network.Identifier { + t.Errorf("expecting Account Networks ID to be equal, got %s expected %s", network.Identifier, account.Network.Identifier) } } diff --git a/management/server/personal_access_token.go b/management/server/personal_access_token.go index c7deca9de..f46666112 100644 --- a/management/server/personal_access_token.go +++ b/management/server/personal_access_token.go @@ -26,7 +26,9 @@ const ( // PersonalAccessToken holds all information about a PAT including a hashed version of it for verification type PersonalAccessToken struct { - ID string + ID string `gorm:"primaryKey"` + // User is a reference to Account that this object belongs + UserID string `gorm:"index"` Name string HashedToken string ExpirationDate time.Time diff --git a/management/server/policy.go b/management/server/policy.go index 308a5c3c0..d470ab4bf 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -63,7 +63,10 @@ type PolicyUpdateOperation struct { // PolicyRule is the metadata of the policy type PolicyRule struct { // ID of the policy rule - ID string + ID string `gorm:"primaryKey"` + + // PolicyID is a reference to Policy that this object belongs + PolicyID string `json:"-" gorm:"index"` // Name of the rule visible in the UI Name string @@ -78,10 +81,10 @@ type PolicyRule struct { Action PolicyTrafficActionType // Destinations policy destination groups - Destinations []string + Destinations []string `gorm:"serializer:json"` // Sources policy source groups - Sources []string + Sources []string `gorm:"serializer:json"` // Bidirectional define if the rule is applicable in both directions, sources, and destinations Bidirectional bool @@ -90,7 +93,7 @@ type PolicyRule struct { Protocol PolicyRuleProtocolType // Ports or it ranges list - Ports []string + Ports []string `gorm:"serializer:json"` } // Copy returns a copy of a policy rule @@ -128,8 +131,11 @@ func (pm *PolicyRule) ToRule() *Rule { // Policy of the Rego query type Policy struct { - // ID of the policy - ID string + // ID of the policy' + ID string `gorm:"primaryKey"` + + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index"` // Name of the Policy Name string @@ -141,7 +147,7 @@ type Policy struct { Enabled bool // Rules of the policy - Rules []*PolicyRule + Rules []*PolicyRule `gorm:"foreignKey:PolicyID;references:id"` } // Copy returns a copy of the policy. @@ -201,7 +207,6 @@ type FirewallRule struct { // This function returns the list of peers and firewall rules that are applicable to a given peer. func (a *Account) getPeerConnectionResources(peerID string) ([]*Peer, []*FirewallRule) { generateResources, getAccumulatedResources := a.connResourcesGenerator() - for _, policy := range a.Policies { if !policy.Enabled { continue diff --git a/management/server/route_test.go b/management/server/route_test.go index 00ef3e93a..efd73d6c2 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1017,7 +1017,7 @@ func createRouterManager(t *testing.T) (*DefaultAccountManager, error) { func createRouterStore(t *testing.T) (Store, error) { dataDir := t.TempDir() - store, err := NewFileStore(dataDir, nil) + store, err := NewStoreFromJson(dataDir, nil) if err != nil { return nil, err } diff --git a/management/server/rule.go b/management/server/rule.go index cb85d633d..19085840c 100644 --- a/management/server/rule.go +++ b/management/server/rule.go @@ -25,6 +25,9 @@ type Rule struct { // ID of the rule ID string + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index"` + // Name of the rule visible in the UI Name string @@ -35,10 +38,10 @@ type Rule struct { Disabled bool // Source list of groups IDs of peers - Source []string + Source []string `gorm:"serializer:json"` // Destination list of groups IDs of peers - Destination []string + Destination []string `gorm:"serializer:json"` // Flow of the traffic allowed by the rule Flow TrafficFlowType diff --git a/management/server/setupkey.go b/management/server/setupkey.go index 6e626d084..a33f537a7 100644 --- a/management/server/setupkey.go +++ b/management/server/setupkey.go @@ -68,13 +68,15 @@ type SetupKeyType string // SetupKey represents a pre-authorized key used to register machines (peers) type SetupKey struct { - Id string + Id string + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index"` Key string Name string Type SetupKeyType CreatedAt time.Time ExpiresAt time.Time - UpdatedAt time.Time + UpdatedAt time.Time `gorm:"autoUpdateTime:false"` // Revoked indicates whether the key was revoked or not (we don't remove them for tracking purposes) Revoked bool // UsedTimes indicates how many times the key was used @@ -82,7 +84,7 @@ type SetupKey struct { // LastUsed last time the key was used for peer registration LastUsed time.Time // AutoGroups is a list of Group IDs that are auto assigned to a Peer when it uses this key to register - AutoGroups []string + AutoGroups []string `gorm:"serializer:json"` // UsageLimit indicates the number of times this key can be used to enroll a machine. // The value of 0 indicates the unlimited usage. UsageLimit int @@ -99,6 +101,7 @@ func (key *SetupKey) Copy() *SetupKey { } return &SetupKey{ Id: key.Id, + AccountID: key.AccountID, Key: key.Key, Name: key.Name, Type: key.Type, diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go new file mode 100644 index 000000000..dfe6c3dfa --- /dev/null +++ b/management/server/sqlite_store.go @@ -0,0 +1,457 @@ +package server + +import ( + "path/filepath" + "runtime" + "strings" + "sync" + "time" + + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/server/status" + "github.com/netbirdio/netbird/management/server/telemetry" + "github.com/netbirdio/netbird/route" + log "github.com/sirupsen/logrus" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/logger" +) + +// SqliteStore represents an account storage backed by a Sqlite DB persisted to disk +type SqliteStore struct { + db *gorm.DB + storeFile string + accountLocks sync.Map + globalAccountLock sync.Mutex + metrics telemetry.AppMetrics + installationPK int +} + +type installation struct { + ID uint `gorm:"primaryKey"` + InstallationIDValue string +} + +// NewSqliteStore restores a store from the file located in the datadir +func NewSqliteStore(dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, error) { + storeStr := "store.db?cache=shared" + if runtime.GOOS == "windows" { + // Vo avoid `The process cannot access the file because it is being used by another process` on Windows + storeStr = "store.db" + } + + file := filepath.Join(dataDir, storeStr) + db, err := gorm.Open(sqlite.Open(file), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + PrepareStmt: true, + }) + if err != nil { + return nil, err + } + + sql, err := db.DB() + if err != nil { + return nil, err + } + conns := runtime.NumCPU() + sql.SetMaxOpenConns(conns) // TODO: make it configurable + + err = db.AutoMigrate( + &SetupKey{}, &Peer{}, &User{}, &PersonalAccessToken{}, &Group{}, &Rule{}, + &Account{}, &Policy{}, &PolicyRule{}, &route.Route{}, &nbdns.NameServerGroup{}, + &installation{}, + ) + if err != nil { + return nil, err + } + + return &SqliteStore{db: db, storeFile: file, metrics: metrics, installationPK: 1}, nil +} + +// NewSqliteStoreFromFileStore restores a store from FileStore and stores SQLite DB in the file located in datadir +func NewSqliteStoreFromFileStore(filestore *FileStore, dataDir string, metrics telemetry.AppMetrics) (*SqliteStore, error) { + store, err := NewSqliteStore(dataDir, metrics) + if err != nil { + return nil, err + } + + err = store.SaveInstallationID(filestore.InstallationID) + if err != nil { + return nil, err + } + + for _, account := range filestore.GetAllAccounts() { + err := store.SaveAccount(account) + if err != nil { + return nil, err + } + } + + return store, nil +} + +// AcquireGlobalLock acquires global lock across all the accounts and returns a function that releases the lock +func (s *SqliteStore) AcquireGlobalLock() (unlock func()) { + log.Debugf("acquiring global lock") + start := time.Now() + s.globalAccountLock.Lock() + + unlock = func() { + s.globalAccountLock.Unlock() + log.Debugf("released global lock in %v", time.Since(start)) + } + + took := time.Since(start) + log.Debugf("took %v to acquire global lock", took) + if s.metrics != nil { + s.metrics.StoreMetrics().CountGlobalLockAcquisitionDuration(took) + } + + return unlock +} + +func (s *SqliteStore) AcquireAccountLock(accountID string) (unlock func()) { + log.Debugf("acquiring lock for account %s", accountID) + + start := time.Now() + value, _ := s.accountLocks.LoadOrStore(accountID, &sync.Mutex{}) + mtx := value.(*sync.Mutex) + mtx.Lock() + + unlock = func() { + mtx.Unlock() + log.Debugf("released lock for account %s in %v", accountID, time.Since(start)) + } + + return unlock +} + +func (s *SqliteStore) SaveAccount(account *Account) error { + start := time.Now() + + for _, key := range account.SetupKeys { + account.SetupKeysG = append(account.SetupKeysG, *key) + } + + for id, peer := range account.Peers { + peer.ID = id + account.PeersG = append(account.PeersG, *peer) + } + + for id, user := range account.Users { + user.Id = id + for id, pat := range user.PATs { + pat.ID = id + user.PATsG = append(user.PATsG, *pat) + } + account.UsersG = append(account.UsersG, *user) + } + + for id, group := range account.Groups { + group.ID = id + account.GroupsG = append(account.GroupsG, *group) + } + + for id, rule := range account.Rules { + rule.ID = id + account.RulesG = append(account.RulesG, *rule) + } + + for id, route := range account.Routes { + route.ID = id + account.RoutesG = append(account.RoutesG, *route) + } + + for id, ns := range account.NameServerGroups { + ns.ID = id + account.NameServerGroupsG = append(account.NameServerGroupsG, *ns) + } + + err := s.db.Transaction(func(tx *gorm.DB) error { + result := tx.Select(clause.Associations).Delete(account.Policies, "account_id = ?", account.Id) + if result.Error != nil { + return result.Error + } + + result = tx.Select(clause.Associations).Delete(account.UsersG, "account_id = ?", account.Id) + if result.Error != nil { + return result.Error + } + + result = tx.Select(clause.Associations).Delete(account) + if result.Error != nil { + return result.Error + } + + result = tx. + Session(&gorm.Session{FullSaveAssociations: true}). + Clauses(clause.OnConflict{UpdateAll: true}).Create(account) + if result.Error != nil { + return result.Error + } + return nil + }) + + took := time.Since(start) + if s.metrics != nil { + s.metrics.StoreMetrics().CountPersistenceDuration(took) + } + log.Debugf("took %d ms to persist an account to the SQLite", took.Milliseconds()) + + return err +} + +func (s *SqliteStore) SaveInstallationID(ID string) error { + installation := installation{InstallationIDValue: ID} + installation.ID = uint(s.installationPK) + + return s.db.Clauses(clause.OnConflict{UpdateAll: true}).Create(&installation).Error +} + +func (s *SqliteStore) GetInstallationID() string { + var installation installation + + if result := s.db.First(&installation, "id = ?", s.installationPK); result.Error != nil { + return "" + } + + return installation.InstallationIDValue +} + +func (s *SqliteStore) SavePeerStatus(accountID, peerID string, peerStatus PeerStatus) error { + var peer Peer + + result := s.db.First(&peer, "account_id = ? and id = ?", accountID, peerID) + if result.Error != nil { + return status.Errorf(status.NotFound, "peer %s not found", peerID) + } + + peer.Status = &peerStatus + + return s.db.Save(peer).Error +} + +// DeleteHashedPAT2TokenIDIndex is noop in Sqlite +func (s *SqliteStore) DeleteHashedPAT2TokenIDIndex(hashedToken string) error { + return nil +} + +// DeleteTokenID2UserIDIndex is noop in Sqlite +func (s *SqliteStore) DeleteTokenID2UserIDIndex(tokenID string) error { + return nil +} + +func (s *SqliteStore) GetAccountByPrivateDomain(domain string) (*Account, error) { + var account Account + + result := s.db.First(&account, "domain = ?", strings.ToLower(domain)) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found: provided domain is not registered or is not private") + } + + // TODO: rework to not call GetAccount + return s.GetAccount(account.Id) +} + +func (s *SqliteStore) GetAccountBySetupKey(setupKey string) (*Account, error) { + var key SetupKey + result := s.db.Select("account_id").First(&key, "key = ?", strings.ToUpper(setupKey)) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + if key.AccountID == "" { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + return s.GetAccount(key.AccountID) +} + +func (s *SqliteStore) GetTokenIDByHashedToken(hashedToken string) (string, error) { + var token PersonalAccessToken + result := s.db.First(&token, "hashed_token = ?", hashedToken) + if result.Error != nil { + return "", status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + return token.ID, nil +} + +func (s *SqliteStore) GetUserByTokenID(tokenID string) (*User, error) { + var token PersonalAccessToken + result := s.db.First(&token, "id = ?", tokenID) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + if token.UserID == "" { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + var user User + result = s.db.Preload("PATsG").First(&user, "id = ?", token.UserID) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + user.PATs = make(map[string]*PersonalAccessToken, len(user.PATsG)) + for _, pat := range user.PATsG { + user.PATs[pat.ID] = &pat + } + + return &user, nil +} + +func (s *SqliteStore) GetAllAccounts() (all []*Account) { + var accounts []Account + result := s.db.Find(&accounts) + if result.Error != nil { + return all + } + + for _, account := range accounts { + if acc, err := s.GetAccount(account.Id); err == nil { + all = append(all, acc) + } + } + + return all +} + +func (s *SqliteStore) GetAccount(accountID string) (*Account, error) { + var account Account + + result := s.db.Model(&account). + Preload("UsersG.PATsG"). // have to be specifies as this is nester reference + Preload(clause.Associations). + First(&account, "id = ?", accountID) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found") + } + + // we have to manually preload policy rules as it seems that gorm preloading doesn't do it for us + for i, policy := range account.Policies { + var rules []*PolicyRule + err := s.db.Model(&PolicyRule{}).Find(&rules, "policy_id = ?", policy.ID).Error + if err != nil { + return nil, status.Errorf(status.NotFound, "account not found") + } + account.Policies[i].Rules = rules + } + + account.SetupKeys = make(map[string]*SetupKey, len(account.SetupKeysG)) + for _, key := range account.SetupKeysG { + account.SetupKeys[key.Key] = key.Copy() + } + account.SetupKeysG = nil + + account.Peers = make(map[string]*Peer, len(account.PeersG)) + for _, peer := range account.PeersG { + account.Peers[peer.ID] = peer.Copy() + } + account.PeersG = nil + + account.Users = make(map[string]*User, len(account.UsersG)) + for _, user := range account.UsersG { + user.PATs = make(map[string]*PersonalAccessToken, len(user.PATs)) + for _, pat := range user.PATsG { + user.PATs[pat.ID] = pat.Copy() + } + account.Users[user.Id] = user.Copy() + } + account.UsersG = nil + + account.Groups = make(map[string]*Group, len(account.GroupsG)) + for _, group := range account.GroupsG { + account.Groups[group.ID] = group.Copy() + } + account.GroupsG = nil + + account.Rules = make(map[string]*Rule, len(account.RulesG)) + for _, rule := range account.RulesG { + account.Rules[rule.ID] = rule.Copy() + } + account.RulesG = nil + + account.Routes = make(map[string]*route.Route, len(account.RoutesG)) + for _, route := range account.RoutesG { + account.Routes[route.ID] = route.Copy() + } + account.RoutesG = nil + + account.NameServerGroups = make(map[string]*nbdns.NameServerGroup, len(account.NameServerGroupsG)) + for _, ns := range account.NameServerGroupsG { + account.NameServerGroups[ns.ID] = ns.Copy() + } + account.NameServerGroupsG = nil + + return &account, nil +} + +func (s *SqliteStore) GetAccountByUser(userID string) (*Account, error) { + var user User + result := s.db.Select("account_id").First(&user, "id = ?", userID) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + if user.AccountID == "" { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + return s.GetAccount(user.AccountID) +} + +func (s *SqliteStore) GetAccountByPeerID(peerID string) (*Account, error) { + var peer Peer + result := s.db.Select("account_id").First(&peer, "id = ?", peerID) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + if peer.AccountID == "" { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + return s.GetAccount(peer.AccountID) +} + +func (s *SqliteStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) { + var peer Peer + + result := s.db.Select("account_id").First(&peer, "key = ?", peerKey) + if result.Error != nil { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + if peer.AccountID == "" { + return nil, status.Errorf(status.NotFound, "account not found: index lookup failed") + } + + return s.GetAccount(peer.AccountID) +} + +// SaveUserLastLogin stores the last login time for a user in DB. +func (s *SqliteStore) SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error { + var peer Peer + + result := s.db.First(&peer, "account_id = ? and user_id = ?", accountID, userID) + if result.Error != nil { + return status.Errorf(status.NotFound, "user %s not found", userID) + } + + peer.LastLogin = lastLogin + + return s.db.Save(peer).Error +} + +// Close is noop in Sqlite +func (s *SqliteStore) Close() error { + return nil +} + +// GetStoreKind returns SqliteStoreKind +func (s *SqliteStore) GetStoreKind() StoreKind { + return SqliteStoreKind +} diff --git a/management/server/sqlite_store_test.go b/management/server/sqlite_store_test.go new file mode 100644 index 000000000..4a16e2525 --- /dev/null +++ b/management/server/sqlite_store_test.go @@ -0,0 +1,229 @@ +package server + +import ( + "fmt" + "net" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/google/uuid" + "github.com/netbirdio/netbird/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSqlite_NewStore(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStore(t) + + if len(store.GetAllAccounts()) != 0 { + t.Errorf("expected to create a new empty Accounts map when creating a new FileStore") + } +} + +func TestSqlite_SaveAccount(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStore(t) + + account := newAccountWithId("account_id", "testuser", "") + setupKey := GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["testpeer"] = &Peer{ + Key: "peerkey", + SetupKey: "peerkeysetupkey", + IP: net.IP{127, 0, 0, 1}, + Meta: PeerSystemMeta{}, + Name: "peer name", + Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } + + err := store.SaveAccount(account) + require.NoError(t, err) + + account2 := newAccountWithId("account_id2", "testuser2", "") + setupKey = GenerateDefaultSetupKey() + account2.SetupKeys[setupKey.Key] = setupKey + account2.Peers["testpeer2"] = &Peer{ + Key: "peerkey2", + SetupKey: "peerkeysetupkey2", + IP: net.IP{127, 0, 0, 2}, + Meta: PeerSystemMeta{}, + Name: "peer name 2", + Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } + + err = store.SaveAccount(account2) + require.NoError(t, err) + + if len(store.GetAllAccounts()) != 2 { + t.Errorf("expecting 2 Accounts to be stored after SaveAccount()") + } + + a, err := store.GetAccount(account.Id) + if a == nil { + t.Errorf("expecting Account to be stored after SaveAccount(): %v", err) + } + + if a != nil && len(a.Policies) != 1 { + t.Errorf("expecting Account to have one policy stored after SaveAccount(), got %d", len(a.Policies)) + } + + if a != nil && len(a.Policies[0].Rules) != 1 { + t.Errorf("expecting Account to have one policy rule stored after SaveAccount(), got %d", len(a.Policies[0].Rules)) + return + } + + if a, err := store.GetAccountByPeerPubKey("peerkey"); a == nil { + t.Errorf("expecting PeerKeyID2AccountID index updated after SaveAccount(): %v", err) + } + + if a, err := store.GetAccountByUser("testuser"); a == nil { + t.Errorf("expecting UserID2AccountID index updated after SaveAccount(): %v", err) + } + + if a, err := store.GetAccountByPeerID("testpeer"); a == nil { + t.Errorf("expecting PeerID2AccountID index updated after SaveAccount(): %v", err) + } + + if a, err := store.GetAccountBySetupKey(setupKey.Key); a == nil { + t.Errorf("expecting SetupKeyID2AccountID index updated after SaveAccount(): %v", err) + } +} + +func TestSqlite_SavePeerStatus(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStoreFromFile(t, "testdata/store.json") + + account, err := store.GetAccount("bf1c8084-ba50-4ce7-9439-34653001fc3b") + require.NoError(t, err) + + // save status of non-existing peer + newStatus := PeerStatus{Connected: true, LastSeen: time.Now().UTC()} + err = store.SavePeerStatus(account.Id, "non-existing-peer", newStatus) + assert.Error(t, err) + + // save new status of existing peer + account.Peers["testpeer"] = &Peer{ + Key: "peerkey", + ID: "testpeer", + SetupKey: "peerkeysetupkey", + IP: net.IP{127, 0, 0, 1}, + Meta: PeerSystemMeta{}, + Name: "peer name", + Status: &PeerStatus{Connected: false, LastSeen: time.Now().UTC()}, + } + + err = store.SaveAccount(account) + require.NoError(t, err) + + err = store.SavePeerStatus(account.Id, "testpeer", newStatus) + require.NoError(t, err) + + account, err = store.GetAccount(account.Id) + require.NoError(t, err) + + actual := account.Peers["testpeer"].Status + assert.Equal(t, newStatus, *actual) +} + +func TestSqlite_TestGetAccountByPrivateDomain(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStoreFromFile(t, "testdata/store.json") + + existingDomain := "test.com" + + account, err := store.GetAccountByPrivateDomain(existingDomain) + require.NoError(t, err, "should found account") + require.Equal(t, existingDomain, account.Domain, "domains should match") + + _, err = store.GetAccountByPrivateDomain("missing-domain.com") + require.Error(t, err, "should return error on domain lookup") +} + +func TestSqlite_GetTokenIDByHashedToken(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStoreFromFile(t, "testdata/store.json") + + hashed := "SoMeHaShEdToKeN" + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + token, err := store.GetTokenIDByHashedToken(hashed) + require.NoError(t, err) + require.Equal(t, id, token) +} + +func TestSqlite_GetUserByTokenID(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("The SQLite store is not properly supported by Windows yet") + } + + store := newSqliteStoreFromFile(t, "testdata/store.json") + + id := "9dj38s35-63fb-11ec-90d6-0242ac120003" + + user, err := store.GetUserByTokenID(id) + require.NoError(t, err) + require.Equal(t, id, user.PATs[id].ID) +} + +func newSqliteStore(t *testing.T) *SqliteStore { + t.Helper() + + store, err := NewSqliteStore(t.TempDir(), nil) + require.NoError(t, err) + require.NotNil(t, store) + + return store +} + +func newSqliteStoreFromFile(t *testing.T, filename string) *SqliteStore { + t.Helper() + + storeDir := t.TempDir() + + err := util.CopyFileContents(filename, filepath.Join(storeDir, "store.json")) + require.NoError(t, err) + + fStore, err := NewFileStore(storeDir, nil) + require.NoError(t, err) + + store, err := NewSqliteStoreFromFileStore(fStore, storeDir, nil) + require.NoError(t, err) + require.NotNil(t, store) + + return store +} + +func newAccount(store Store, id int) error { + str := fmt.Sprintf("%s-%d", uuid.New().String(), id) + account := newAccountWithId(str, str+"-testuser", "example.com") + setupKey := GenerateDefaultSetupKey() + account.SetupKeys[setupKey.Key] = setupKey + account.Peers["p"+str] = &Peer{ + Key: "peerkey" + str, + SetupKey: "peerkeysetupkey", + IP: net.IP{127, 0, 0, 1}, + Meta: PeerSystemMeta{}, + Name: "peer name", + Status: &PeerStatus{Connected: true, LastSeen: time.Now().UTC()}, + } + + return store.SaveAccount(account) +} diff --git a/management/server/store.go b/management/server/store.go index 9ebe41235..6606c91e6 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -1,6 +1,12 @@ package server -import "time" +import ( + "fmt" + "os" + "time" + + "github.com/netbirdio/netbird/management/server/telemetry" +) type Store interface { GetAllAccounts() []*Account @@ -25,4 +31,63 @@ type Store interface { SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error // Close should close the store persisting all unsaved data. Close() error + // GetStoreKind should return StoreKind of the current store implementation. + // This is also a method of metrics.DataSource interface. + GetStoreKind() StoreKind +} + +type StoreKind string + +const ( + FileStoreKind StoreKind = "JsonFile" + SqliteStoreKind StoreKind = "Sqlite" +) + +func GetStoreKindFromEnv() StoreKind { + kind, ok := os.LookupEnv("NETBIRD_STORE_KIND") + if !ok { + return FileStoreKind + } + + value := StoreKind(kind) + + if value == FileStoreKind || value == SqliteStoreKind { + return value + } + + return FileStoreKind +} + +func NewStore(kind StoreKind, dataDir string, metrics telemetry.AppMetrics) (Store, error) { + if kind == "" { + // fallback to env. Normally this only should be used from tests + kind = GetStoreKindFromEnv() + } + switch kind { + case FileStoreKind: + return NewFileStore(dataDir, metrics) + case SqliteStoreKind: + return NewSqliteStore(dataDir, metrics) + default: + return nil, fmt.Errorf("unsupported kind of store %s", kind) + } +} + +func NewStoreFromJson(dataDir string, metrics telemetry.AppMetrics) (Store, error) { + fstore, err := NewFileStore(dataDir, nil) + if err != nil { + return nil, err + } + + kind := GetStoreKindFromEnv() + + switch kind { + case FileStoreKind: + return fstore, nil + case SqliteStoreKind: + return NewSqliteStoreFromFileStore(fstore, dataDir, metrics) + default: + return nil, fmt.Errorf("unsupported kind of store %s", kind) + } + } diff --git a/management/server/store_test.go b/management/server/store_test.go new file mode 100644 index 000000000..72bbaf949 --- /dev/null +++ b/management/server/store_test.go @@ -0,0 +1,88 @@ +package server + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type benchCase struct { + name string + storeFn func(b *testing.B) Store + size int +} + +var newFs = func(b *testing.B) Store { + store, _ := NewFileStore(b.TempDir(), nil) + return store +} + +var newSqlite = func(b *testing.B) Store { + store, _ := NewSqliteStore(b.TempDir(), nil) + return store +} + +func BenchmarkTest_StoreWrite(b *testing.B) { + cases := []benchCase{ + {name: "FileStore_Write", storeFn: newFs, size: 100}, + {name: "SqliteStore_Write", storeFn: newSqlite, size: 100}, + {name: "FileStore_Write", storeFn: newFs, size: 500}, + {name: "SqliteStore_Write", storeFn: newSqlite, size: 500}, + {name: "FileStore_Write", storeFn: newFs, size: 1000}, + {name: "SqliteStore_Write", storeFn: newSqlite, size: 1000}, + {name: "FileStore_Write", storeFn: newFs, size: 2000}, + {name: "SqliteStore_Write", storeFn: newSqlite, size: 2000}, + } + + for _, c := range cases { + name := fmt.Sprintf("%s_%d", c.name, c.size) + store := c.storeFn(b) + + for i := 0; i < c.size; i++ { + _ = newAccount(store, i) + } + + b.Run(name, func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + i := c.size + for pb.Next() { + i++ + err := newAccount(store, i) + require.NoError(b, err) + } + }) + }) + } +} + +func BenchmarkTest_StoreRead(b *testing.B) { + cases := []benchCase{ + {name: "FileStore_Read", storeFn: newFs, size: 100}, + {name: "SqliteStore_Read", storeFn: newSqlite, size: 100}, + {name: "FileStore_Read", storeFn: newFs, size: 500}, + {name: "SqliteStore_Read", storeFn: newSqlite, size: 500}, + {name: "FileStore_Read", storeFn: newFs, size: 1000}, + {name: "SqliteStore_Read", storeFn: newSqlite, size: 1000}, + } + + for _, c := range cases { + name := fmt.Sprintf("%s_%d", c.name, c.size) + store := c.storeFn(b) + + for i := 0; i < c.size; i++ { + _ = newAccount(store, i) + } + + accounts := store.GetAllAccounts() + id := accounts[c.size-1].Id + + b.Run(name, func(b *testing.B) { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + _, _ = store.GetAccount(id) + } + }) + }) + } +} diff --git a/management/server/testdata/store.json b/management/server/testdata/store.json index ecde766c3..1fa4e3a9a 100644 --- a/management/server/testdata/store.json +++ b/management/server/testdata/store.json @@ -2,52 +2,87 @@ "Accounts": { "bf1c8084-ba50-4ce7-9439-34653001fc3b": { "Id": "bf1c8084-ba50-4ce7-9439-34653001fc3b", + "CreatedBy": "", "Domain": "test.com", "DomainCategory": "private", "IsDomainPrimaryAccount": true, "SetupKeys": { "A2C8E62B-38F5-4553-B31E-DD66C696CEBB": { + "Id": "", + "AccountID": "", "Key": "A2C8E62B-38F5-4553-B31E-DD66C696CEBB", "Name": "Default key", "Type": "reusable", "CreatedAt": "2021-08-19T20:46:20.005936822+02:00", "ExpiresAt": "2321-09-18T20:46:20.005936822+02:00", + "UpdatedAt": "0001-01-01T00:00:00Z", "Revoked": false, - "UsedTimes": 0 - + "UsedTimes": 0, + "LastUsed": "0001-01-01T00:00:00Z", + "AutoGroups": null, + "UsageLimit": 0, + "Ephemeral": false } }, "Network": { - "Id": "af1c8024-ha40-4ce2-9418-34653101fc3c", + "id": "af1c8024-ha40-4ce2-9418-34653101fc3c", "Net": { "IP": "100.64.0.0", "Mask": "//8AAA==" }, - "Dns": null + "Dns": "", + "Serial": 0 }, "Peers": {}, "Users": { "edafee4e-63fb-11ec-90d6-0242ac120003": { "Id": "edafee4e-63fb-11ec-90d6-0242ac120003", + "AccountID": "", "Role": "admin", - "PATs": {} + "IsServiceUser": false, + "ServiceUserName": "", + "AutoGroups": null, + "PATs": {}, + "Blocked": false, + "LastLogin": "0001-01-01T00:00:00Z" }, "f4f6d672-63fb-11ec-90d6-0242ac120003": { "Id": "f4f6d672-63fb-11ec-90d6-0242ac120003", + "AccountID": "", "Role": "user", + "IsServiceUser": false, + "ServiceUserName": "", + "AutoGroups": null, "PATs": { "9dj38s35-63fb-11ec-90d6-0242ac120003": { - "ID":"9dj38s35-63fb-11ec-90d6-0242ac120003", - "Description":"some Description", - "HashedToken":"SoMeHaShEdToKeN", - "ExpirationDate":"2023-02-27T00:00:00Z", - "CreatedBy":"user", - "CreatedAt":"2023-01-01T00:00:00Z", - "LastUsed":"2023-02-01T00:00:00Z" + "ID": "9dj38s35-63fb-11ec-90d6-0242ac120003", + "UserID": "", + "Name": "", + "HashedToken": "SoMeHaShEdToKeN", + "ExpirationDate": "2023-02-27T00:00:00Z", + "CreatedBy": "user", + "CreatedAt": "2023-01-01T00:00:00Z", + "LastUsed": "2023-02-01T00:00:00Z" } - } + }, + "Blocked": false, + "LastLogin": "0001-01-01T00:00:00Z" } + }, + "Groups": null, + "Rules": null, + "Policies": [], + "Routes": null, + "NameServerGroups": null, + "DNSSettings": null, + "Settings": { + "PeerLoginExpirationEnabled": false, + "PeerLoginExpiration": 86400000000000, + "GroupsPropagationEnabled": false, + "JWTGroupsEnabled": false, + "JWTGroupsClaimName": "" } } - } + }, + "InstallationID": "" } \ No newline at end of file diff --git a/management/server/user.go b/management/server/user.go index 3169c784f..585872080 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -44,14 +44,17 @@ type UserRole string // User represents a user of the system type User struct { - Id string + Id string `gorm:"primaryKey"` + // AccountID is a reference to Account that this object belongs + AccountID string `json:"-" gorm:"index"` Role UserRole IsServiceUser bool // ServiceUserName is only set if IsServiceUser is true ServiceUserName string // AutoGroups is a list of Group IDs to auto-assign to peers registered by this user - AutoGroups []string - PATs map[string]*PersonalAccessToken + AutoGroups []string `gorm:"serializer:json"` + PATs map[string]*PersonalAccessToken `gorm:"-"` + PATsG []PersonalAccessToken `json:"-" gorm:"foreignKey:UserID;references:id"` // Blocked indicates whether the user is blocked. Blocked users can't use the system. Blocked bool // LastLogin is the last time the user logged in to IdP @@ -124,6 +127,7 @@ func (u *User) Copy() *User { } return &User{ Id: u.Id, + AccountID: u.AccountID, Role: u.Role, AutoGroups: autoGroups, IsServiceUser: u.IsServiceUser, diff --git a/management/server/user_test.go b/management/server/user_test.go index 1565814b8..fdaffc693 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -251,6 +251,7 @@ func TestUser_Copy(t *testing.T) { // this is an imaginary case which will never be in DB this way user := User{ Id: "userId", + AccountID: "accountId", Role: "role", IsServiceUser: true, ServiceUserName: "servicename", @@ -291,6 +292,11 @@ func validateStruct(s interface{}) (err error) { field := structVal.Field(i) fieldName := structType.Field(i).Name + // skip gorm internal fields + if json, ok := structType.Field(i).Tag.Lookup("json"); ok && json == "-" { + continue + } + isSet := field.IsValid() && (!field.IsZero() || field.Type().String() == "bool") if !isSet { diff --git a/route/route.go b/route/route.go index eb7bcba2f..194e0c80d 100644 --- a/route/route.go +++ b/route/route.go @@ -65,17 +65,19 @@ func ToPrefixType(prefix string) NetworkType { // Route represents a route type Route struct { - ID string - Network netip.Prefix + ID string `gorm:"primaryKey"` + // AccountID is a reference to Account that this object belongs + AccountID string `gorm:"index"` + Network netip.Prefix `gorm:"serializer:gob"` NetID string Description string Peer string - PeerGroups []string + PeerGroups []string `gorm:"serializer:gob"` NetworkType NetworkType Masquerade bool Metric int Enabled bool - Groups []string + Groups []string `gorm:"serializer:json"` } // EventMeta returns activity event meta related to the route From 46f5f148daae0b4f261daf3728fb880bcb95e32e Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Mon, 16 Oct 2023 11:19:39 +0200 Subject: [PATCH 10/43] Move StoreKind under own StoreConfig configuration and rename to Engine (#1219) * Move StoreKind under own StoreConfig configuration parameter * Rename StoreKind option to Engine * Rename StoreKind internal methods and types to Engine * Add template engine value test --------- Co-authored-by: Maycon Santos --- .github/workflows/golang-test-darwin.yml | 4 +- .github/workflows/golang-test-linux.yml | 8 ++-- .github/workflows/golang-test-windows.yml | 2 +- .../workflows/test-infrastructure-files.yml | 5 ++- infrastructure_files/base.setup.env | 6 ++- infrastructure_files/management.json.tmpl | 3 ++ infrastructure_files/tests/setup.env | 3 +- management/cmd/management.go | 2 +- management/server/config.go | 7 +++- management/server/file_store.go | 6 +-- management/server/metrics/selfhosted.go | 4 +- management/server/metrics/selfhosted_test.go | 10 ++--- management/server/sqlite_store.go | 6 +-- management/server/store.go | 41 ++++++++++--------- 14 files changed, 62 insertions(+), 45 deletions(-) diff --git a/.github/workflows/golang-test-darwin.yml b/.github/workflows/golang-test-darwin.yml index 8cd28bcb4..5998fab01 100644 --- a/.github/workflows/golang-test-darwin.yml +++ b/.github/workflows/golang-test-darwin.yml @@ -14,7 +14,7 @@ jobs: test: strategy: matrix: - store: ['JsonFile', 'Sqlite'] + store: ['jsonfile', 'sqlite'] runs-on: macos-latest steps: - name: Install Go @@ -36,4 +36,4 @@ jobs: run: go mod tidy - name: Test - run: NETBIRD_STORE_KIND=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... + run: NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index 026779885..8015fb36a 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: arch: ['386','amd64'] - store: ['JsonFile', 'Sqlite'] + store: ['jsonfile', 'sqlite'] runs-on: ubuntu-latest steps: - name: Install Go @@ -42,7 +42,7 @@ jobs: run: go mod tidy - name: Test - run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_KIND=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./... test_client_on_docker: runs-on: ubuntu-20.04 @@ -102,10 +102,10 @@ jobs: run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1 - name: Run Engine tests in docker with file store - run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_KIND="JsonFile" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1 + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="jsonfile" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1 - name: Run Engine tests in docker with sqlite store - run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_KIND="Sqlite" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1 + run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="sqlite" --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 -test.timeout 5m -test.parallel 1 \ No newline at end of file diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index 1fc84ff2a..ec5576d88 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -16,7 +16,7 @@ jobs: test: strategy: matrix: - store: ['JsonFile', 'Sqlite'] + store: ['jsonfile', 'sqlite'] runs-on: windows-latest steps: - name: Checkout code diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index c2c4f7598..da54ceaf5 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -56,6 +56,7 @@ jobs: CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified" + CI_NETBIRD_STORE_CONFIG_ENGINE: "sqlite" - name: check values working-directory: infrastructure_files @@ -81,6 +82,7 @@ jobs: CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret CI_NETBIRD_SIGNAL_PORT: 12345 + CI_NETBIRD_STORE_CONFIG_ENGINE: "sqlite" run: | grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID @@ -97,7 +99,8 @@ jobs: grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE - grep -A 8 DeviceAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE" + grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE + grep Engine management.json | grep "$CI_NETBIRD_STORE_CONFIG_ENGINE" grep UseIDToken management.json | grep false grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index f610a9691..210b30364 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -55,6 +55,9 @@ NETBIRD_AUTH_PKCE_AUDIENCE=$NETBIRD_AUTH_AUDIENCE NETBIRD_DASH_AUTH_USE_AUDIENCE=${NETBIRD_DASH_AUTH_USE_AUDIENCE:-true} NETBIRD_DASH_AUTH_AUDIENCE=$NETBIRD_AUTH_AUDIENCE +# Store config +NETBIRD_STORE_CONFIG_ENGINE=${NETBIRD_STORE_CONFIG_ENGINE:-"jsonfile"} + # exports export NETBIRD_DOMAIN export NETBIRD_AUTH_CLIENT_ID @@ -97,4 +100,5 @@ export NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT export NETBIRD_AUTH_PKCE_USE_ID_TOKEN export NETBIRD_AUTH_PKCE_AUDIENCE export NETBIRD_DASH_AUTH_USE_AUDIENCE -export NETBIRD_DASH_AUTH_AUDIENCE \ No newline at end of file +export NETBIRD_DASH_AUTH_AUDIENCE +export NETBIRD_STORE_CONFIG_ENGINE \ No newline at end of file diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index 847ce6222..7a15bdd2c 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -28,6 +28,9 @@ }, "Datadir": "", "DataStoreEncryptionKey": "$NETBIRD_DATASTORE_ENC_KEY", + "StoreConfig": { + "Engine": "$NETBIRD_STORE_CONFIG_ENGINE" + }, "HttpConfig": { "Address": "0.0.0.0:$NETBIRD_MGMT_API_PORT", "AuthIssuer": "$NETBIRD_AUTH_AUTHORITY", diff --git a/infrastructure_files/tests/setup.env b/infrastructure_files/tests/setup.env index b0999eb51..f6e3b4a15 100644 --- a/infrastructure_files/tests/setup.env +++ b/infrastructure_files/tests/setup.env @@ -22,4 +22,5 @@ NETBIRD_AUTH_DEVICE_AUTH_SCOPE="openid email" NETBIRD_MGMT_IDP=$CI_NETBIRD_MGMT_IDP NETBIRD_IDP_MGMT_CLIENT_ID=$CI_NETBIRD_IDP_MGMT_CLIENT_ID NETBIRD_IDP_MGMT_CLIENT_SECRET=$CI_NETBIRD_IDP_MGMT_CLIENT_SECRET -NETBIRD_SIGNAL_PORT=12345 \ No newline at end of file +NETBIRD_SIGNAL_PORT=12345 +NETBIRD_STORE_CONFIG_ENGINE=$CI_NETBIRD_STORE_CONFIG_ENGINE \ No newline at end of file diff --git a/management/cmd/management.go b/management/cmd/management.go index fda16566c..9ad2b7274 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -126,7 +126,7 @@ var ( if err != nil { return err } - store, err := server.NewStore(config.StoreKind, config.Datadir, appMetrics) + store, err := server.NewStore(config.StoreConfig.Engine, config.Datadir, appMetrics) if err != nil { return fmt.Errorf("failed creating Store: %s: %v", config.Datadir, err) } diff --git a/management/server/config.go b/management/server/config.go index 19a71ff7a..4fed93bba 100644 --- a/management/server/config.go +++ b/management/server/config.go @@ -46,7 +46,7 @@ type Config struct { PKCEAuthorizationFlow *PKCEAuthorizationFlow - StoreKind StoreKind + StoreConfig StoreConfig } // GetAuthAudiences returns the audience from the http config and device authorization flow config @@ -138,6 +138,11 @@ type ProviderConfig struct { RedirectURLs []string } +// StoreConfig contains Store configuration +type StoreConfig struct { + Engine StoreEngine +} + // validateURL validates input http url func validateURL(httpURL string) bool { _, err := url.ParseRequestURI(httpURL) diff --git a/management/server/file_store.go b/management/server/file_store.go index c8d24433f..0bd137b42 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -615,7 +615,7 @@ func (s *FileStore) Close() error { return s.persist(s.storeFile) } -// GetStoreKind returns FileStoreKind -func (s *FileStore) GetStoreKind() StoreKind { - return FileStoreKind +// GetStoreEngine returns FileStoreEngine +func (s *FileStore) GetStoreEngine() StoreEngine { + return FileStoreEngine } diff --git a/management/server/metrics/selfhosted.go b/management/server/metrics/selfhosted.go index 59364b940..cf6b2e440 100644 --- a/management/server/metrics/selfhosted.go +++ b/management/server/metrics/selfhosted.go @@ -48,7 +48,7 @@ type properties map[string]interface{} // DataSource metric data source type DataSource interface { GetAllAccounts() []*server.Account - GetStoreKind() server.StoreKind + GetStoreEngine() server.StoreEngine } // ConnManager peer connection manager that holds state for current active connections @@ -296,7 +296,7 @@ func (w *Worker) generateProperties() properties { metricsProperties["max_active_peer_version"] = maxActivePeerVersion metricsProperties["ui_clients"] = uiClient metricsProperties["idp_manager"] = w.idpManager - metricsProperties["store_kind"] = w.dataSource.GetStoreKind() + metricsProperties["store_engine"] = w.dataSource.GetStoreEngine() for protocol, count := range rulesProtocol { metricsProperties["rules_protocol_"+protocol] = count diff --git a/management/server/metrics/selfhosted_test.go b/management/server/metrics/selfhosted_test.go index f69c0f8f8..7717ff409 100644 --- a/management/server/metrics/selfhosted_test.go +++ b/management/server/metrics/selfhosted_test.go @@ -151,9 +151,9 @@ func (mockDatasource) GetAllAccounts() []*server.Account { } } -// GetStoreKind returns FileStoreKind -func (mockDatasource) GetStoreKind() server.StoreKind { - return server.FileStoreKind +// GetStoreEngine returns FileStoreEngine +func (mockDatasource) GetStoreEngine() server.StoreEngine { + return server.FileStoreEngine } // TestGenerateProperties tests and validate the properties generation by using the mockDatasource for the Worker.generateProperties @@ -242,7 +242,7 @@ func TestGenerateProperties(t *testing.T) { t.Errorf("expected 2 user_peers, got %d", properties["user_peers"]) } - if properties["store_kind"] != server.FileStoreKind { - t.Errorf("expected JsonFile, got %s", properties["store_kind"]) + if properties["store_engine"] != server.FileStoreEngine { + t.Errorf("expected JsonFile, got %s", properties["store_engine"]) } } diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index dfe6c3dfa..97c759d8a 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -451,7 +451,7 @@ func (s *SqliteStore) Close() error { return nil } -// GetStoreKind returns SqliteStoreKind -func (s *SqliteStore) GetStoreKind() StoreKind { - return SqliteStoreKind +// GetStoreEngine returns SqliteStoreEngine +func (s *SqliteStore) GetStoreEngine() StoreEngine { + return SqliteStoreEngine } diff --git a/management/server/store.go b/management/server/store.go index 6606c91e6..458912e97 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -3,6 +3,7 @@ package server import ( "fmt" "os" + "strings" "time" "github.com/netbirdio/netbird/management/server/telemetry" @@ -31,42 +32,43 @@ type Store interface { SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error // Close should close the store persisting all unsaved data. Close() error - // GetStoreKind should return StoreKind of the current store implementation. + // GetStoreEngine should return StoreEngine of the current store implementation. // This is also a method of metrics.DataSource interface. - GetStoreKind() StoreKind + GetStoreEngine() StoreEngine } -type StoreKind string +type StoreEngine string const ( - FileStoreKind StoreKind = "JsonFile" - SqliteStoreKind StoreKind = "Sqlite" + FileStoreEngine StoreEngine = "jsonfile" + SqliteStoreEngine StoreEngine = "sqlite" ) -func GetStoreKindFromEnv() StoreKind { - kind, ok := os.LookupEnv("NETBIRD_STORE_KIND") +func getStoreEngineFromEnv() StoreEngine { + // NETBIRD_STORE_ENGINE supposed to be used in tests. Otherwise rely on the config file. + kind, ok := os.LookupEnv("NETBIRD_STORE_ENGINE") if !ok { - return FileStoreKind + return FileStoreEngine } - value := StoreKind(kind) + value := StoreEngine(strings.ToLower(kind)) - if value == FileStoreKind || value == SqliteStoreKind { + if value == FileStoreEngine || value == SqliteStoreEngine { return value } - return FileStoreKind + return FileStoreEngine } -func NewStore(kind StoreKind, dataDir string, metrics telemetry.AppMetrics) (Store, error) { +func NewStore(kind StoreEngine, dataDir string, metrics telemetry.AppMetrics) (Store, error) { if kind == "" { // fallback to env. Normally this only should be used from tests - kind = GetStoreKindFromEnv() + kind = getStoreEngineFromEnv() } switch kind { - case FileStoreKind: + case FileStoreEngine: return NewFileStore(dataDir, metrics) - case SqliteStoreKind: + case SqliteStoreEngine: return NewSqliteStore(dataDir, metrics) default: return nil, fmt.Errorf("unsupported kind of store %s", kind) @@ -79,15 +81,14 @@ func NewStoreFromJson(dataDir string, metrics telemetry.AppMetrics) (Store, erro return nil, err } - kind := GetStoreKindFromEnv() + kind := getStoreEngineFromEnv() switch kind { - case FileStoreKind: + case FileStoreEngine: return fstore, nil - case SqliteStoreKind: + case SqliteStoreEngine: return NewSqliteStoreFromFileStore(fstore, dataDir, metrics) default: - return nil, fmt.Errorf("unsupported kind of store %s", kind) + return nil, fmt.Errorf("unsupported store engine %s", kind) } - } From 73e57f17ea301210bb82610e8e00ff4cfe25172a Mon Sep 17 00:00:00 2001 From: guangwu Date: Mon, 16 Oct 2023 23:00:05 +0800 Subject: [PATCH 11/43] chore: pkg import only once (#1222) Signed-off-by: guoguangwu --- management/client/client_test.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/management/client/client_test.go b/management/client/client_test.go index b66dacc73..889b7a131 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -16,7 +16,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/netbirdio/netbird/encryption" - "github.com/netbirdio/netbird/management/proto" mgmtProto "github.com/netbirdio/netbird/management/proto" mgmt "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/mock_server" @@ -95,8 +94,8 @@ func startMockManagement(t *testing.T) (*grpc.Server, net.Listener, *mock_server } mgmtMockServer := &mock_server.ManagementServiceServerMock{ - GetServerKeyFunc: func(context.Context, *proto.Empty) (*proto.ServerKeyResponse, error) { - response := &proto.ServerKeyResponse{ + GetServerKeyFunc: func(context.Context, *mgmtProto.Empty) (*mgmtProto.ServerKeyResponse, error) { + response := &mgmtProto.ServerKeyResponse{ Key: serverKey.PublicKey().String(), } return response, nil @@ -300,19 +299,19 @@ func Test_SystemMetaDataFromClient(t *testing.T) { log.Fatalf("error while getting server public key from testclient, %v", err) } - var actualMeta *proto.PeerSystemMeta + var actualMeta *mgmtProto.PeerSystemMeta var actualValidKey string var wg sync.WaitGroup wg.Add(1) - mgmtMockServer.LoginFunc = func(ctx context.Context, msg *proto.EncryptedMessage) (*proto.EncryptedMessage, error) { + mgmtMockServer.LoginFunc = func(ctx context.Context, msg *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) { peerKey, err := wgtypes.ParseKey(msg.GetWgPubKey()) if err != nil { log.Warnf("error while parsing peer's Wireguard public key %s on Sync request.", msg.WgPubKey) return nil, status.Errorf(codes.InvalidArgument, "provided wgPubKey %s is invalid", msg.WgPubKey) } - loginReq := &proto.LoginRequest{} + loginReq := &mgmtProto.LoginRequest{} err = encryption.DecryptMessage(peerKey, serverKey, msg.Body, loginReq) if err != nil { log.Fatal(err) @@ -322,7 +321,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) { actualValidKey = loginReq.GetSetupKey() wg.Done() - loginResp := &proto.LoginResponse{} + loginResp := &mgmtProto.LoginResponse{} encryptedResp, err := encryption.EncryptMessage(peerKey, serverKey, loginResp) if err != nil { return nil, err @@ -343,7 +342,7 @@ func Test_SystemMetaDataFromClient(t *testing.T) { wg.Wait() - expectedMeta := &proto.PeerSystemMeta{ + expectedMeta := &mgmtProto.PeerSystemMeta{ Hostname: info.Hostname, GoOS: info.GoOS, Kernel: info.Kernel, @@ -374,12 +373,12 @@ func Test_GetDeviceAuthorizationFlow(t *testing.T) { log.Fatalf("error while creating testClient: %v", err) } - expectedFlowInfo := &proto.DeviceAuthorizationFlow{ + expectedFlowInfo := &mgmtProto.DeviceAuthorizationFlow{ Provider: 0, - ProviderConfig: &proto.ProviderConfig{ClientID: "client"}, + ProviderConfig: &mgmtProto.ProviderConfig{ClientID: "client"}, } - mgmtMockServer.GetDeviceAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*proto.EncryptedMessage, error) { + mgmtMockServer.GetDeviceAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) { encryptedResp, err := encryption.EncryptMessage(serverKey, client.key, expectedFlowInfo) if err != nil { return nil, err @@ -418,14 +417,14 @@ func Test_GetPKCEAuthorizationFlow(t *testing.T) { log.Fatalf("error while creating testClient: %v", err) } - expectedFlowInfo := &proto.PKCEAuthorizationFlow{ - ProviderConfig: &proto.ProviderConfig{ + expectedFlowInfo := &mgmtProto.PKCEAuthorizationFlow{ + ProviderConfig: &mgmtProto.ProviderConfig{ ClientID: "client", ClientSecret: "secret", }, } - mgmtMockServer.GetPKCEAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*proto.EncryptedMessage, error) { + mgmtMockServer.GetPKCEAuthorizationFlowFunc = func(ctx context.Context, req *mgmtProto.EncryptedMessage) (*mgmtProto.EncryptedMessage, error) { encryptedResp, err := encryption.EncryptMessage(serverKey, client.key, expectedFlowInfo) if err != nil { return nil, err From e4de1d75de50991f662f94fb66cd65820ca95e94 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 17 Oct 2023 11:37:58 +0200 Subject: [PATCH 12/43] Update contribution guide with go version and Windows driver (#1226) --- CONTRIBUTING.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 298251108..0d3c73633 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,6 @@ If you haven't already, join our slack workspace [here](https://join.slack.com/t - [Test suite](#test-suite) - [Checklist before submitting a PR](#checklist-before-submitting-a-pr) - [Other project repositories](#other-project-repositories) - - [Checklist before submitting a new node](#checklist-before-submitting-a-new-node) - [Contributor License Agreement](#contributor-license-agreement) ## Code of conduct @@ -70,7 +69,7 @@ dependencies are installed. Here is a short guide on how that can be done. ### Requirements -#### Go 1.19 +#### Go 1.21 Follow the installation guide from https://go.dev/ @@ -139,15 +138,14 @@ checked out and set up: ### Build and start #### Client -> Windows clients have a Wireguard driver requirement. We provide a bash script that can be executed in WLS 2 with docker support [wireguard_nt.sh](/client/wireguard_nt.sh). - To start NetBird, execute: ``` cd client -# bash wireguard_nt.sh # if windows go build . ``` +> Windows clients have a Wireguard driver requirement. You can downlowd the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to the same path as your binary file or to `C:\Windows\System32\wintun.dll`. + To start NetBird the client in the foreground: ``` From 7d8a69cc0c82580f652051984df8f0101cabf06b Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 17 Oct 2023 15:54:50 +0200 Subject: [PATCH 13/43] Use account creator as inviter as a fallback (#1225) When inviting a user using a service user PAT, we need to fall back to a known ID to get the user's email, which is used in the invite message. --- management/server/user.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/management/server/user.go b/management/server/user.go index 585872080..edb649340 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -228,10 +228,20 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite return nil, status.Errorf(status.NotFound, "account %s doesn't exist", accountID) } - // initiator is the one who is inviting the new user - initiatorUser, err := am.lookupUserInCache(userID, account) + initiatorUser, err := account.FindUser(userID) if err != nil { - return nil, status.Errorf(status.NotFound, "user %s doesn't exist in IdP", userID) + return nil, status.Errorf(status.NotFound, "initiator user with ID %s doesn't exist", userID) + } + + inviterID := userID + if initiatorUser.IsServiceUser { + inviterID = account.CreatedBy + } + + // inviterUser is the one who is inviting the new user + inviterUser, err := am.lookupUserInCache(inviterID, account) + if err != nil || inviterUser == nil { + return nil, status.Errorf(status.NotFound, "inviter user with ID %s doesn't exist in IdP", inviterID) } // check if the user is already registered with this email => reject @@ -253,7 +263,7 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite return nil, status.Errorf(status.UserAlreadyExists, "can't invite a user with an existing NetBird account") } - idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID, initiatorUser.Email) + idpUser, err := am.idpManager.CreateUser(invite.Email, invite.Name, accountID, inviterUser.Email) if err != nil { return nil, err } From 87cc53b74356ce63999cc93c265d98de429c5f66 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 17 Oct 2023 17:19:47 +0200 Subject: [PATCH 14/43] Add management-integrations (#1227) --- go.mod | 1 + go.sum | 2 + management/server/http/handler.go | 6 ++- .../server/http/middleware/auth_middleware.go | 46 ++++++++----------- .../http/middleware/auth_middleware_test.go | 7 ++- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 1f8eec24e..76e592f73 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/miekg/dns v1.1.43 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 + github.com/netbirdio/management-integrations/integrations v0.0.0-20231017101406-322cbabed3da github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pion/logging v0.2.2 diff --git a/go.sum b/go.sum index 15e69283c..561d3e17e 100644 --- a/go.sum +++ b/go.sum @@ -495,6 +495,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc= github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ= +github.com/netbirdio/management-integrations/integrations v0.0.0-20231017101406-322cbabed3da h1:S1RoPhLTw3+IhHGnyfcQlj4aqIIaQdVd3SqaiK+MYFY= +github.com/netbirdio/management-integrations/integrations v0.0.0-20231017101406-322cbabed3da/go.mod h1:KSqjzHcqlodTWiuap5lRXxt5KT3vtYRoksL0KIrTK40= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0 h1:hirFRfx3grVA/9eEyjME5/z3nxdJlN9kfQpvWWPk32g= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c h1:wK/s4nyZj/GF/kFJQjX6nqNfE0G3gcqd6hhnPCyp4sw= diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 6e9b029c7..0d415a087 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/mux" "github.com/rs/cors" + "github.com/netbirdio/management-integrations/integrations" s "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/http/middleware" "github.com/netbirdio/netbird/management/server/jwtclaims" @@ -58,6 +59,7 @@ func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValid AuthCfg: authCfg, } + integrations.RegisterHandlers(api.Router, accountManager) api.addAccountsEndpoint() api.addPeersEndpoint() api.addUsersEndpoint() @@ -73,8 +75,8 @@ func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValid err := api.Router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error { methods, err := route.GetMethods() - if err != nil { - return err + if err != nil { // we may have wildcard routes from integrations without methods, skip them for now + methods = []string{} } for _, method := range methods { template, err := route.GetPathTemplate() diff --git a/management/server/http/middleware/auth_middleware.go b/management/server/http/middleware/auth_middleware.go index 710723124..99482bfb7 100644 --- a/management/server/http/middleware/auth_middleware.go +++ b/management/server/http/middleware/auth_middleware.go @@ -57,10 +57,17 @@ func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParse func (m *AuthMiddleware) Handler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth := strings.Split(r.Header.Get("Authorization"), " ") - authType := auth[0] - switch strings.ToLower(authType) { + authType := strings.ToLower(auth[0]) + + // fallback to token when receive pat as bearer + if len(auth) >= 2 && authType == "bearer" && strings.HasPrefix(auth[1], "nbp_") { + authType = "token" + auth[0] = authType + } + + switch authType { case "bearer": - err := m.CheckJWTFromRequest(w, r) + err := m.checkJWTFromRequest(w, r, auth) if err != nil { log.Errorf("Error when validating JWT claims: %s", err.Error()) util.WriteError(status.Errorf(status.Unauthorized, "token invalid"), w) @@ -68,7 +75,7 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler { } h.ServeHTTP(w, r) case "token": - err := m.CheckPATFromRequest(w, r) + err := m.checkPATFromRequest(w, r, auth) if err != nil { log.Debugf("Error when validating PAT claims: %s", err.Error()) util.WriteError(status.Errorf(status.Unauthorized, "token invalid"), w) @@ -83,9 +90,8 @@ func (m *AuthMiddleware) Handler(h http.Handler) http.Handler { } // CheckJWTFromRequest checks if the JWT is valid -func (m *AuthMiddleware) CheckJWTFromRequest(w http.ResponseWriter, r *http.Request) error { - - token, err := getTokenFromJWTRequest(r) +func (m *AuthMiddleware) checkJWTFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error { + token, err := getTokenFromJWTRequest(auth) // If an error occurs, call the error handler and return an error if err != nil { @@ -110,8 +116,8 @@ func (m *AuthMiddleware) CheckJWTFromRequest(w http.ResponseWriter, r *http.Requ } // CheckPATFromRequest checks if the PAT is valid -func (m *AuthMiddleware) CheckPATFromRequest(w http.ResponseWriter, r *http.Request) error { - token, err := getTokenFromPATRequest(r) +func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error { + token, err := getTokenFromPATRequest(auth) // If an error occurs, call the error handler and return an error if err != nil { @@ -143,16 +149,9 @@ func (m *AuthMiddleware) CheckPATFromRequest(w http.ResponseWriter, r *http.Requ return nil } -// getTokenFromJWTRequest is a "TokenExtractor" that takes a give request and extracts +// getTokenFromJWTRequest is a "TokenExtractor" that takes auth header parts and extracts // the JWT token from the Authorization header. -func getTokenFromJWTRequest(r *http.Request) (string, error) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - return "", nil // No error, just no token - } - - // TODO: Make this a bit more robust, parsing-wise - authHeaderParts := strings.Fields(authHeader) +func getTokenFromJWTRequest(authHeaderParts []string) (string, error) { if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { return "", errors.New("Authorization header format must be Bearer {token}") } @@ -160,16 +159,9 @@ func getTokenFromJWTRequest(r *http.Request) (string, error) { return authHeaderParts[1], nil } -// getTokenFromPATRequest is a "TokenExtractor" that takes a give request and extracts +// getTokenFromPATRequest is a "TokenExtractor" that takes auth header parts and extracts // the PAT token from the Authorization header. -func getTokenFromPATRequest(r *http.Request) (string, error) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - return "", nil // No error, just no token - } - - // TODO: Make this a bit more robust, parsing-wise - authHeaderParts := strings.Fields(authHeader) +func getTokenFromPATRequest(authHeaderParts []string) (string, error) { if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "token" { return "", errors.New("Authorization header format must be Token {token}") } diff --git a/management/server/http/middleware/auth_middleware_test.go b/management/server/http/middleware/auth_middleware_test.go index 608bf42fa..55e5de260 100644 --- a/management/server/http/middleware/auth_middleware_test.go +++ b/management/server/http/middleware/auth_middleware_test.go @@ -19,7 +19,7 @@ const ( domain = "domain" userID = "userID" tokenID = "tokenID" - PAT = "PAT" + PAT = "nbp_PAT" JWT = "JWT" wrongToken = "wrongToken" ) @@ -82,6 +82,11 @@ func TestAuthMiddleware_Handler(t *testing.T) { authHeader: "Token " + wrongToken, expectedStatusCode: 401, }, + { + name: "Fallback to PAT Token", + authHeader: "Bearer " + PAT, + expectedStatusCode: 200, + }, { name: "Valid JWT Token", authHeader: "Bearer " + JWT, From f2fc0df104d672d2cc03b804affee7c7439a2383 Mon Sep 17 00:00:00 2001 From: Fabio Fantoni Date: Wed, 18 Oct 2023 18:03:51 +0200 Subject: [PATCH 15/43] Make possible set IdpSignKeyRefreshEnabled from setup.env (#1230) * Make possible set IdpSignKeyRefreshEnabled from setup.env IdpSignKeyRefreshEnabled is default to false but with some idps on token expire of logged users netbird always give error and return usable only on server restart so I think is useful make easier/faster set it on server configuration * add template IdpSignKeyRefreshEnabled value test --- .github/workflows/test-infrastructure-files.yml | 3 +++ infrastructure_files/base.setup.env | 2 ++ infrastructure_files/management.json.tmpl | 1 + infrastructure_files/setup.env.example | 2 ++ infrastructure_files/tests/setup.env | 3 ++- 5 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index da54ceaf5..ce6f0b75a 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -57,6 +57,7 @@ jobs: CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified" CI_NETBIRD_STORE_CONFIG_ENGINE: "sqlite" + CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false - name: check values working-directory: infrastructure_files @@ -83,6 +84,7 @@ jobs: CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret CI_NETBIRD_SIGNAL_PORT: 12345 CI_NETBIRD_STORE_CONFIG_ENGINE: "sqlite" + CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false run: | grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID @@ -101,6 +103,7 @@ jobs: grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE grep Engine management.json | grep "$CI_NETBIRD_STORE_CONFIG_ENGINE" + grep IdpSignKeyRefreshEnabled management.json | grep "$CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH" grep UseIDToken management.json | grep false grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index 210b30364..fa337c55d 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -14,6 +14,7 @@ NETBIRD_MGMT_API_CERT_KEY_FILE="/etc/letsencrypt/live/$NETBIRD_LETSENCRYPT_DOMAI # By default Management single account mode is enabled and domain set to $NETBIRD_DOMAIN, you may want to set this to your user's email domain NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN=$NETBIRD_DOMAIN NETBIRD_MGMT_DNS_DOMAIN=${NETBIRD_MGMT_DNS_DOMAIN:-netbird.selfhosted} +NETBIRD_MGMT_IDP_SIGNKEY_REFRESH=${NETBIRD_MGMT_IDP_SIGNKEY_REFRESH:-false} # Signal NETBIRD_SIGNAL_PROTOCOL="http" @@ -89,6 +90,7 @@ export LETSENCRYPT_VOLUMESUFFIX export NETBIRD_DISABLE_ANONYMOUS_METRICS export NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN export NETBIRD_MGMT_DNS_DOMAIN +export NETBIRD_MGMT_IDP_SIGNKEY_REFRESH export NETBIRD_SIGNAL_PROTOCOL export NETBIRD_SIGNAL_PORT export NETBIRD_AUTH_USER_ID_CLAIM diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index 7a15bdd2c..7b8d6190d 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -39,6 +39,7 @@ "AuthUserIDClaim": "$NETBIRD_AUTH_USER_ID_CLAIM", "CertFile":"$NETBIRD_MGMT_API_CERT_FILE", "CertKey":"$NETBIRD_MGMT_API_CERT_KEY_FILE", + "IdpSignKeyRefreshEnabled": $NETBIRD_MGMT_IDP_SIGNKEY_REFRESH, "OIDCConfigEndpoint":"$NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT" }, "IdpManagerConfig": { diff --git a/infrastructure_files/setup.env.example b/infrastructure_files/setup.env.example index f9ad63846..00c0c07f9 100644 --- a/infrastructure_files/setup.env.example +++ b/infrastructure_files/setup.env.example @@ -53,6 +53,8 @@ NETBIRD_MGMT_IDP="none" # Some IDPs requires different client id and client secret for management api NETBIRD_IDP_MGMT_CLIENT_ID=$NETBIRD_AUTH_CLIENT_ID NETBIRD_IDP_MGMT_CLIENT_SECRET="" +# With some IDPs may be needed enabling automatic refresh of signing keys on expire +# NETBIRD_MGMT_IDP_SIGNKEY_REFRESH=false # NETBIRD_IDP_MGMT_EXTRA_ variables. See https://docs.netbird.io/selfhosted/identity-providers for more information about your IDP of choice. # ------------------------------------------- # Letsencrypt diff --git a/infrastructure_files/tests/setup.env b/infrastructure_files/tests/setup.env index f6e3b4a15..f02ef3d14 100644 --- a/infrastructure_files/tests/setup.env +++ b/infrastructure_files/tests/setup.env @@ -23,4 +23,5 @@ NETBIRD_MGMT_IDP=$CI_NETBIRD_MGMT_IDP NETBIRD_IDP_MGMT_CLIENT_ID=$CI_NETBIRD_IDP_MGMT_CLIENT_ID NETBIRD_IDP_MGMT_CLIENT_SECRET=$CI_NETBIRD_IDP_MGMT_CLIENT_SECRET NETBIRD_SIGNAL_PORT=12345 -NETBIRD_STORE_CONFIG_ENGINE=$CI_NETBIRD_STORE_CONFIG_ENGINE \ No newline at end of file +NETBIRD_STORE_CONFIG_ENGINE=$CI_NETBIRD_STORE_CONFIG_ENGINE +NETBIRD_MGMT_IDP_SIGNKEY_REFRESH=$CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH \ No newline at end of file From c979a4e9fb4cc50a5a3c745830842c35674a06f0 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 18 Oct 2023 18:15:18 +0200 Subject: [PATCH 16/43] Explicitly disable CGO for client (#1228) --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0d3c73633..6d31de651 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,7 +141,7 @@ checked out and set up: To start NetBird, execute: ``` cd client -go build . +CGO_ENABLED=0 go build . ``` > Windows clients have a Wireguard driver requirement. You can downlowd the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to the same path as your binary file or to `C:\Windows\System32\wintun.dll`. @@ -213,4 +213,4 @@ NetBird project is composed of 3 main repositories: That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button. -A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in. \ No newline at end of file +A bot will automatically comment on the pull request once it got opened asking for the agreement to be signed. Before it did not get signed it is sadly not possible to merge it in. From a9f5fad6255c3ddb490dae6226147e618350e9f6 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 19 Oct 2023 10:18:16 +0200 Subject: [PATCH 17/43] Update grpc clients' keepalive interval (#1231) Some reverse proxies might find 15s interval too short and respond with an enhance your-calm message This change is setting the management and signal clients' keepalive interval to 30 seconds to minimize the number of reconnections --- management/client/grpc.go | 2 +- signal/client/grpc.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/management/client/grpc.go b/management/client/grpc.go index e4caed4b0..ddb420ee2 100644 --- a/management/client/grpc.go +++ b/management/client/grpc.go @@ -57,7 +57,7 @@ func NewClient(ctx context.Context, addr string, ourPrivateKey wgtypes.Key, tlsE transportOption, grpc.WithBlock(), grpc.WithKeepaliveParams(keepalive.ClientParameters{ - Time: 15 * time.Second, + Time: 30 * time.Second, Timeout: 10 * time.Second, })) if err != nil { diff --git a/signal/client/grpc.go b/signal/client/grpc.go index 08430e8ef..fef443173 100644 --- a/signal/client/grpc.go +++ b/signal/client/grpc.go @@ -79,7 +79,7 @@ func NewClient(ctx context.Context, addr string, key wgtypes.Key, tlsEnabled boo transportOption, grpc.WithBlock(), grpc.WithKeepaliveParams(keepalive.ClientParameters{ - Time: 15 * time.Second, + Time: 30 * time.Second, Timeout: 10 * time.Second, })) From ee6be58a675742cfda13f6fa32cbbd8c6f14f4c0 Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 19 Oct 2023 18:47:39 +0300 Subject: [PATCH 18/43] Fix update script's failure to update netbird-ui in binary installation (#1218) Resolve the problem with the update script that prevents netbird-ui from updating during binary installation. Introduce the variable UPDATE_NETBIRD. Now we can upgrade the binary installation with A function stop_running_netbird_ui has been added which checks if NetBird UI is currently running. If so, it stops the UI to allow the application update process to proceed smoothly. This was necessary to prevent conflicts or errors during updates if the UI was running. --------- Co-authored-by: Maycon Santos --- .github/workflows/golang-test-windows.yml | 7 +-- release_files/install.sh | 52 ++++++++++++++++------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/.github/workflows/golang-test-windows.yml b/.github/workflows/golang-test-windows.yml index ec5576d88..34f0ec680 100644 --- a/.github/workflows/golang-test-windows.yml +++ b/.github/workflows/golang-test-windows.yml @@ -14,9 +14,6 @@ concurrency: jobs: test: - strategy: - matrix: - store: ['jsonfile', 'sqlite'] runs-on: windows-latest steps: - name: Checkout code @@ -42,9 +39,9 @@ jobs: - run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\' - - run: choco install -y sysinternals + - run: choco install -y sysinternals --ignore-checksums - run: choco install -y mingw - + - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=C:\Users\runneradmin\go\pkg\mod - run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=C:\Users\runneradmin\AppData\Local\go-build diff --git a/release_files/install.sh b/release_files/install.sh index c553cc28a..e529c229e 100755 --- a/release_files/install.sh +++ b/release_files/install.sh @@ -66,9 +66,14 @@ download_release_binary() { if [ "$OS_TYPE" = "darwin" ] && [ "$1" = "$UI_APP" ]; then INSTALL_DIR="/Applications/NetBird UI.app" + if test -d "$INSTALL_DIR" ; then + echo "removing $INSTALL_DIR" + rm -rfv "$INSTALL_DIR" + fi + # Unzip the app and move to INSTALL_DIR unzip -q -o "$BINARY_NAME" - mv "netbird_ui_${OS_TYPE}_${ARCH}" "$INSTALL_DIR" + mv "netbird_ui_${OS_TYPE}_${ARCH}/" "$INSTALL_DIR/" else ${SUDO} mkdir -p "$INSTALL_DIR" tar -xzvf "$BINARY_NAME" @@ -184,16 +189,6 @@ install_netbird() { fi fi - # Checks if SKIP_UI_APP env is set - if [ -z "$SKIP_UI_APP" ]; then - SKIP_UI_APP=false - else - if $SKIP_UI_APP; then - echo "SKIP_UI_APP has been set to true in the environment" - echo "NetBird UI installation will be omitted based on your preference" - fi - fi - # Run the installation, if a desktop environment is not detected # only the CLI will be installed case "$PACKAGE_MANAGER" in @@ -294,6 +289,14 @@ is_bin_package_manager() { fi } +stop_running_netbird_ui() { + NB_UI_PROC=$(ps -ef | grep "[n]etbird-ui" | awk '{print $2}') + if [ -n "$NB_UI_PROC" ]; then + echo "NetBird UI is running with PID $NB_UI_PROC. Stopping it..." + kill -9 "$NB_UI_PROC" + fi +} + update_netbird() { if is_bin_package_manager "$CONFIG_FILE"; then latest_release=$(get_latest_release) @@ -301,7 +304,7 @@ update_netbird() { installed_version=$(netbird version) if [ "$latest_version" = "$installed_version" ]; then - echo "Installed netbird version ($installed_version) is up-to-date" + echo "Installed NetBird version ($installed_version) is up-to-date" exit 0 fi @@ -310,8 +313,9 @@ update_netbird() { echo "" echo "Initiating NetBird update. This will stop the netbird service and restart it after the update" - ${SUDO} netbird service stop - ${SUDO} netbird service uninstall + ${SUDO} netbird service stop || true + ${SUDO} netbird service uninstall || true + stop_running_netbird_ui install_native_binaries ${SUDO} netbird service install @@ -322,6 +326,16 @@ update_netbird() { fi } +# Checks if SKIP_UI_APP env is set +if [ -z "$SKIP_UI_APP" ]; then + SKIP_UI_APP=false +else + if $SKIP_UI_APP; then + echo "SKIP_UI_APP has been set to true in the environment" + echo "NetBird UI installation will be omitted based on your preference" + fi +fi + # Identify OS name and default package manager if type uname >/dev/null 2>&1; then case "$(uname)" in @@ -334,7 +348,7 @@ if type uname >/dev/null 2>&1; then if [ "$ARCH" != "amd64" ] && [ "$ARCH" != "arm64" ] \ && [ "$ARCH" != "x86_64" ];then SKIP_UI_APP=true - echo "NetBird UI installation will be omitted as $ARCH is not a compactible architecture" + echo "NetBird UI installation will be omitted as $ARCH is not a compatible architecture" fi # Allow netbird UI installation for linux running desktop enviroment @@ -376,7 +390,13 @@ if type uname >/dev/null 2>&1; then esac fi -case "$1" in +UPDATE_FLAG=$1 + +if [ "${UPDATE_NETBIRD}-x" = "true-x" ]; then + UPDATE_FLAG="--update" +fi + +case "$UPDATE_FLAG" in --update) update_netbird ;; From e59d2317fee1bdf4479a1f3b85b18dbadc50bd48 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 19 Oct 2023 19:32:42 +0200 Subject: [PATCH 19/43] Add search domains support (#1224) Supporting search domains will allow users to define match domains to also be added to a list of search domains in their systems Fix Windows registry key configuration for search domains using a key within the netbird interface path --- client/internal/dns/host.go | 2 +- client/internal/dns/host_windows.go | 59 +------ client/internal/engine.go | 5 +- dns/nameserver.go | 20 ++- management/proto/management.pb.go | 149 ++++++++++-------- management/proto/management.proto | 1 + management/server/account.go | 2 +- management/server/dns.go | 5 +- management/server/http/api/openapi.yml | 7 +- management/server/http/api/types.gen.go | 12 +- management/server/http/nameservers_handler.go | 36 +++-- .../server/http/nameservers_handler_test.go | 19 +-- management/server/mock_server/account_mock.go | 6 +- management/server/nameserver.go | 29 ++-- management/server/nameserver_test.go | 16 +- 15 files changed, 179 insertions(+), 189 deletions(-) diff --git a/client/internal/dns/host.go b/client/internal/dns/host.go index 743ececdc..4fd164c45 100644 --- a/client/internal/dns/host.go +++ b/client/internal/dns/host.go @@ -78,7 +78,7 @@ func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) hostD for _, domain := range nsConfig.Domains { config.domains = append(config.domains, domainConfig{ domain: strings.TrimSuffix(domain, "."), - matchOnly: true, + matchOnly: !nsConfig.SearchDomainsEnabled, }) } } diff --git a/client/internal/dns/host_windows.go b/client/internal/dns/host_windows.go index cea806bd2..3814be00b 100644 --- a/client/internal/dns/host_windows.go +++ b/client/internal/dns/host_windows.go @@ -22,13 +22,11 @@ const ( interfaceConfigPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces" interfaceConfigNameServerKey = "NameServer" interfaceConfigSearchListKey = "SearchList" - tcpipParametersPath = "SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters" ) type registryConfigurator struct { - guid string - routingAll bool - existingSearchDomains []string + guid string + routingAll bool } func newHostManager(wgInterface WGIface) (hostManager, error) { @@ -148,30 +146,11 @@ func (r *registryConfigurator) restoreHostDNS() error { log.Error(err) } - return r.updateSearchDomains([]string{}) + return r.deleteInterfaceRegistryKeyProperty(interfaceConfigSearchListKey) } func (r *registryConfigurator) updateSearchDomains(domains []string) error { - value, err := getLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey) - if err != nil { - return fmt.Errorf("unable to get current search domains failed with error: %s", err) - } - - valueList := strings.Split(value, ",") - setExisting := false - if len(r.existingSearchDomains) == 0 { - r.existingSearchDomains = valueList - setExisting = true - } - - if len(domains) == 0 && setExisting { - log.Infof("added %d search domains to the registry. Domain list: %s", len(domains), domains) - return nil - } - - newList := append(r.existingSearchDomains, domains...) - - err = setLocalMachineRegistryKeyStringValue(tcpipParametersPath, interfaceConfigSearchListKey, strings.Join(newList, ",")) + err := r.setInterfaceRegistryKeyStringValue(interfaceConfigSearchListKey, strings.Join(domains, ",")) if err != nil { return fmt.Errorf("adding search domain failed with error: %s", err) } @@ -235,33 +214,3 @@ func removeRegistryKeyFromDNSPolicyConfig(regKeyPath string) error { } return nil } - -func getLocalMachineRegistryKeyStringValue(keyPath, key string) (string, error) { - regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.QUERY_VALUE) - if err != nil { - return "", fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err) - } - defer regKey.Close() - - val, _, err := regKey.GetStringValue(key) - if err != nil { - return "", fmt.Errorf("getting %s value for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, keyPath, err) - } - - return val, nil -} - -func setLocalMachineRegistryKeyStringValue(keyPath, key, value string) error { - regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, keyPath, registry.SET_VALUE) - if err != nil { - return fmt.Errorf("unable to open existing key from registry, key path: HKEY_LOCAL_MACHINE\\%s, error: %s", keyPath, err) - } - defer regKey.Close() - - err = regKey.SetStringValue(key, value) - if err != nil { - return fmt.Errorf("setting %s value %s for key path HKEY_LOCAL_MACHINE\\%s failed with error: %s", key, value, keyPath, err) - } - - return nil -} diff --git a/client/internal/engine.go b/client/internal/engine.go index 8a6c08642..aeeddc372 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -714,8 +714,9 @@ func toDNSConfig(protoDNSConfig *mgmProto.DNSConfig) nbdns.Config { for _, nsGroup := range protoDNSConfig.GetNameServerGroups() { dnsNSGroup := &nbdns.NameServerGroup{ - Primary: nsGroup.GetPrimary(), - Domains: nsGroup.GetDomains(), + Primary: nsGroup.GetPrimary(), + Domains: nsGroup.GetDomains(), + SearchDomainsEnabled: nsGroup.GetSearchDomainsEnabled(), } for _, ns := range nsGroup.GetNameServers() { dnsNS := nbdns.NameServer{ diff --git a/dns/nameserver.go b/dns/nameserver.go index f3ae2569d..bb904b165 100644 --- a/dns/nameserver.go +++ b/dns/nameserver.go @@ -67,6 +67,8 @@ type NameServerGroup struct { Domains []string `gorm:"serializer:json"` // Enabled group status Enabled bool + // SearchDomainsEnabled indicates whether to add match domains to search domains list or not + SearchDomainsEnabled bool } // NameServer represents a DNS nameserver @@ -133,14 +135,15 @@ func ParseNameServerURL(nsURL string) (NameServer, error) { // Copy copies a nameserver group object func (g *NameServerGroup) Copy() *NameServerGroup { nsGroup := &NameServerGroup{ - ID: g.ID, - Name: g.Name, - Description: g.Description, - NameServers: make([]NameServer, len(g.NameServers)), - Groups: make([]string, len(g.Groups)), - Enabled: g.Enabled, - Primary: g.Primary, - Domains: make([]string, len(g.Domains)), + ID: g.ID, + Name: g.Name, + Description: g.Description, + NameServers: make([]NameServer, len(g.NameServers)), + Groups: make([]string, len(g.Groups)), + Enabled: g.Enabled, + Primary: g.Primary, + Domains: make([]string, len(g.Domains)), + SearchDomainsEnabled: g.SearchDomainsEnabled, } copy(nsGroup.NameServers, g.NameServers) @@ -156,6 +159,7 @@ func (g *NameServerGroup) IsEqual(other *NameServerGroup) bool { other.Name == g.Name && other.Description == g.Description && other.Primary == g.Primary && + other.SearchDomainsEnabled == g.SearchDomainsEnabled && compareNameServerList(g.NameServers, other.NameServers) && compareGroupsList(g.Groups, other.Groups) && compareGroupsList(g.Domains, other.Domains) diff --git a/management/proto/management.pb.go b/management/proto/management.pb.go index eb80f9299..45ef49e1f 100644 --- a/management/proto/management.pb.go +++ b/management/proto/management.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.26.0 -// protoc v3.21.12 +// protoc v3.21.9 // source: management.proto package proto @@ -1999,9 +1999,10 @@ type NameServerGroup struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - NameServers []*NameServer `protobuf:"bytes,1,rep,name=NameServers,proto3" json:"NameServers,omitempty"` - Primary bool `protobuf:"varint,2,opt,name=Primary,proto3" json:"Primary,omitempty"` - Domains []string `protobuf:"bytes,3,rep,name=Domains,proto3" json:"Domains,omitempty"` + NameServers []*NameServer `protobuf:"bytes,1,rep,name=NameServers,proto3" json:"NameServers,omitempty"` + Primary bool `protobuf:"varint,2,opt,name=Primary,proto3" json:"Primary,omitempty"` + Domains []string `protobuf:"bytes,3,rep,name=Domains,proto3" json:"Domains,omitempty"` + SearchDomainsEnabled bool `protobuf:"varint,4,opt,name=SearchDomainsEnabled,proto3" json:"SearchDomainsEnabled,omitempty"` } func (x *NameServerGroup) Reset() { @@ -2057,6 +2058,13 @@ func (x *NameServerGroup) GetDomains() []string { return nil } +func (x *NameServerGroup) GetSearchDomainsEnabled() bool { + if x != nil { + return x.SearchDomainsEnabled + } + return false +} + // NameServer represents a dns.NameServer type NameServer struct { state protoimpl.MessageState @@ -2444,73 +2452,76 @@ var file_management_proto_rawDesc = []byte{ 0x52, 0x05, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x54, 0x54, 0x4c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 0x54, 0x54, 0x4c, 0x12, 0x14, 0x0a, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x52, 0x44, 0x61, 0x74, 0x61, 0x22, - 0x7f, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, 0x6f, - 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, - 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x50, - 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, - 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x0e, - 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x50, 0x12, 0x16, - 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, - 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0xf0, 0x02, 0x0a, 0x0c, 0x46, - 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x50, - 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x50, 0x65, 0x65, - 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, - 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x44, 0x69, 0x72, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0xb3, 0x01, 0x0a, 0x0f, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x47, 0x72, + 0x6f, 0x75, 0x70, 0x12, 0x38, 0x0a, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, + 0x52, 0x0b, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, + 0x07, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, + 0x73, 0x12, 0x32, 0x0a, 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, + 0x6e, 0x73, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x14, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x73, 0x45, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x4e, 0x61, 0x6d, 0x65, 0x53, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x49, 0x50, 0x12, 0x16, 0x0a, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x4e, 0x53, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x50, + 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, + 0xf0, 0x02, 0x0a, 0x0c, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, + 0x12, 0x16, 0x0a, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x50, 0x65, 0x65, 0x72, 0x49, 0x50, 0x12, 0x40, 0x0a, 0x09, 0x44, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, + 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x09, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, + 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, - 0x0a, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x69, - 0x72, 0x65, 0x77, 0x61, 0x6c, 0x6c, 0x52, 0x75, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x12, 0x0a, - 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x50, 0x6f, 0x72, - 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x06, - 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, 0x55, 0x54, 0x10, 0x01, 0x22, - 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x43, 0x43, - 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, 0x4f, 0x50, 0x10, 0x01, 0x22, - 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, - 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x4c, 0x4c, 0x10, - 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x55, 0x44, - 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, 0x10, 0x04, 0x32, 0xd1, 0x03, - 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1c, 0x2e, 0x6d, - 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, - 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, - 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x04, 0x53, 0x79, - 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, - 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, - 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, 0x73, 0x48, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, - 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x5a, 0x0a, 0x1a, 0x47, - 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x52, 0x08, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x22, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x06, 0x0a, 0x02, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, 0x03, 0x4f, + 0x55, 0x54, 0x10, 0x01, 0x22, 0x1e, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0a, + 0x0a, 0x06, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x44, 0x52, + 0x4f, 0x50, 0x10, 0x01, 0x22, 0x3c, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, 0x0a, + 0x03, 0x41, 0x4c, 0x4c, 0x10, 0x01, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x43, 0x50, 0x10, 0x02, 0x12, + 0x07, 0x0a, 0x03, 0x55, 0x44, 0x50, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x43, 0x4d, 0x50, + 0x10, 0x04, 0x32, 0xd1, 0x03, 0x0a, 0x11, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x45, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, + 0x6e, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, + 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, + 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, + 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, + 0x46, 0x0a, 0x04, 0x53, 0x79, 0x6e, 0x63, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x50, 0x4b, - 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, - 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, - 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x42, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x53, 0x65, + 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, + 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, + 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x09, 0x69, + 0x73, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x79, 0x12, 0x11, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x11, 0x2e, 0x6d, 0x61, + 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x5a, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, + 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, + 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x12, 0x58, 0x0a, 0x18, + 0x47, 0x65, 0x74, 0x50, 0x4b, 0x43, 0x45, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x6f, 0x77, 0x12, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x1c, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x22, 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/management/proto/management.proto b/management/proto/management.proto index d5b925d73..ae90beaf3 100644 --- a/management/proto/management.proto +++ b/management/proto/management.proto @@ -317,6 +317,7 @@ message NameServerGroup { repeated NameServer NameServers = 1; bool Primary = 2; repeated string Domains = 3; + bool SearchDomainsEnabled = 4; } // NameServer represents a dns.NameServer diff --git a/management/server/account.go b/management/server/account.go index f78530b44..9ca44615a 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -91,7 +91,7 @@ type AccountManager interface { DeleteRoute(accountID, routeID, userID string) error ListRoutes(accountID, userID string) ([]*route.Route, error) GetNameServerGroup(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) - CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error) + CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainsEnabled bool) (*nbdns.NameServerGroup, error) SaveNameServerGroup(accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error DeleteNameServerGroup(accountID, nsGroupID, userID string) error ListNameServerGroups(accountID string) ([]*nbdns.NameServerGroup, error) diff --git a/management/server/dns.go b/management/server/dns.go index 7b25e230f..9c39a00fa 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -130,8 +130,9 @@ func toProtocolDNSConfig(update nbdns.Config) *proto.DNSConfig { for _, nsGroup := range update.NameServerGroups { protoGroup := &proto.NameServerGroup{ - Primary: nsGroup.Primary, - Domains: nsGroup.Domains, + Primary: nsGroup.Primary, + Domains: nsGroup.Domains, + SearchDomainsEnabled: nsGroup.SearchDomainsEnabled, } for _, ns := range nsGroup.NameServers { protoNS := &proto.NameServer{ diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 658d389f6..30e55571c 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -857,13 +857,17 @@ components: type: boolean example: true domains: - description: Nameserver group domain list + description: Nameserver group match domain list type: array items: type: string minLength: 1 maxLength: 255 example: "example.com" + search_domains_enabled: + description: Nameserver group search domain status for match domains. It should be true only if domains list is not empty. + type: boolean + example: true required: - name - description @@ -872,6 +876,7 @@ components: - groups - primary - domains + - search_domains_enabled NameserverGroup: allOf: - type: object diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index fd3eedde3..cf9b0892e 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.15.0 DO NOT EDIT. +// Code generated by github.com/deepmap/oapi-codegen version v1.11.1-0.20220912230023-4a1477f6a8ba DO NOT EDIT. package api import ( @@ -248,7 +248,7 @@ type NameserverGroup struct { // Description Nameserver group description Description string `json:"description"` - // Domains Nameserver group domain list + // Domains Nameserver group match domain list Domains []string `json:"domains"` // Enabled Nameserver group status @@ -268,6 +268,9 @@ type NameserverGroup struct { // Primary Nameserver group primary status Primary bool `json:"primary"` + + // SearchDomainsEnabled Nameserver group search domain status for match domains. It should be true only if domains list is not empty. + SearchDomainsEnabled bool `json:"search_domains_enabled"` } // NameserverGroupRequest defines model for NameserverGroupRequest. @@ -275,7 +278,7 @@ type NameserverGroupRequest struct { // Description Nameserver group description Description string `json:"description"` - // Domains Nameserver group domain list + // Domains Nameserver group match domain list Domains []string `json:"domains"` // Enabled Nameserver group status @@ -292,6 +295,9 @@ type NameserverGroupRequest struct { // Primary Nameserver group primary status Primary bool `json:"primary"` + + // SearchDomainsEnabled Nameserver group search domain status for match domains. It should be true only if domains list is not empty. + SearchDomainsEnabled bool `json:"search_domains_enabled"` } // Peer defines model for Peer. diff --git a/management/server/http/nameservers_handler.go b/management/server/http/nameservers_handler.go index 918988d69..871bf639a 100644 --- a/management/server/http/nameservers_handler.go +++ b/management/server/http/nameservers_handler.go @@ -79,7 +79,7 @@ func (h *NameserversHandler) CreateNameserverGroup(w http.ResponseWriter, r *htt return } - nsGroup, err := h.accountManager.CreateNameServerGroup(account.Id, req.Name, req.Description, nsList, req.Groups, req.Primary, req.Domains, req.Enabled, user.Id) + nsGroup, err := h.accountManager.CreateNameServerGroup(account.Id, req.Name, req.Description, nsList, req.Groups, req.Primary, req.Domains, req.Enabled, user.Id, req.SearchDomainsEnabled) if err != nil { util.WriteError(err, w) return @@ -119,14 +119,15 @@ func (h *NameserversHandler) UpdateNameserverGroup(w http.ResponseWriter, r *htt } updatedNSGroup := &nbdns.NameServerGroup{ - ID: nsGroupID, - Name: req.Name, - Description: req.Description, - Primary: req.Primary, - Domains: req.Domains, - NameServers: nsList, - Groups: req.Groups, - Enabled: req.Enabled, + ID: nsGroupID, + Name: req.Name, + Description: req.Description, + Primary: req.Primary, + Domains: req.Domains, + NameServers: nsList, + Groups: req.Groups, + Enabled: req.Enabled, + SearchDomainsEnabled: req.SearchDomainsEnabled, } err = h.accountManager.SaveNameServerGroup(account.Id, user.Id, updatedNSGroup) @@ -216,13 +217,14 @@ func toNameserverGroupResponse(serverNSGroup *nbdns.NameServerGroup) *api.Namese } return &api.NameserverGroup{ - Id: serverNSGroup.ID, - Name: serverNSGroup.Name, - Description: serverNSGroup.Description, - Primary: serverNSGroup.Primary, - Domains: serverNSGroup.Domains, - Groups: serverNSGroup.Groups, - Nameservers: nsList, - Enabled: serverNSGroup.Enabled, + Id: serverNSGroup.ID, + Name: serverNSGroup.Name, + Description: serverNSGroup.Description, + Primary: serverNSGroup.Primary, + Domains: serverNSGroup.Domains, + Groups: serverNSGroup.Groups, + Nameservers: nsList, + Enabled: serverNSGroup.Enabled, + SearchDomainsEnabled: serverNSGroup.SearchDomainsEnabled, } } diff --git a/management/server/http/nameservers_handler_test.go b/management/server/http/nameservers_handler_test.go index 100f4b87a..b00ff606f 100644 --- a/management/server/http/nameservers_handler_test.go +++ b/management/server/http/nameservers_handler_test.go @@ -67,16 +67,17 @@ func initNameserversTestData() *NameserversHandler { } return nil, status.Errorf(status.NotFound, "nameserver group with ID %s not found", nsGroupID) }, - CreateNameServerGroupFunc: func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, _ string) (*nbdns.NameServerGroup, error) { + CreateNameServerGroupFunc: func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, _ string, searchDomains bool) (*nbdns.NameServerGroup, error) { return &nbdns.NameServerGroup{ - ID: existingNSGroupID, - Name: name, - Description: description, - NameServers: nameServerList, - Groups: groups, - Enabled: enabled, - Primary: primary, - Domains: domains, + ID: existingNSGroupID, + Name: name, + Description: description, + NameServers: nameServerList, + Groups: groups, + Enabled: enabled, + Primary: primary, + Domains: domains, + SearchDomainsEnabled: searchDomains, }, nil }, DeleteNameServerGroupFunc: func(accountID, nsGroupID, _ string) error { diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index ab3748c01..ea4a18f56 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -60,7 +60,7 @@ type MockAccountManager struct { GetPATFunc func(accountID string, initiatorUserID string, targetUserId string, tokenID string) (*server.PersonalAccessToken, error) GetAllPATsFunc func(accountID string, initiatorUserID string, targetUserId string) ([]*server.PersonalAccessToken, error) GetNameServerGroupFunc func(accountID, nsGroupID string) (*nbdns.NameServerGroup, error) - CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error) + CreateNameServerGroupFunc func(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainsEnabled bool) (*nbdns.NameServerGroup, error) SaveNameServerGroupFunc func(accountID, userID string, nsGroupToSave *nbdns.NameServerGroup) error DeleteNameServerGroupFunc func(accountID, nsGroupID, userID string) error ListNameServerGroupsFunc func(accountID string) ([]*nbdns.NameServerGroup, error) @@ -464,9 +464,9 @@ func (am *MockAccountManager) GetNameServerGroup(accountID, nsGroupID string) (* } // CreateNameServerGroup mocks CreateNameServerGroup of the AccountManager interface -func (am *MockAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error) { +func (am *MockAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainsEnabled bool) (*nbdns.NameServerGroup, error) { if am.CreateNameServerGroupFunc != nil { - return am.CreateNameServerGroupFunc(accountID, name, description, nameServerList, groups, primary, domains, enabled, userID) + return am.CreateNameServerGroupFunc(accountID, name, description, nameServerList, groups, primary, domains, enabled, userID, searchDomainsEnabled) } return nil, nil } diff --git a/management/server/nameserver.go b/management/server/nameserver.go index 9af5b49ad..8ae71dbae 100644 --- a/management/server/nameserver.go +++ b/management/server/nameserver.go @@ -35,7 +35,7 @@ func (am *DefaultAccountManager) GetNameServerGroup(accountID, nsGroupID string) } // CreateNameServerGroup creates and saves a new nameserver group -func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string) (*nbdns.NameServerGroup, error) { +func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, description string, nameServerList []nbdns.NameServer, groups []string, primary bool, domains []string, enabled bool, userID string, searchDomainEnabled bool) (*nbdns.NameServerGroup, error) { unlock := am.Store.AcquireAccountLock(accountID) defer unlock() @@ -46,14 +46,15 @@ func (am *DefaultAccountManager) CreateNameServerGroup(accountID string, name, d } newNSGroup := &nbdns.NameServerGroup{ - ID: xid.New().String(), - Name: name, - Description: description, - NameServers: nameServerList, - Groups: groups, - Enabled: enabled, - Primary: primary, - Domains: domains, + ID: xid.New().String(), + Name: name, + Description: description, + NameServers: nameServerList, + Groups: groups, + Enabled: enabled, + Primary: primary, + Domains: domains, + SearchDomainsEnabled: searchDomainEnabled, } err = validateNameServerGroup(false, newNSGroup, account) @@ -174,7 +175,7 @@ func validateNameServerGroup(existingGroup bool, nameserverGroup *nbdns.NameServ } } - err := validateDomainInput(nameserverGroup.Primary, nameserverGroup.Domains) + err := validateDomainInput(nameserverGroup.Primary, nameserverGroup.Domains, nameserverGroup.SearchDomainsEnabled) if err != nil { return err } @@ -197,7 +198,7 @@ func validateNameServerGroup(existingGroup bool, nameserverGroup *nbdns.NameServ return nil } -func validateDomainInput(primary bool, domains []string) error { +func validateDomainInput(primary bool, domains []string, searchDomainsEnabled bool) error { if !primary && len(domains) == 0 { return status.Errorf(status.InvalidArgument, "nameserver group primary status is false and domains are empty,"+ " it should be primary or have at least one domain") @@ -206,6 +207,12 @@ func validateDomainInput(primary bool, domains []string) error { return status.Errorf(status.InvalidArgument, "nameserver group primary status is true and domains are not empty,"+ " you should set either primary or domain") } + + if primary && searchDomainsEnabled { + return status.Errorf(status.InvalidArgument, "nameserver group primary status is true and search domains is enabled,"+ + " you should not set search domains for primary nameservers") + } + for _, domain := range domains { if err := validateDomain(domain); err != nil { return status.Errorf(status.InvalidArgument, "nameserver group got an invalid domain: %s %q", domain, err) diff --git a/management/server/nameserver_test.go b/management/server/nameserver_test.go index 8809dc8ad..6210ae538 100644 --- a/management/server/nameserver_test.go +++ b/management/server/nameserver_test.go @@ -23,13 +23,14 @@ const ( func TestCreateNameServerGroup(t *testing.T) { type input struct { - name string - description string - enabled bool - groups []string - nameServers []nbdns.NameServer - primary bool - domains []string + name string + description string + enabled bool + groups []string + nameServers []nbdns.NameServer + primary bool + domains []string + searchDomains bool } testCases := []struct { @@ -383,6 +384,7 @@ func TestCreateNameServerGroup(t *testing.T) { testCase.inputArgs.domains, testCase.inputArgs.enabled, userID, + testCase.inputArgs.searchDomains, ) testCase.errFunc(t, err) From eeb38b7ecfa395236315b03d37f07efee8a1fe71 Mon Sep 17 00:00:00 2001 From: pascal-fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:07:25 +0200 Subject: [PATCH 20/43] Update management.json template with all existing configuration parameters (#1182) trigger test on management/cmd and signal/cmd changes. --------- Co-authored-by: Maycon Santos --- .github/workflows/test-infrastructure-files.yml | 14 ++++++++------ infrastructure_files/management.json.tmpl | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index ce6f0b75a..29f868a64 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -8,6 +8,8 @@ on: paths: - 'infrastructure_files/**' - '.github/workflows/test-infrastructure-files.yml' + - 'management/cmd/**' + - 'signal/cmd/**' concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }} @@ -111,12 +113,12 @@ jobs: grep -A 5 IdpManagerConfig management.json | grep -A 3 ClientConfig | grep ClientID | grep $CI_NETBIRD_IDP_MGMT_CLIENT_ID grep -A 6 IdpManagerConfig management.json | grep -A 4 ClientConfig | grep ClientSecret | grep $CI_NETBIRD_IDP_MGMT_CLIENT_SECRET grep -A 7 IdpManagerConfig management.json | grep -A 5 ClientConfig | grep GrantType | grep client_credentials - grep -A 2 PKCEAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_AUDIENCE - grep -A 3 PKCEAuthorizationFlow management.json | grep -A 2 ProviderConfig | grep ClientID | grep $CI_NETBIRD_AUTH_CLIENT_ID - grep -A 4 PKCEAuthorizationFlow management.json | grep -A 3 ProviderConfig | grep ClientSecret | grep $CI_NETBIRD_AUTH_CLIENT_SECRET - grep -A 5 PKCEAuthorizationFlow management.json | grep -A 4 ProviderConfig | grep AuthorizationEndpoint | grep $CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT - grep -A 6 PKCEAuthorizationFlow management.json | grep -A 5 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT - grep -A 7 PKCEAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES" + grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_AUDIENCE + grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep ClientID | grep $CI_NETBIRD_AUTH_CLIENT_ID + grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep ClientSecret | grep $CI_NETBIRD_AUTH_CLIENT_SECRET + grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep AuthorizationEndpoint | grep $CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT + grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT + grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES" - name: Install modules run: go mod tidy diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index 7b8d6190d..ece953515 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -51,18 +51,25 @@ "ClientSecret": "$NETBIRD_IDP_MGMT_CLIENT_SECRET", "GrantType": "client_credentials" }, - "ExtraConfig": $NETBIRD_IDP_MGMT_EXTRA_CONFIG + "ExtraConfig": $NETBIRD_IDP_MGMT_EXTRA_CONFIG, + "Auth0ClientCredentials": null, + "AzureClientCredentials": null, + "KeycloakClientCredentials": null, + "ZitadelClientCredentials": null }, "DeviceAuthorizationFlow": { "Provider": "$NETBIRD_AUTH_DEVICE_AUTH_PROVIDER", "ProviderConfig": { "Audience": "$NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE", + "AuthorizationEndpoint": "", "Domain": "$NETBIRD_AUTH0_DOMAIN", "ClientID": "$NETBIRD_AUTH_DEVICE_AUTH_CLIENT_ID", + "ClientSecret": "", "TokenEndpoint": "$NETBIRD_AUTH_TOKEN_ENDPOINT", "DeviceAuthEndpoint": "$NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT", "Scope": "$NETBIRD_AUTH_DEVICE_AUTH_SCOPE", - "UseIDToken": $NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN + "UseIDToken": $NETBIRD_AUTH_DEVICE_AUTH_USE_ID_TOKEN, + "RedirectURLs": null } }, "PKCEAuthorizationFlow": { @@ -70,11 +77,13 @@ "Audience": "$NETBIRD_AUTH_PKCE_AUDIENCE", "ClientID": "$NETBIRD_AUTH_CLIENT_ID", "ClientSecret": "$NETBIRD_AUTH_CLIENT_SECRET", + "Domain": "", "AuthorizationEndpoint": "$NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT", "TokenEndpoint": "$NETBIRD_AUTH_TOKEN_ENDPOINT", "Scope": "$NETBIRD_AUTH_SUPPORTED_SCOPES", "RedirectURLs": [$NETBIRD_AUTH_PKCE_REDIRECT_URLS], - "UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN + "UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN, + "RedirectURLs": null } } } From 06318a15e1efd74013dd52f86e510076d655cbaa Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Thu, 19 Oct 2023 21:14:05 +0200 Subject: [PATCH 21/43] Log store engine type (#1234) --- management/server/store.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/management/server/store.go b/management/server/store.go index 458912e97..66b239f96 100644 --- a/management/server/store.go +++ b/management/server/store.go @@ -6,6 +6,8 @@ import ( "strings" "time" + log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/management/server/telemetry" ) @@ -67,8 +69,10 @@ func NewStore(kind StoreEngine, dataDir string, metrics telemetry.AppMetrics) (S } switch kind { case FileStoreEngine: + log.Info("using JSON file store engine") return NewFileStore(dataDir, metrics) case SqliteStoreEngine: + log.Info("using SQLite store engine") return NewSqliteStore(dataDir, metrics) default: return nil, fmt.Errorf("unsupported kind of store %s", kind) From 90c2093018bfcd63937193ef6daa69acc567882e Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Mon, 23 Oct 2023 16:08:21 +0200 Subject: [PATCH 22/43] Fix SaveUserLastLogin in SQLite store (#1241) --- management/server/sqlite_store.go | 8 ++++---- management/server/user.go | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index 97c759d8a..c5356581a 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -434,16 +434,16 @@ func (s *SqliteStore) GetAccountByPeerPubKey(peerKey string) (*Account, error) { // SaveUserLastLogin stores the last login time for a user in DB. func (s *SqliteStore) SaveUserLastLogin(accountID, userID string, lastLogin time.Time) error { - var peer Peer + var user User - result := s.db.First(&peer, "account_id = ? and user_id = ?", accountID, userID) + result := s.db.First(&user, "account_id = ? and id = ?", accountID, userID) if result.Error != nil { return status.Errorf(status.NotFound, "user %s not found", userID) } - peer.LastLogin = lastLogin + user.LastLogin = lastLogin - return s.db.Save(peer).Error + return s.db.Save(user).Error } // Close is noop in Sqlite diff --git a/management/server/user.go b/management/server/user.go index edb649340..6093d93a2 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -299,6 +299,14 @@ func (am *DefaultAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) ( return nil, fmt.Errorf("failed to get account with token claims %v", err) } + unlock := am.Store.AcquireAccountLock(account.Id) + defer unlock() + + account, err = am.Store.GetAccount(account.Id) + if err != nil { + return nil, fmt.Errorf("failed to get an account from store %v", err) + } + user, ok := account.Users[claims.UserId] if !ok { return nil, status.Errorf(status.NotFound, "user not found") @@ -306,16 +314,16 @@ func (am *DefaultAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) ( // this code should be outside of the am.GetAccountFromToken(claims) because this method is called also by the gRPC // server when user authenticates a device. And we need to separate the Dashboard login event from the Device login event. - unlock := am.Store.AcquireAccountLock(account.Id) newLogin := user.LastDashboardLoginChanged(claims.LastLogin) + err = am.Store.SaveUserLastLogin(account.Id, claims.UserId, claims.LastLogin) - unlock() + if err != nil { + log.Errorf("failed saving user last login: %v", err) + } + if newLogin { meta := map[string]any{"timestamp": claims.LastLogin} am.storeEvent(claims.UserId, claims.UserId, account.Id, activity.DashboardLogin, meta) - if err != nil { - log.Errorf("failed saving user last login: %v", err) - } } return user, nil From 7a5c6b24ae2503955046d2cf79c36e0481d42164 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Wed, 25 Oct 2023 00:12:10 +0200 Subject: [PATCH 23/43] Fix GetAccountByPrivateDomain for SQLite (#1242) --- management/server/sqlite_store.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/management/server/sqlite_store.go b/management/server/sqlite_store.go index c5356581a..ed473e143 100644 --- a/management/server/sqlite_store.go +++ b/management/server/sqlite_store.go @@ -245,7 +245,8 @@ func (s *SqliteStore) DeleteTokenID2UserIDIndex(tokenID string) error { func (s *SqliteStore) GetAccountByPrivateDomain(domain string) (*Account, error) { var account Account - result := s.db.First(&account, "domain = ?", strings.ToLower(domain)) + result := s.db.First(&account, "domain = ? and is_domain_primary_account = ? and domain_category = ?", + strings.ToLower(domain), true, PrivateCategory) if result.Error != nil { return nil, status.Errorf(status.NotFound, "account not found: provided domain is not registered or is not private") } From e7d52beeabfe68533c9c252a9f846bff72bef5e2 Mon Sep 17 00:00:00 2001 From: Glenn Sommer Date: Wed, 25 Oct 2023 00:14:00 +0200 Subject: [PATCH 24/43] Support pinning version during binary install (#1237) For installations using the binary release method (using the official installer script), it would be nice to be able to define a specific version to install. A user/developer can choose to define the NETBIRD_RELEASE variable during installation, to pin a specific version during installation. If NETBIRD_RELEASE is not defined, we default to the current behavior of latest --- release_files/install.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/release_files/install.sh b/release_files/install.sh index e529c229e..ef5012f0d 100755 --- a/release_files/install.sh +++ b/release_files/install.sh @@ -23,19 +23,28 @@ if command -v sudo > /dev/null && [ "$(id -u)" -ne 0 ]; then SUDO="sudo" fi -get_latest_release() { +if [ -z ${NETBIRD_RELEASE+x} ]; then + NETBIRD_RELEASE=latest +fi + +get_release() { + local RELEASE=$1 + if [ "$RELEASE" = "latest" ]; then + local TAG="latest" + else + local TAG="tags/${RELEASE}" + fi if [ -n "$GITHUB_TOKEN" ]; then - curl -H "Authorization: token ${GITHUB_TOKEN}" -s "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \ + curl -H "Authorization: token ${GITHUB_TOKEN}" -s "https://api.github.com/repos/${OWNER}/${REPO}/releases/${TAG}" \ | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' else - curl -s "https://api.github.com/repos/${OWNER}/${REPO}/releases/latest" \ + curl -s "https://api.github.com/repos/${OWNER}/${REPO}/releases/${TAG}" \ | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' fi - } download_release_binary() { - VERSION=$(get_latest_release) + VERSION=$(get_release "$NETBIRD_RELEASE") BASE_URL="https://github.com/${OWNER}/${REPO}/releases/download" BINARY_BASE_NAME="${VERSION#v}_${OS_TYPE}_${ARCH}.tar.gz" @@ -299,7 +308,7 @@ stop_running_netbird_ui() { update_netbird() { if is_bin_package_manager "$CONFIG_FILE"; then - latest_release=$(get_latest_release) + latest_release=$(get_release "latest") latest_version=${latest_release#v} installed_version=$(netbird version) From 40bea645e982b36a77e1182c04bf8b3107be4992 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 00:23:07 +0200 Subject: [PATCH 25/43] Bump golang.org/x/net from 0.10.0 to 0.17.0 (#1214) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.10.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.10.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 16 ++++++++++------ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 76e592f73..e1596aab7 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/spf13/cobra v1.6.1 github.com/spf13/pflag v1.0.5 github.com/vishvananda/netlink v1.1.0 - golang.org/x/crypto v0.9.0 - golang.org/x/sys v0.8.0 + golang.org/x/crypto v0.14.0 + golang.org/x/sys v0.13.0 golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de golang.zx2c4.com/wireguard/windows v0.5.3 @@ -69,10 +69,10 @@ require ( goauthentik.io/api/v3 v3.2023051.3 golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 - golang.org/x/net v0.10.0 + golang.org/x/net v0.17.0 golang.org/x/oauth2 v0.8.0 golang.org/x/sync v0.2.0 - golang.org/x/term v0.8.0 + golang.org/x/term v0.13.0 google.golang.org/api v0.126.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/sqlite v1.5.3 @@ -142,7 +142,7 @@ require ( go.opentelemetry.io/otel/trace v1.11.1 // indirect golang.org/x/image v0.5.0 // indirect golang.org/x/mod v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/tools v0.6.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect diff --git a/go.sum b/go.sum index 561d3e17e..07c118e3c 100644 --- a/go.sum +++ b/go.sum @@ -730,8 +730,8 @@ golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -838,8 +838,9 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -963,15 +964,17 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -985,8 +988,9 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 74ff2619d01625768cd2e5fd4e92a9441d8a5473 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 25 Oct 2023 00:47:40 +0200 Subject: [PATCH 26/43] Log client version on startup (#1240) --- client/internal/connect.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/internal/connect.go b/client/internal/connect.go index 6eecf4207..66946c4aa 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -22,6 +22,7 @@ import ( mgm "github.com/netbirdio/netbird/management/client" mgmProto "github.com/netbirdio/netbird/management/proto" signal "github.com/netbirdio/netbird/signal/client" + "github.com/netbirdio/netbird/version" ) // RunClient with main logic. @@ -43,6 +44,8 @@ func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.S } func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, mobileDependency MobileDependency) error { + log.Infof("starting NetBird client version %s", version.NetbirdVersion()) + backOff := &backoff.ExponentialBackOff{ InitialInterval: time.Second, RandomizationFactor: 1, From a8d03d8c91fb895a187e19adfb49a1cac06a9f41 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Wed, 25 Oct 2023 17:51:05 +0200 Subject: [PATCH 27/43] Fix redirect urls template processing (#1251) removed duplicated key and added tests --- .github/workflows/test-infrastructure-files.yml | 1 + infrastructure_files/management.json.tmpl | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-infrastructure-files.yml b/.github/workflows/test-infrastructure-files.yml index 29f868a64..6482b716f 100644 --- a/.github/workflows/test-infrastructure-files.yml +++ b/.github/workflows/test-infrastructure-files.yml @@ -119,6 +119,7 @@ jobs: grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep AuthorizationEndpoint | grep $CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES" + grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep -A 3 RedirectURLs | grep "http://localhost:53000" - name: Install modules run: go mod tidy diff --git a/infrastructure_files/management.json.tmpl b/infrastructure_files/management.json.tmpl index ece953515..64c2d0816 100644 --- a/infrastructure_files/management.json.tmpl +++ b/infrastructure_files/management.json.tmpl @@ -82,8 +82,7 @@ "TokenEndpoint": "$NETBIRD_AUTH_TOKEN_ENDPOINT", "Scope": "$NETBIRD_AUTH_SUPPORTED_SCOPES", "RedirectURLs": [$NETBIRD_AUTH_PKCE_REDIRECT_URLS], - "UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN, - "RedirectURLs": null + "UseIDToken": $NETBIRD_AUTH_PKCE_USE_ID_TOKEN } } } From db25ca21a8689d53404ecbf72abe4f6e32d16a0e Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 27 Oct 2023 09:52:11 +0200 Subject: [PATCH 28/43] Log auth0 batch length (#1255) --- management/server/idp/auth0.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/idp/auth0.go b/management/server/idp/auth0.go index d3802d8ad..745136f62 100644 --- a/management/server/idp/auth0.go +++ b/management/server/idp/auth0.go @@ -337,7 +337,7 @@ func (am *Auth0Manager) GetAccount(accountID string) ([]*UserData, error) { return nil, err } - log.Debugf("returned user batch for accountID %s on page %d, %v", accountID, page, batch) + log.Debugf("returned user batch for accountID %s on page %d, batch length %d", accountID, page, len(batch)) err = res.Body.Close() if err != nil { From 76318f3f0618e4e08d0cef4a6766240e9b4166ff Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 27 Oct 2023 10:54:26 +0200 Subject: [PATCH 29/43] Fix Windows firewall message check (#1254) The no rules matched message is operating system language specific, and can cause errors Now we check if firewall is reachable by the app and then if the rule is returned or not in two different calls: isWindowsFirewallReachable isFirewallRuleActive --- .../uspfilter/allow_netbird_windows.go | 87 ++++++++++--------- client/internal/acl/manager_create.go | 2 +- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/client/firewall/uspfilter/allow_netbird_windows.go b/client/firewall/uspfilter/allow_netbird_windows.go index 05a6d22ae..140bfc87a 100644 --- a/client/firewall/uspfilter/allow_netbird_windows.go +++ b/client/firewall/uspfilter/allow_netbird_windows.go @@ -1,21 +1,19 @@ package uspfilter import ( - "errors" "fmt" "os/exec" - "strings" "syscall" + + log "github.com/sirupsen/logrus" ) type action string const ( - addRule action = "add" - deleteRule action = "delete" - - firewallRuleName = "Netbird" - noRulesMatchCriteria = "No rules match the specified criteria" + addRule action = "add" + deleteRule action = "delete" + firewallRuleName = "Netbird" ) // Reset firewall to the default state @@ -26,6 +24,14 @@ func (m *Manager) Reset() error { m.outgoingRules = make(map[string]RuleSet) m.incomingRules = make(map[string]RuleSet) + if !isWindowsFirewallReachable() { + return nil + } + + if !isFirewallRuleActive(firewallRuleName) { + return nil + } + if err := manageFirewallRule(firewallRuleName, deleteRule); err != nil { return fmt.Errorf("couldn't remove windows firewall: %w", err) } @@ -35,6 +41,13 @@ func (m *Manager) Reset() error { // AllowNetbird allows netbird interface traffic func (m *Manager) AllowNetbird() error { + if !isWindowsFirewallReachable() { + return nil + } + + if isFirewallRuleActive(firewallRuleName) { + return nil + } return manageFirewallRule(firewallRuleName, addRule, "dir=in", @@ -45,47 +58,37 @@ func (m *Manager) AllowNetbird() error { ) } -func manageFirewallRule(ruleName string, action action, args ...string) error { - active, err := isFirewallRuleActive(ruleName) - if err != nil { - return err +func manageFirewallRule(ruleName string, action action, extraArgs ...string) error { + + args := []string{"advfirewall", "firewall", string(action), "rule", "name=" + ruleName} + if action == addRule { + args = append(args, extraArgs...) } - if (action == addRule && !active) || (action == deleteRule && active) { - baseArgs := []string{"advfirewall", "firewall", string(action), "rule", "name=" + ruleName} - args := append(baseArgs, args...) - - cmd := exec.Command("netsh", args...) - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - return cmd.Run() - } - - return nil + cmd := exec.Command("netsh", args...) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + return cmd.Run() } -func isFirewallRuleActive(ruleName string) (bool, error) { +func isWindowsFirewallReachable() bool { + args := []string{"advfirewall", "show", "allprofiles", "state"} + cmd := exec.Command("netsh", args...) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + + _, err := cmd.Output() + if err != nil { + log.Infof("Windows firewall is not reachable, skipping default rule management. Using only user space rules. Error: %s", err) + return false + } + + return true +} + +func isFirewallRuleActive(ruleName string) bool { args := []string{"advfirewall", "firewall", "show", "rule", "name=" + ruleName} cmd := exec.Command("netsh", args...) cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - output, err := cmd.Output() - if err != nil { - var exitError *exec.ExitError - if errors.As(err, &exitError) { - // if the firewall rule is not active, we expect last exit code to be 1 - exitStatus := exitError.Sys().(syscall.WaitStatus).ExitStatus() - if exitStatus == 1 { - if strings.Contains(string(output), noRulesMatchCriteria) { - return false, nil - } - } - } - return false, err - } - - if strings.Contains(string(output), noRulesMatchCriteria) { - return false, nil - } - - return true, nil + _, err := cmd.Output() + return err == nil } diff --git a/client/internal/acl/manager_create.go b/client/internal/acl/manager_create.go index 2fdca02ae..66185749b 100644 --- a/client/internal/acl/manager_create.go +++ b/client/internal/acl/manager_create.go @@ -20,7 +20,7 @@ func Create(iface IFaceMapper) (manager *DefaultManager, err error) { return nil, err } if err := fm.AllowNetbird(); err != nil { - log.Errorf("failed to allow netbird interface traffic: %v", err) + log.Warnf("failed to allow netbird interface traffic: %v", err) } return newDefaultManager(fm), nil } From e2eef4e3fd6a96d5261dc09770f6c9e3afd6096e Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Fri, 27 Oct 2023 17:18:44 +0200 Subject: [PATCH 30/43] Pass JWT Claims Extractor to Integrations (#1258) --- go.mod | 2 +- go.sum | 4 ++-- management/server/http/handler.go | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index e1596aab7..e186002f3 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/miekg/dns v1.1.43 github.com/mitchellh/hashstructure/v2 v2.0.2 github.com/nadoo/ipset v0.5.0 - github.com/netbirdio/management-integrations/integrations v0.0.0-20231017101406-322cbabed3da + github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200-a966bce7db88 github.com/okta/okta-sdk-golang/v2 v2.18.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pion/logging v0.2.2 diff --git a/go.sum b/go.sum index 07c118e3c..f06eb08d0 100644 --- a/go.sum +++ b/go.sum @@ -495,8 +495,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nadoo/ipset v0.5.0 h1:5GJUAuZ7ITQQQGne5J96AmFjRtI8Avlbk6CabzYWVUc= github.com/nadoo/ipset v0.5.0/go.mod h1:rYF5DQLRGGoQ8ZSWeK+6eX5amAuPqwFkWjhQlEITGJQ= -github.com/netbirdio/management-integrations/integrations v0.0.0-20231017101406-322cbabed3da h1:S1RoPhLTw3+IhHGnyfcQlj4aqIIaQdVd3SqaiK+MYFY= -github.com/netbirdio/management-integrations/integrations v0.0.0-20231017101406-322cbabed3da/go.mod h1:KSqjzHcqlodTWiuap5lRXxt5KT3vtYRoksL0KIrTK40= +github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200-a966bce7db88 h1:zhe8qseauBuYOS910jpl5sv8Tb+36zxQPXrwYXqll0g= +github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200-a966bce7db88/go.mod h1:KSqjzHcqlodTWiuap5lRXxt5KT3vtYRoksL0KIrTK40= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0 h1:hirFRfx3grVA/9eEyjME5/z3nxdJlN9kfQpvWWPk32g= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c h1:wK/s4nyZj/GF/kFJQjX6nqNfE0G3gcqd6hhnPCyp4sw= diff --git a/management/server/http/handler.go b/management/server/http/handler.go index 0d415a087..c589512e5 100644 --- a/management/server/http/handler.go +++ b/management/server/http/handler.go @@ -59,7 +59,12 @@ func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValid AuthCfg: authCfg, } - integrations.RegisterHandlers(api.Router, accountManager) + claimsExtractor := jwtclaims.NewClaimsExtractor( + jwtclaims.WithAudience(authCfg.Audience), + jwtclaims.WithUserIDClaim(authCfg.UserIDClaim), + ) + + integrations.RegisterHandlers(api.Router, accountManager, claimsExtractor) api.addAccountsEndpoint() api.addPeersEndpoint() api.addUsersEndpoint() From 52f5101715e892cf12445f03e346becb33ee6c37 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Mon, 30 Oct 2023 09:25:33 +0100 Subject: [PATCH 31/43] Fix network route adding rule to filter table (#1266) Set filterTable only for ipv4 table --- client/internal/routemanager/client.go | 2 +- client/internal/routemanager/nftables_linux.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/internal/routemanager/client.go b/client/internal/routemanager/client.go index 62fe4dfc1..fda7b012f 100644 --- a/client/internal/routemanager/client.go +++ b/client/internal/routemanager/client.go @@ -119,7 +119,7 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]ro log.Warnf("the network %s has not been assigned a routing peer as no peers from the list %s are currently connected", c.network, peers) } else if chosen != currID { - log.Infof("new chosen route is %s with peer %s with score %d", chosen, c.routes[chosen].Peer, chosenScore) + log.Infof("new chosen route is %s with peer %s with score %d for network %s", chosen, c.routes[chosen].Peer, chosenScore, c.network) } return chosen diff --git a/client/internal/routemanager/nftables_linux.go b/client/internal/routemanager/nftables_linux.go index 25dc6e7db..d6eac96a6 100644 --- a/client/internal/routemanager/nftables_linux.go +++ b/client/internal/routemanager/nftables_linux.go @@ -135,7 +135,8 @@ func (n *nftablesManager) RestoreOrCreateContainers() error { } for _, table := range tables { - if table.Name == "filter" { + if table.Name == "filter" && table.Family == nftables.TableFamilyIPv4 { + log.Debugf("nftables: found filter table for ipv4") n.filterTable = table continue } From 6d4240a5ae2c1bc16a469dd407a8a6a858c833f0 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Mon, 30 Oct 2023 10:32:48 +0100 Subject: [PATCH 32/43] Feature/update check (#1232) Periodically fetch the latest available version, and the UI will shows a new menu for the download link. It checks both the daemon version and the UI version. --- .github/workflows/release.yml | 1 + .goreleaser_ui.yaml | 6 +- client/ui/client_ui.go | 145 ++++++++++++-- client/ui/connected.ico | Bin 110003 -> 0 bytes client/ui/connected.png | Bin 10290 -> 0 bytes client/ui/disconnected.ico | Bin 106176 -> 0 bytes client/ui/disconnected.png | Bin 6461 -> 0 bytes client/ui/netbird-systemtray-connected.ico | Bin 0 -> 4452 bytes client/ui/netbird-systemtray-connected.png | Bin 0 -> 7251 bytes client/ui/netbird-systemtray-default.ico | Bin 0 -> 2876 bytes client/ui/netbird-systemtray-default.png | Bin 0 -> 4938 bytes client/ui/netbird-systemtray-update-cloud.ico | Bin 0 -> 3647 bytes client/ui/netbird-systemtray-update-cloud.png | Bin 0 -> 5652 bytes client/ui/netbird-systemtray-update.ico | Bin 0 -> 4726 bytes client/ui/netbird-systemtray-update.png | Bin 0 -> 7521 bytes release_files/darwin-ui-installer.sh | 3 +- release_files/darwin_pkg/preinstall | 7 + version/update.go | 184 ++++++++++++++++++ version/update_test.go | 101 ++++++++++ version/url.go | 5 + version/url_darwin.go | 33 ++++ version/url_linux.go | 6 + version/url_windows.go | 19 ++ 23 files changed, 492 insertions(+), 18 deletions(-) delete mode 100644 client/ui/connected.ico delete mode 100644 client/ui/connected.png delete mode 100644 client/ui/disconnected.ico delete mode 100644 client/ui/disconnected.png create mode 100644 client/ui/netbird-systemtray-connected.ico create mode 100644 client/ui/netbird-systemtray-connected.png create mode 100644 client/ui/netbird-systemtray-default.ico create mode 100644 client/ui/netbird-systemtray-default.png create mode 100644 client/ui/netbird-systemtray-update-cloud.ico create mode 100644 client/ui/netbird-systemtray-update-cloud.png create mode 100644 client/ui/netbird-systemtray-update.ico create mode 100644 client/ui/netbird-systemtray-update.png create mode 100644 version/update.go create mode 100644 version/update_test.go create mode 100644 version/url.go create mode 100644 version/url_darwin.go create mode 100644 version/url_linux.go create mode 100644 version/url_windows.go diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3feefdd49..5833638c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,7 @@ on: - 'release_files/**' - '**/Dockerfile' - '**/Dockerfile.*' + - 'client/ui/**' env: SIGN_PIPE_VER: "v0.0.9" diff --git a/.goreleaser_ui.yaml b/.goreleaser_ui.yaml index c6f7a7c34..66a22ee34 100644 --- a/.goreleaser_ui.yaml +++ b/.goreleaser_ui.yaml @@ -54,7 +54,7 @@ nfpms: contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/disconnected.png + - src: client/ui/netbird-systemtray-default.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird @@ -71,7 +71,7 @@ nfpms: contents: - src: client/ui/netbird.desktop dst: /usr/share/applications/netbird.desktop - - src: client/ui/disconnected.png + - src: client/ui/netbird-systemtray-default.png dst: /usr/share/pixmaps/netbird.png dependencies: - netbird @@ -91,4 +91,4 @@ uploads: mode: archive target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }} username: dev@wiretrustee.com - method: PUT \ No newline at end of file + method: PUT diff --git a/client/ui/client_ui.go b/client/ui/client_ui.go index 9c7685db0..e66d03d95 100644 --- a/client/ui/client_ui.go +++ b/client/ui/client_ui.go @@ -15,8 +15,10 @@ import ( "runtime" "strconv" "strings" + "sync" "syscall" "time" + "unicode" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" @@ -74,18 +76,30 @@ func main() { } } -//go:embed connected.ico +//go:embed netbird-systemtray-connected.ico var iconConnectedICO []byte -//go:embed connected.png +//go:embed netbird-systemtray-connected.png var iconConnectedPNG []byte -//go:embed disconnected.ico +//go:embed netbird-systemtray-default.ico var iconDisconnectedICO []byte -//go:embed disconnected.png +//go:embed netbird-systemtray-default.png var iconDisconnectedPNG []byte +//go:embed netbird-systemtray-update.ico +var iconUpdateICO []byte + +//go:embed netbird-systemtray-update.png +var iconUpdatePNG []byte + +//go:embed netbird-systemtray-update-cloud.ico +var iconUpdateCloudICO []byte + +//go:embed netbird-systemtray-update-cloud.png +var iconUpdateCloudPNG []byte + type serviceClient struct { ctx context.Context addr string @@ -93,14 +107,20 @@ type serviceClient struct { icConnected []byte icDisconnected []byte + icUpdate []byte + icUpdateCloud []byte // systray menu items - mStatus *systray.MenuItem - mUp *systray.MenuItem - mDown *systray.MenuItem - mAdminPanel *systray.MenuItem - mSettings *systray.MenuItem - mQuit *systray.MenuItem + mStatus *systray.MenuItem + mUp *systray.MenuItem + mDown *systray.MenuItem + mAdminPanel *systray.MenuItem + mSettings *systray.MenuItem + mAbout *systray.MenuItem + mVersionUI *systray.MenuItem + mVersionDaemon *systray.MenuItem + mUpdate *systray.MenuItem + mQuit *systray.MenuItem // application with main windows. app fyne.App @@ -118,6 +138,11 @@ type serviceClient struct { managementURL string preSharedKey string adminURL string + + update *version.Update + daemonVersion string + updateIndicationLock sync.Mutex + isUpdateIconActive bool } // newServiceClient instance constructor @@ -130,14 +155,20 @@ func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient app: a, showSettings: showSettings, + update: version.NewUpdate(), } if runtime.GOOS == "windows" { s.icConnected = iconConnectedICO s.icDisconnected = iconDisconnectedICO + s.icUpdate = iconUpdateICO + s.icUpdateCloud = iconUpdateCloudICO + } else { s.icConnected = iconConnectedPNG s.icDisconnected = iconDisconnectedPNG + s.icUpdate = iconUpdatePNG + s.icUpdateCloud = iconUpdateCloudPNG } if showSettings { @@ -328,19 +359,53 @@ func (s *serviceClient) updateStatus() error { return err } + s.updateIndicationLock.Lock() + defer s.updateIndicationLock.Unlock() + + var systrayIconState bool if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() { - systray.SetIcon(s.icConnected) + if !s.isUpdateIconActive { + systray.SetIcon(s.icConnected) + } systray.SetTooltip("NetBird (Connected)") s.mStatus.SetTitle("Connected") s.mUp.Disable() s.mDown.Enable() + systrayIconState = true } else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() { - systray.SetIcon(s.icDisconnected) + if !s.isUpdateIconActive { + systray.SetIcon(s.icDisconnected) + } systray.SetTooltip("NetBird (Disconnected)") s.mStatus.SetTitle("Disconnected") s.mDown.Disable() s.mUp.Enable() + systrayIconState = false } + + // the updater struct notify by the upgrades available only, but if meanwhile the daemon has successfully + // updated must reset the mUpdate visibility state + if s.daemonVersion != status.DaemonVersion { + s.mUpdate.Hide() + s.daemonVersion = status.DaemonVersion + + s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion) + if !s.isUpdateIconActive { + if systrayIconState { + systray.SetIcon(s.icConnected) + s.mAbout.SetIcon(s.icConnected) + } else { + systray.SetIcon(s.icDisconnected) + s.mAbout.SetIcon(s.icDisconnected) + } + } + + daemonVersionTitle := normalizedVersion(s.daemonVersion) + s.mVersionDaemon.SetTitle(fmt.Sprintf("Daemon: %s", daemonVersionTitle)) + s.mVersionDaemon.SetTooltip(fmt.Sprintf("Daemon version: %s", daemonVersionTitle)) + s.mVersionDaemon.Show() + } + return nil }, &backoff.ExponentialBackOff{ InitialInterval: time.Second, @@ -374,11 +439,24 @@ func (s *serviceClient) onTrayReady() { systray.AddSeparator() s.mSettings = systray.AddMenuItem("Settings", "Settings of the application") systray.AddSeparator() - v := systray.AddMenuItem("v"+version.NetbirdVersion(), "Client Version: "+version.NetbirdVersion()) - v.Disable() + + s.mAbout = systray.AddMenuItem("About", "About") + s.mAbout.SetIcon(s.icDisconnected) + versionString := normalizedVersion(version.NetbirdVersion()) + s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString)) + s.mVersionUI.Disable() + + s.mVersionDaemon = s.mAbout.AddSubMenuItem("", "") + s.mVersionDaemon.Disable() + s.mVersionDaemon.Hide() + + s.mUpdate = s.mAbout.AddSubMenuItem("Download latest version", "Download latest version") + s.mUpdate.Hide() + systray.AddSeparator() s.mQuit = systray.AddMenuItem("Quit", "Quit the client app") + s.update.SetOnUpdateListener(s.onUpdateAvailable) go func() { s.getSrvConfig() for { @@ -436,6 +514,11 @@ func (s *serviceClient) onTrayReady() { case <-s.mQuit.ClickedCh: systray.Quit() return + case <-s.mUpdate.ClickedCh: + err := openURL(version.DownloadUrl()) + if err != nil { + log.Errorf("%s", err) + } } if err != nil { log.Errorf("process connection: %v", err) @@ -444,6 +527,14 @@ func (s *serviceClient) onTrayReady() { }() } +func normalizedVersion(version string) string { + versionString := version + if unicode.IsDigit(rune(versionString[0])) { + versionString = fmt.Sprintf("v%s", versionString) + } + return versionString +} + func (s *serviceClient) onTrayExit() {} // getSrvClient connection to the service. @@ -504,6 +595,32 @@ func (s *serviceClient) getSrvConfig() { } } +func (s *serviceClient) onUpdateAvailable() { + s.updateIndicationLock.Lock() + defer s.updateIndicationLock.Unlock() + + s.mUpdate.Show() + s.mAbout.SetIcon(s.icUpdateCloud) + + s.isUpdateIconActive = true + systray.SetIcon(s.icUpdate) +} + +func openURL(url string) error { + var err error + switch runtime.GOOS { + case "windows": + err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() + case "darwin": + err = exec.Command("open", url).Start() + case "linux": + err = exec.Command("xdg-open", url).Start() + default: + err = fmt.Errorf("unsupported platform") + } + return err +} + // checkPIDFile exists and return error, or write new. func checkPIDFile() error { pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid") diff --git a/client/ui/connected.ico b/client/ui/connected.ico deleted file mode 100644 index 3dd598fa70df6209567ef4613b08d4274b0ca8c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 110003 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 b2LSaA9%w(j9p>mQ0RevaIt$-sio^X6IH3q4 diff --git a/client/ui/connected.png b/client/ui/connected.png deleted file mode 100644 index 64b25ade12e36b1b1f8ca695f4fac45c38cf162a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10290 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 QuO&71k2Z{ED`d}jv3NLULD{gJTl*vu{%wiJF- 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 G=KlcAE9s5^ diff --git a/client/ui/netbird-systemtray-connected.ico b/client/ui/netbird-systemtray-connected.ico new file mode 100644 index 0000000000000000000000000000000000000000..621afce9fb2a65c34e68df372aaa6770ee98c8d0 GIT binary patch literal 4452 zcmb_Ai8~Zr_cLQKNHc@ik~hPMEZHl)e3_xK#Dpl>GTwv~S+a#@(IychgJQf9r6My~ zUfYZ%WTzs@PIksVX8rX0{R`i@&wcJ$?!C`>?z!hV3jhGzAHM+zPzKn;0RY*3{E?%* zwWRnV@qJYCtPRHLxAgafi0m((VXysuQ^8^O7l6GlqjCTMIOHtG{6h4Pm0X9%&xiha z$b)l*-}Q^mPpKV|KPnjs(dmiyoH^erS$MAaQ|iD}Opd&#Q`76rbCKC@4`pN1&(Rz! zRU~vqd$3xE4$Z$bPnEJc`)2WT(m6ZxZ@1NM&y@QW#*TBw=)FSZfx+^_$vta%9MieH z*eUS79{&SqX=;w5F5->eu8ejJD*fy@sdSt+=w0DP8*288d=d-;o72;BGxXkp+07oq zx#l0DL~&DQcDE$WPz@I+c2~_HtVValiYxei;YvhZHf2sx@xHoW#=Y`$jI`?X7LBg1%VW0jstmV60=$-w3(`p9Ldf ztbkSi;f7>yJ*J{t1a5H~jA%s^ewvVolDw5dMI31zTREl}H)#US#kC@8QLe+=Vy8Or zsS|F9^xxt|=B>fvWzhf;frF|YAMsG02_xym#m ziB3Zycv)Z6TF?%P*CVs_K0AGM>O5($fis{-z*f%uG$+xEV7+59ik5x8W?iV z*JQQyMV|`K9obX3(2%M|(A@m7!|auJa12Ury>P;cQ1-(WeMJ_~!<8qrMoNjThI%_B z1+QYtoBvF()Rq&=zF`bYNTnr;8gEkIjh4d>r`y#S*0qsGx9!XO=>!k6^ti$ZKodG0 z%Hz|7c-}|-vkJzZZq=NS6~@Kmo)~ijn>x(wMjU61mFP81J_lS}@-*B1SCMv%b%(lT zyF~+w0bhM+3wn`h>zF<=o}xDOt1fYA$bDItoF7&U=`F5 zSM(;6XO-@;=Pmum-8+lCM&G-wG_-*4Fr}3*9JQ(SeBr@Pvc7?Myw||Xr z>Tn>_+`7`o#&kQw7SxKU+TYL;nNr^`2btDur{G5XtmTm~8mw+FNS)KSmI66)`$#lr zflnusIlH($FTxtRcQZR=WKEu2ojp1$3LARLkORu=-6I;{63|FH&>ce-RHBieY!qjo zw3qYaSWC9S)>r!aSLs^8$BNk5+PgkYMsa>gdnWnDbqY3Zu8gJUlzM$5)=@Gy#>_a9 zfKf(HJ4DR3#lvX8fRLKg1|8>$&L^v%`25sVKix=WQ((pxwE8Sqsk4f>W}K>JXw`aH zmt0h9vuEw4h5|J}3Y9d3>q>!njXkZ)O(BmQTPm87K67n$HR(W08mg+rl^OZ#2om;v zl`xI{XJS`VF`dq+k2aQouJ)zB?5r`?=jH{mkDCMy{=pG# zZZruRC5$Vp+pB{9CiBF7h01iF#t^OPD@0wJvqgQrs(3;Qk;-+JDQed^;yX7xlVd`L7CD6Vh`nnXc+m zR#g{xXNZg@GrEOt*A6Iv!-AQVJJL5;u{s|ul`+_vUS!Rj98e??e@frX!4$6o$X(1b0oyz%@o*c#)&D7B-C^xB4|PtoPfUIOOL7d8CZTh z`qstzRW060J3<8w7GLHE=RQHQKK+#7C*iQ*nk%yEI-o8oNDIqIo?B3qYfmMiEFn+3 zMdUT4FvDdxv{>Xytsw||Vd)j>!XONo8O-Bb9iVgHQQ|*e3OO6~_tRm3kY(L5j)EBf z=j1LeHboVPL=UgEAEZBfmXtwJC0)$Ynz0`4#l)^n*wvXU%bk|GFqke4^b`B@2d@m} z&%5F<$>+)3;LN_4#1N^Jid%&KiQ<+^_S3Nad&;<-r-iYocm!M@hr~vgq^?lMCw|gx znW`YOM8@SL`cvzep{4r`t}pi1!7x%tC;t8-ubFfhtp1H8bv7u!eKn<5G>t9I-4as< zsVEY*W2|@QR0?QfnBuqOpZBI@O5q+S_{W_EbybudFWip@4oJ`zXP*VZQwA+e>tluB ziD<7Kk2kl2xuL6VP&62%$Jx##EB<(c0?xfcr2dj@vMCaG)8hmjWGQdYX-wc!&|o1; zi+PsnZz@>5udC<$j`rHJ!y@AA`H8DJNZ15Ajo0ez5=FQ?Cjh-~_$h}5Hz77((Kt}V zit+~w_YEUE7qG>@R)Miy5Ff+rw+yaw}{6Lkp-{ zLV1KIGbOEy(PNN5pv-otMrce;MNm8e*DI0+D5D<hQ`J_>^KQZKe<214yjy}X!K$_xppA&ZFdAuN&0BP@vNS%Kt}9sZ z;1RfUYj-XNiV>3zsxBUW#4x_pYa+()-xC~JAnYxT(ajc@n3I?26+2PbFzQ6>u7)c2FktDmQ>42;g1x=%!!pKgi#u$B_povMd=F zhaIZ;sF?8-hKoKgyoXomEcjfcjO4MbR0^E$uhJ{$T>vt{f>8b?kD5lka~3^$GIlE} za9_CoamSX6!(~}O*8te}&G%3`JJ*Hz_?oNP?zB)f4CvoF-soh%JCkG+((AQ$XI*jS zmf#!xg}Vk_18*tJ z2!#@DbAs%^W_mEaU*A z>WW{Gu!4n$fDAadd-+F!7Q>^sUK;rAIk8k2{lG`2@g8J;jJY|sbq3J!m%tRiY@{Xq z!RB-K|FI>SwoE-mxIVLIURK-aG2SoteDxqIoe+8>&KT&i(4Of$X^F{im!!k6s1iU? zlwi6P6pI9vlmYkpATt(VCw<;Eu-3oQOffiXPvRp;6bY*NBBHdp;&E z2B9gsLc>zPQ9nu_7NsksaEzmQP9#hNR3c51tFnLJdYtH!`CCVc{;g?~507XBBafj& z&|fy-7_zQr?&fumZ^tS%|6~0Kou-I4&%t9wbca}*p|*}#)kbV0IxWoSbw(iQYjd`b&@Cv zPx!Re4EX4}vuO)P5)Qw{!V}uIre*V;r+dpG8j2(p7Qpv08e4?WVLa);HzW`jU**Ga z5JOE6JO}?m@GPF5{Vh-d_YhG%!vJ-EXKzsn5XbuF( z{kCn}0l<(i>V8EO?_Z}lz_)d86_iYgHpn0cUnt1S;AQASpXobdB2J=Tw*fB06TWS! z@^-(no@;{Wr`8vC%g(LnWY_vX<(UTFUhq`y#{L|<^io{0yIMzz z5kJSR(-^3)11f1F)@inHcA9V6D`sx;3Sl-Ao!TwsYuqdXP@XV|K^Gr_naeU=t(Xh6 zy+pHl0|8f$qxp#8fD3n$l&=)g3wIXJs+Nrsw!!AKHwq@1Dej7{Yglo4-|`UySNx)h`AIh?yBA2`8Jn^_5&>hU)4C? zRSI9WF5{Hx^el6naGIg zr$`*WySP%n?{WQ;`U;k9=OIcl=u5%dH zyAFqKREe~UD2;3@{4yWb(}XK4dJh>tW0y&{MEZ+mF~J9^r0bbiPJZ}U-{&`E?9E*< zrYQjfabWBP(Wn2BOnfre34!2`~4#XqYFO#Aj)y)ad5s({n)_mh-f7h*TjiL zgEwZaIC1Cu;-iPNR^S&_9~>t*@=LF*!dV%-fkPV9I?wBgrXAw$4I=g2(IfMr+jfQH z?e2c@PPZY>LNja>`d{HbTwHh(6%(IFMIgN=Z9*-Qt6iwalm;eB0E$uO- I7T)py1qRz*Pyhe` literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-connected.png b/client/ui/netbird-systemtray-connected.png new file mode 100644 index 0000000000000000000000000000000000000000..c5878d0187e9a5151814a6dfb29e311e475110b4 GIT binary patch literal 7251 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-default.ico b/client/ui/netbird-systemtray-default.ico new file mode 100644 index 0000000000000000000000000000000000000000..5a025267599c8bb00849160cc4da5dbdb4ea41ca GIT binary patch literal 2876 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-default.png b/client/ui/netbird-systemtray-default.png new file mode 100644 index 0000000000000000000000000000000000000000..12e7a2dc1008861fe1d98914b8fb44364990c7aa GIT binary patch literal 4938 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update-cloud.ico b/client/ui/netbird-systemtray-update-cloud.ico new file mode 100644 index 0000000000000000000000000000000000000000..b87c6f4b55620eea0b397d6243063e22315933f4 GIT binary patch literal 3647 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 UPWN{{D&hjh2Il(Jy7=h-0rzr)wg3PC literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update.ico b/client/ui/netbird-systemtray-update.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a1c4086d5ec7ed97f8c633b4ade786fe24fb522 GIT binary patch literal 4726 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 literal 0 HcmV?d00001 diff --git a/client/ui/netbird-systemtray-update.png b/client/ui/netbird-systemtray-update.png new file mode 100644 index 0000000000000000000000000000000000000000..1f4651df9db198901f4498d240c498c504731f40 GIT binary patch literal 7521 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 literal 0 HcmV?d00001 diff --git a/release_files/darwin-ui-installer.sh b/release_files/darwin-ui-installer.sh index 7e8115b64..5179f02d6 100644 --- a/release_files/darwin-ui-installer.sh +++ b/release_files/darwin-ui-installer.sh @@ -10,6 +10,7 @@ then wiretrustee service stop || true wiretrustee service uninstall || true fi + # check if netbird is installed NB_BIN=$(which netbird) if [ -z "$NB_BIN" ] @@ -41,4 +42,4 @@ netbird service install 2> /dev/null || true netbird service start || true # start app -open /Applications/Netbird\ UI.app \ No newline at end of file +open /Applications/Netbird\ UI.app diff --git a/release_files/darwin_pkg/preinstall b/release_files/darwin_pkg/preinstall index cdea1465c..5965e82eb 100755 --- a/release_files/darwin_pkg/preinstall +++ b/release_files/darwin_pkg/preinstall @@ -8,6 +8,13 @@ AGENT=/usr/local/bin/netbird mkdir -p /var/log/netbird/ { + # check if it was installed with brew + brew list --formula | grep netbird + if [ $? -eq 0 ] + then + echo "NetBird has been installed with Brew. Please use Brew to update the package." + exit 1 + fi osascript -e 'quit app "Netbird"' || true $AGENT service stop || true diff --git a/version/update.go b/version/update.go new file mode 100644 index 000000000..1de60ea9a --- /dev/null +++ b/version/update.go @@ -0,0 +1,184 @@ +package version + +import ( + "io" + "net/http" + "sync" + "time" + + goversion "github.com/hashicorp/go-version" + log "github.com/sirupsen/logrus" +) + +const ( + fetchPeriod = 30 * time.Minute +) + +var ( + versionURL = "https://pkgs.netbird.io/releases/latest/version" +) + +// Update fetch the version info periodically and notify the onUpdateListener in case the UI version or the +// daemon version are deprecated +type Update struct { + uiVersion *goversion.Version + daemonVersion *goversion.Version + latestAvailable *goversion.Version + versionsLock sync.Mutex + + fetchTicker *time.Ticker + fetchDone chan struct{} + + onUpdateListener func() + listenerLock sync.Mutex +} + +// NewUpdate instantiate Update and start to fetch the new version information +func NewUpdate() *Update { + currentVersion, err := goversion.NewVersion(version) + if err != nil { + currentVersion, _ = goversion.NewVersion("0.0.0") + } + + latestAvailable, _ := goversion.NewVersion("0.0.0") + + u := &Update{ + latestAvailable: latestAvailable, + uiVersion: currentVersion, + fetchTicker: time.NewTicker(fetchPeriod), + fetchDone: make(chan struct{}), + } + go u.startFetcher() + return u +} + +// StopWatch stop the version info fetch loop +func (u *Update) StopWatch() { + u.fetchTicker.Stop() + + select { + case u.fetchDone <- struct{}{}: + default: + } +} + +// SetDaemonVersion update the currently running daemon version. If new version is available it will trigger +// the onUpdateListener +func (u *Update) SetDaemonVersion(newVersion string) bool { + daemonVersion, err := goversion.NewVersion(newVersion) + if err != nil { + daemonVersion, _ = goversion.NewVersion("0.0.0") + } + + u.versionsLock.Lock() + if u.daemonVersion != nil && u.daemonVersion.Equal(daemonVersion) { + u.versionsLock.Unlock() + return false + } + + u.daemonVersion = daemonVersion + u.versionsLock.Unlock() + return u.checkUpdate() +} + +// SetOnUpdateListener set new update listener +func (u *Update) SetOnUpdateListener(updateFn func()) { + u.listenerLock.Lock() + defer u.listenerLock.Unlock() + + u.onUpdateListener = updateFn + if u.isUpdateAvailable() { + u.onUpdateListener() + } +} + +func (u *Update) startFetcher() { + changed := u.fetchVersion() + if changed { + u.checkUpdate() + } + + select { + case <-u.fetchDone: + return + case <-u.fetchTicker.C: + changed := u.fetchVersion() + if changed { + u.checkUpdate() + } + } +} + +func (u *Update) fetchVersion() bool { + resp, err := http.Get(versionURL) + if err != nil { + log.Errorf("failed to fetch version info: %s", err) + return false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Errorf("invalid status code: %d", resp.StatusCode) + return false + } + + if resp.ContentLength > 100 { + log.Errorf("too large response: %d", resp.ContentLength) + return false + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + log.Errorf("failed to read content: %s", err) + return false + } + + latestAvailable, err := goversion.NewVersion(string(content)) + if err != nil { + log.Errorf("failed to parse the version string: %s", err) + return false + } + + u.versionsLock.Lock() + defer u.versionsLock.Unlock() + + if u.latestAvailable.Equal(latestAvailable) { + return false + } + u.latestAvailable = latestAvailable + + return true +} + +func (u *Update) checkUpdate() bool { + if !u.isUpdateAvailable() { + return false + } + + u.listenerLock.Lock() + defer u.listenerLock.Unlock() + if u.onUpdateListener == nil { + return true + } + + go u.onUpdateListener() + return true +} + +func (u *Update) isUpdateAvailable() bool { + u.versionsLock.Lock() + defer u.versionsLock.Unlock() + + if u.latestAvailable.GreaterThan(u.uiVersion) { + return true + } + + if u.daemonVersion == nil { + return false + } + + if u.latestAvailable.GreaterThan(u.daemonVersion) { + return true + } + return false +} diff --git a/version/update_test.go b/version/update_test.go new file mode 100644 index 000000000..c2b477495 --- /dev/null +++ b/version/update_test.go @@ -0,0 +1,101 @@ +package version + +import ( + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" +) + +func TestNewUpdate(t *testing.T) { + version = "1.0.0" + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "10.0.0") + })) + defer svr.Close() + versionURL = svr.URL + + wg := &sync.WaitGroup{} + wg.Add(1) + + onUpdate := false + u := NewUpdate() + defer u.StopWatch() + u.SetOnUpdateListener(func() { + onUpdate = true + wg.Done() + }) + + waitTimeout(wg) + if onUpdate != true { + t.Errorf("update not found") + } +} + +func TestDoNotUpdate(t *testing.T) { + version = "11.0.0" + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "10.0.0") + })) + defer svr.Close() + versionURL = svr.URL + + wg := &sync.WaitGroup{} + wg.Add(1) + + onUpdate := false + u := NewUpdate() + defer u.StopWatch() + u.SetOnUpdateListener(func() { + onUpdate = true + wg.Done() + }) + + waitTimeout(wg) + if onUpdate == true { + t.Errorf("invalid update") + } +} + +func TestDaemonUpdate(t *testing.T) { + version = "11.0.0" + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "11.0.0") + })) + defer svr.Close() + versionURL = svr.URL + + wg := &sync.WaitGroup{} + wg.Add(1) + + onUpdate := false + u := NewUpdate() + defer u.StopWatch() + u.SetOnUpdateListener(func() { + onUpdate = true + wg.Done() + }) + + u.SetDaemonVersion("10.0.0") + + waitTimeout(wg) + if onUpdate != true { + t.Errorf("invalid dameon version check") + } +} + +func waitTimeout(wg *sync.WaitGroup) { + c := make(chan struct{}) + go func() { + wg.Wait() + close(c) + }() + select { + case <-c: + return + case <-time.After(time.Second): + return + } +} diff --git a/version/url.go b/version/url.go new file mode 100644 index 000000000..ed43ab042 --- /dev/null +++ b/version/url.go @@ -0,0 +1,5 @@ +package version + +const ( + downloadURL = "https://app.netbird.io/install" +) diff --git a/version/url_darwin.go b/version/url_darwin.go new file mode 100644 index 000000000..cb58612f5 --- /dev/null +++ b/version/url_darwin.go @@ -0,0 +1,33 @@ +package version + +import ( + "os/exec" + "runtime" +) + +const ( + urlMacIntel = "https://pkgs.netbird.io/macos/amd64" + urlMacM1M2 = "https://pkgs.netbird.io/macos/arm64" +) + +// DownloadUrl return with the proper download link +func DownloadUrl() string { + cmd := exec.Command("brew", "list --formula | grep -i netbird") + if err := cmd.Start(); err != nil { + goto PKGINSTALL + } + + if err := cmd.Wait(); err == nil { + return downloadURL + } + +PKGINSTALL: + switch runtime.GOARCH { + case "amd64": + return urlMacIntel + case "arm64": + return urlMacM1M2 + default: + return downloadURL + } +} diff --git a/version/url_linux.go b/version/url_linux.go new file mode 100644 index 000000000..c8193e30c --- /dev/null +++ b/version/url_linux.go @@ -0,0 +1,6 @@ +package version + +// DownloadUrl return with the proper download link +func DownloadUrl() string { + return downloadURL +} diff --git a/version/url_windows.go b/version/url_windows.go new file mode 100644 index 000000000..f2055b109 --- /dev/null +++ b/version/url_windows.go @@ -0,0 +1,19 @@ +package version + +import "golang.org/x/sys/windows/registry" + +const ( + urlWinExe = "https://pkgs.netbird.io/windows/x64" +) + +var regKeyAppPath = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\Netbird" + +// DownloadUrl return with the proper download link +func DownloadUrl() string { + _, err := registry.OpenKey(registry.LOCAL_MACHINE, regKeyAppPath, registry.QUERY_VALUE) + if err == nil { + return urlWinExe + } else { + return downloadURL + } +} From c38d65ef4caedc33d81442cd7422ebb160554dad Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Wed, 1 Nov 2023 13:04:17 +0300 Subject: [PATCH 33/43] Extends management user and group structure (#1268) * extends user and group structure by introducing fields for issued and integration references * Add integration checks to group management to prevent groups added by integration. * Add integration checks to user management to prevent deleting user added by integration. * Fix broken user update tests * Initialize all user fields for testing * Change a serializer option to embedded for IntegrationReference in user and group models * Add issued field to user api response * Add IntegrationReference to Group in update groups handler * Set the default issued field for users in file store --- management/server/account.go | 21 ++++--- management/server/file_store.go | 5 ++ management/server/group.go | 7 +++ management/server/group_test.go | 64 ++++++++++++-------- management/server/http/api/openapi.yml | 4 ++ management/server/http/api/types.gen.go | 3 + management/server/http/groups_handler.go | 9 +-- management/server/http/users_handler.go | 18 ++++-- management/server/http/users_handler_test.go | 5 ++ management/server/user.go | 62 ++++++++++++++----- management/server/user_test.go | 51 +++++++++++++++- 11 files changed, 188 insertions(+), 61 deletions(-) diff --git a/management/server/account.go b/management/server/account.go index 9ca44615a..30a9bd200 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -36,6 +36,7 @@ const ( UnknownCategory = "unknown" GroupIssuedAPI = "api" GroupIssuedJWT = "jwt" + GroupIssuedIntegration = "integration" CacheExpirationMax = 7 * 24 * 3600 * time.Second // 7 days CacheExpirationMin = 3 * 24 * 3600 * time.Second // 3 days DefaultPeerLoginExpiration = 24 * time.Hour @@ -195,15 +196,17 @@ type Account struct { } type UserInfo struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - Role string `json:"role"` - AutoGroups []string `json:"auto_groups"` - Status string `json:"-"` - IsServiceUser bool `json:"is_service_user"` - IsBlocked bool `json:"is_blocked"` - LastLogin time.Time `json:"last_login"` + ID string `json:"id"` + Email string `json:"email"` + Name string `json:"name"` + Role string `json:"role"` + AutoGroups []string `json:"auto_groups"` + Status string `json:"-"` + IsServiceUser bool `json:"is_service_user"` + IsBlocked bool `json:"is_blocked"` + LastLogin time.Time `json:"last_login"` + Issued string `json:"issued"` + IntegrationReference IntegrationReference `json:"-"` } // getRoutesToSync returns the enabled routes for the peer ID and the routes diff --git a/management/server/file_store.go b/management/server/file_store.go index 0bd137b42..73c52927e 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -133,6 +133,11 @@ func restore(file string) (*FileStore, error) { } for _, user := range account.Users { store.UserID2AccountID[user.Id] = accountID + if user.Issued == "" { + user.Issued = UserIssuedAPI + account.Users[user.Id] = user + } + for _, pat := range user.PATs { store.TokenID2UserID[pat.ID] = user.Id store.HashedPAT2TokenID[pat.HashedToken] = pat.ID diff --git a/management/server/group.go b/management/server/group.go index 28606e02d..201a9cf8e 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -34,6 +34,8 @@ type Group struct { // Peers list of the group Peers []string `gorm:"serializer:json"` + + IntegrationReference IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"` } // EventMeta returns activity event meta related to the group @@ -160,6 +162,11 @@ func (am *DefaultAccountManager) DeleteGroup(accountId, userId, groupID string) return nil } + // check integration link + if g.Issued == GroupIssuedIntegration { + return &GroupLinkError{GroupIssuedIntegration, g.IntegrationReference.String()} + } + // check route links for _, r := range account.Routes { for _, g := range r.Groups { diff --git a/management/server/group_test.go b/management/server/group_test.go index e300fe7fb..5db0ca900 100644 --- a/management/server/group_test.go +++ b/management/server/group_test.go @@ -52,6 +52,11 @@ func TestDefaultAccountManager_DeleteGroup(t *testing.T) { "grp-for-users", "user", }, + { + "integration", + "grp-for-integration", + "integration", + }, } for _, testCase := range testCases { @@ -79,43 +84,51 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { domain := "example.com" groupForRoute := &Group{ - "grp-for-route", - "account-id", - "Group for route", - GroupIssuedAPI, - make([]string, 0), + ID: "grp-for-route", + AccountID: "account-id", + Name: "Group for route", + Issued: GroupIssuedAPI, + Peers: make([]string, 0), } groupForNameServerGroups := &Group{ - "grp-for-name-server-grp", - "account-id", - "Group for name server groups", - GroupIssuedAPI, - make([]string, 0), + ID: "grp-for-name-server-grp", + AccountID: "account-id", + Name: "Group for name server groups", + Issued: GroupIssuedAPI, + Peers: make([]string, 0), } groupForPolicies := &Group{ - "grp-for-policies", - "account-id", - "Group for policies", - GroupIssuedAPI, - make([]string, 0), + ID: "grp-for-policies", + AccountID: "account-id", + Name: "Group for policies", + Issued: GroupIssuedAPI, + Peers: make([]string, 0), } groupForSetupKeys := &Group{ - "grp-for-keys", - "account-id", - "Group for setup keys", - GroupIssuedAPI, - make([]string, 0), + ID: "grp-for-keys", + AccountID: "account-id", + Name: "Group for setup keys", + Issued: GroupIssuedAPI, + Peers: make([]string, 0), } groupForUsers := &Group{ - "grp-for-users", - "account-id", - "Group for users", - GroupIssuedAPI, - make([]string, 0), + ID: "grp-for-users", + AccountID: "account-id", + Name: "Group for users", + Issued: GroupIssuedAPI, + Peers: make([]string, 0), + } + + groupForIntegration := &Group{ + ID: "grp-for-integration", + AccountID: "account-id", + Name: "Group for users", + Issued: GroupIssuedIntegration, + Peers: make([]string, 0), } routeResource := &route.Route{ @@ -164,6 +177,7 @@ func initTestGroupAccount(am *DefaultAccountManager) (*Account, error) { _ = am.SaveGroup(accountID, groupAdminUserID, groupForPolicies) _ = am.SaveGroup(accountID, groupAdminUserID, groupForSetupKeys) _ = am.SaveGroup(accountID, groupAdminUserID, groupForUsers) + _ = am.SaveGroup(accountID, groupAdminUserID, groupForIntegration) return am.Store.GetAccount(account.Id) } diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml index 30e55571c..a0a64fd98 100644 --- a/management/server/http/api/openapi.yml +++ b/management/server/http/api/openapi.yml @@ -125,6 +125,10 @@ components: description: Is true if this user is blocked. Blocked users can't use the system type: boolean example: false + issued: + description: How user was issued by API or Integration + type: string + example: api required: - id - email diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go index cf9b0892e..ddf8ce65f 100644 --- a/management/server/http/api/types.gen.go +++ b/management/server/http/api/types.gen.go @@ -791,6 +791,9 @@ type User struct { // IsServiceUser Is true if this user is a service user IsServiceUser *bool `json:"is_service_user,omitempty"` + // Issued How user was issued by API or Integration + Issued *string `json:"issued,omitempty"` + // LastLogin Last time this user performed a login to the dashboard LastLogin *time.Time `json:"last_login,omitempty"` diff --git a/management/server/http/groups_handler.go b/management/server/http/groups_handler.go index d409623df..c58916250 100644 --- a/management/server/http/groups_handler.go +++ b/management/server/http/groups_handler.go @@ -107,10 +107,11 @@ func (h *GroupsHandler) UpdateGroup(w http.ResponseWriter, r *http.Request) { peers = *req.Peers } group := server.Group{ - ID: groupID, - Name: req.Name, - Peers: peers, - Issued: eg.Issued, + ID: groupID, + Name: req.Name, + Peers: peers, + Issued: eg.Issued, + IntegrationReference: eg.IntegrationReference, } if err := h.accountManager.SaveGroup(account.Id, user.Id, &group); err != nil { diff --git a/management/server/http/users_handler.go b/management/server/http/users_handler.go index d215e1510..c54e07052 100644 --- a/management/server/http/users_handler.go +++ b/management/server/http/users_handler.go @@ -54,6 +54,12 @@ func (h *UsersHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { return } + existingUser, ok := account.Users[userID] + if !ok { + util.WriteError(status.Errorf(status.NotFound, "couldn't find user with ID %s", userID), w) + return + } + req := &api.PutApiUsersUserIdJSONRequestBody{} err = json.NewDecoder(r.Body).Decode(&req) if err != nil { @@ -73,10 +79,12 @@ func (h *UsersHandler) UpdateUser(w http.ResponseWriter, r *http.Request) { } newUser, err := h.accountManager.SaveUser(account.Id, user.Id, &server.User{ - Id: userID, - Role: userRole, - AutoGroups: req.AutoGroups, - Blocked: req.IsBlocked, + Id: userID, + Role: userRole, + AutoGroups: req.AutoGroups, + Blocked: req.IsBlocked, + Issued: existingUser.Issued, + IntegrationReference: existingUser.IntegrationReference, }) if err != nil { @@ -153,6 +161,7 @@ func (h *UsersHandler) CreateUser(w http.ResponseWriter, r *http.Request) { Role: req.Role, AutoGroups: req.AutoGroups, IsServiceUser: req.IsServiceUser, + Issued: server.UserIssuedAPI, }) if err != nil { util.WriteError(err, w) @@ -271,5 +280,6 @@ func toUserResponse(user *server.UserInfo, currenUserID string) *api.User { IsServiceUser: &user.IsServiceUser, IsBlocked: user.IsBlocked, LastLogin: &user.LastLogin, + Issued: &user.Issued, } } diff --git a/management/server/http/users_handler_test.go b/management/server/http/users_handler_test.go index a56507145..b4d449be3 100644 --- a/management/server/http/users_handler_test.go +++ b/management/server/http/users_handler_test.go @@ -33,18 +33,21 @@ var usersTestAccount = &server.Account{ Role: "admin", IsServiceUser: false, AutoGroups: []string{"group_1"}, + Issued: server.UserIssuedAPI, }, regularUserID: { Id: regularUserID, Role: "user", IsServiceUser: false, AutoGroups: []string{"group_1"}, + Issued: server.UserIssuedAPI, }, serviceUserID: { Id: serviceUserID, Role: "user", IsServiceUser: true, AutoGroups: []string{"group_1"}, + Issued: server.UserIssuedAPI, }, }, } @@ -64,6 +67,7 @@ func initUsersTestData() *UsersHandler { Name: "", Email: "", IsServiceUser: v.IsServiceUser, + Issued: v.Issued, }) } return users, nil @@ -170,6 +174,7 @@ func TestGetUsers(t *testing.T) { assert.Equal(t, v.ID, usersTestAccount.Users[v.ID].Id) assert.Equal(t, v.Role, string(usersTestAccount.Users[v.ID].Role)) assert.Equal(t, v.IsServiceUser, usersTestAccount.Users[v.ID].IsServiceUser) + assert.Equal(t, v.Issued, usersTestAccount.Users[v.ID].Issued) } }) } diff --git a/management/server/user.go b/management/server/user.go index 6093d93a2..d3e7b3060 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -22,6 +22,9 @@ const ( UserStatusActive UserStatus = "active" UserStatusDisabled UserStatus = "disabled" UserStatusInvited UserStatus = "invited" + + UserIssuedAPI = "api" + UserIssuedIntegration = "integration" ) // StrRoleToUserRole returns UserRole for a given strRole or UserRoleUnknown if the specified role is unknown @@ -42,6 +45,16 @@ type UserStatus string // UserRole is the role of a User type UserRole string +// IntegrationReference holds the reference to a particular integration +type IntegrationReference struct { + ID int + IntegrationType string +} + +func (ir IntegrationReference) String() string { + return fmt.Sprintf("%d:%s", ir.ID, ir.IntegrationType) +} + // User represents a user of the system type User struct { Id string `gorm:"primaryKey"` @@ -59,6 +72,11 @@ type User struct { Blocked bool // LastLogin is the last time the user logged in to IdP LastLogin time.Time + + // Issued of the user + Issued string `gorm:"default:api"` + + IntegrationReference IntegrationReference `gorm:"embedded;embeddedPrefix:integration_ref_"` } // IsBlocked returns true if the user is blocked, false otherwise @@ -93,6 +111,7 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) { IsServiceUser: u.IsServiceUser, IsBlocked: u.Blocked, LastLogin: u.LastLogin, + Issued: u.Issued, }, nil } if userData.ID != u.Id { @@ -114,6 +133,7 @@ func (u *User) ToUserInfo(userData *idp.UserData) (*UserInfo, error) { IsServiceUser: u.IsServiceUser, IsBlocked: u.Blocked, LastLogin: u.LastLogin, + Issued: u.Issued, }, nil } @@ -126,37 +146,40 @@ func (u *User) Copy() *User { pats[k] = v.Copy() } return &User{ - Id: u.Id, - AccountID: u.AccountID, - Role: u.Role, - AutoGroups: autoGroups, - IsServiceUser: u.IsServiceUser, - ServiceUserName: u.ServiceUserName, - PATs: pats, - Blocked: u.Blocked, - LastLogin: u.LastLogin, + Id: u.Id, + AccountID: u.AccountID, + Role: u.Role, + AutoGroups: autoGroups, + IsServiceUser: u.IsServiceUser, + ServiceUserName: u.ServiceUserName, + PATs: pats, + Blocked: u.Blocked, + LastLogin: u.LastLogin, + Issued: u.Issued, + IntegrationReference: u.IntegrationReference, } } // NewUser creates a new user -func NewUser(id string, role UserRole, isServiceUser bool, serviceUserName string, autoGroups []string) *User { +func NewUser(id string, role UserRole, isServiceUser bool, serviceUserName string, autoGroups []string, issued string) *User { return &User{ Id: id, Role: role, IsServiceUser: isServiceUser, ServiceUserName: serviceUserName, AutoGroups: autoGroups, + Issued: issued, } } // NewRegularUser creates a new user with role UserRoleUser func NewRegularUser(id string) *User { - return NewUser(id, UserRoleUser, false, "", []string{}) + return NewUser(id, UserRoleUser, false, "", []string{}, UserIssuedAPI) } // NewAdminUser creates a new user with role UserRoleAdmin func NewAdminUser(id string) *User { - return NewUser(id, UserRoleAdmin, false, "", []string{}) + return NewUser(id, UserRoleAdmin, false, "", []string{}, UserIssuedAPI) } // createServiceUser creates a new service user under the given account. @@ -178,7 +201,7 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs } newUserID := uuid.New().String() - newUser := NewUser(newUserID, role, true, serviceUserName, autoGroups) + newUser := NewUser(newUserID, role, true, serviceUserName, autoGroups, UserIssuedAPI) log.Debugf("New User: %v", newUser) account.Users[newUserID] = newUser @@ -199,6 +222,7 @@ func (am *DefaultAccountManager) createServiceUser(accountID string, initiatorUs Status: string(UserStatusActive), IsServiceUser: true, LastLogin: time.Time{}, + Issued: UserIssuedAPI, }, nil } @@ -270,9 +294,11 @@ func (am *DefaultAccountManager) inviteNewUser(accountID, userID string, invite role := StrRoleToUserRole(invite.Role) newUser := &User{ - Id: idpUser.ID, - Role: role, - AutoGroups: invite.AutoGroups, + Id: idpUser.ID, + Role: role, + AutoGroups: invite.AutoGroups, + Issued: invite.Issued, + IntegrationReference: invite.IntegrationReference, } account.Users[idpUser.ID] = newUser @@ -361,6 +387,10 @@ func (am *DefaultAccountManager) DeleteUser(accountID, initiatorUserID string, t return status.Errorf(status.NotFound, "target user not found") } + if targetUser.Issued == UserIssuedIntegration { + return status.Errorf(status.PermissionDenied, "only integration can delete this user") + } + // handle service user first and exit, no need to fetch extra data from IDP, etc if targetUser.IsServiceUser { am.deleteServiceUser(account, initiatorUserID, targetUser) diff --git a/management/server/user_test.go b/management/server/user_test.go index fdaffc693..f1b997186 100644 --- a/management/server/user_test.go +++ b/management/server/user_test.go @@ -269,6 +269,11 @@ func TestUser_Copy(t *testing.T) { }, Blocked: false, LastLogin: time.Now(), + Issued: "test", + IntegrationReference: IntegrationReference{ + ID: 0, + IntegrationType: "test", + }, } err := validateStruct(user) @@ -453,12 +458,25 @@ func TestUser_DeleteUser_SelfDelete(t *testing.T) { func TestUser_DeleteUser_regularUser(t *testing.T) { store := newStore(t) account := newAccountWithId(mockAccountID, mockUserID, "") + targetId := "user2" account.Users[targetId] = &User{ Id: targetId, IsServiceUser: true, ServiceUserName: "user2username", } + targetId = "user3" + account.Users[targetId] = &User{ + Id: targetId, + IsServiceUser: false, + Issued: UserIssuedAPI, + } + targetId = "user4" + account.Users[targetId] = &User{ + Id: targetId, + IsServiceUser: false, + Issued: UserIssuedIntegration, + } err := store.SaveAccount(account) if err != nil { @@ -470,10 +488,37 @@ func TestUser_DeleteUser_regularUser(t *testing.T) { eventStore: &activity.InMemoryEventStore{}, } - err = am.DeleteUser(mockAccountID, mockUserID, targetId) - if err != nil { - t.Errorf("unexpected error: %s", err) + testCases := []struct { + name string + userID string + assertErrFunc assert.ErrorAssertionFunc + assertErrMessage string + }{ + { + name: "Delete service user successfully ", + userID: "user2", + assertErrFunc: assert.NoError, + }, + { + name: "Delete regular user successfully ", + userID: "user3", + assertErrFunc: assert.NoError, + }, + { + name: "Delete integration regular user permission denied ", + userID: "user4", + assertErrFunc: assert.Error, + assertErrMessage: "only integration can delete this user", + }, } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err = am.DeleteUser(mockAccountID, mockUserID, testCase.userID) + testCase.assertErrFunc(t, err, testCase.assertErrMessage) + }) + } + } func TestDefaultAccountManager_GetUser(t *testing.T) { From 8843784312c5c535fd11e768395f9c6fb39f677d Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Wed, 1 Nov 2023 12:00:47 +0100 Subject: [PATCH 34/43] Remove logging of User names on Debug level (#1275) --- management/server/http/users_handler.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/management/server/http/users_handler.go b/management/server/http/users_handler.go index c54e07052..0441e8cc0 100644 --- a/management/server/http/users_handler.go +++ b/management/server/http/users_handler.go @@ -207,9 +207,7 @@ func (h *UsersHandler) GetAllUsers(w http.ResponseWriter, r *http.Request) { util.WriteError(status.Errorf(status.InvalidArgument, "invalid service_user query parameter"), w) return } - log.Debugf("User %v is service user: %v", r.Name, r.IsServiceUser) if includeServiceUser == r.IsServiceUser { - log.Debugf("Found service user: %v", r.Name) users = append(users, toUserResponse(r, claims.UserId)) } } From c99ae6f0099d0ac69ca084f06bf57b8c2c65f451 Mon Sep 17 00:00:00 2001 From: Fabio Fantoni Date: Wed, 1 Nov 2023 17:11:16 +0100 Subject: [PATCH 35/43] fix some typo spotted with codespell (#1278) Fixed spelling typos on logs, comments and command help text --- CONTRIBUTING.md | 2 +- client/android/preferences_test.go | 8 ++++---- client/cmd/up.go | 8 ++++---- client/firewall/iptables/manager_linux.go | 4 ++-- client/firewall/nftables/manager_linux.go | 2 +- client/firewall/nftables/manager_linux_test.go | 2 +- client/firewall/uspfilter/uspfilter.go | 4 ++-- client/internal/acl/manager.go | 6 +++--- client/internal/connect.go | 2 +- client/internal/dns/file_linux.go | 2 +- client/internal/dns/resolvconf_linux.go | 2 +- client/internal/dns/server.go | 4 ++-- client/internal/dns/server_export_test.go | 2 +- client/internal/dns/upstream.go | 2 +- client/internal/routemanager/manager.go | 2 +- client/internal/routemanager/nftables_linux.go | 2 +- encryption/encryption.go | 2 +- iface/bind/udp_mux_universal.go | 2 +- iface/device_wrapper_test.go | 8 ++++---- iface/iface_test.go | 2 +- iface/module_linux.go | 6 +++--- iface/tun_darwin.go | 2 +- infrastructure_files/base.setup.env | 4 ++-- infrastructure_files/configure.sh | 2 +- infrastructure_files/turnserver.conf.tmpl | 2 +- management/cmd/management.go | 2 +- management/cmd/root.go | 2 +- management/server/account_test.go | 2 +- management/server/dns.go | 2 +- management/server/grpcserver.go | 2 +- management/server/http/accounts_handler_test.go | 4 ++-- management/server/http/util/util.go | 2 +- management/server/peer.go | 2 +- management/server/policy.go | 2 +- management/server/policy_test.go | 4 ++-- management/server/user.go | 2 +- release_files/install.sh | 2 +- version/update_test.go | 2 +- 38 files changed, 57 insertions(+), 57 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d31de651..80be72fa9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,7 +144,7 @@ cd client CGO_ENABLED=0 go build . ``` -> Windows clients have a Wireguard driver requirement. You can downlowd the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to the same path as your binary file or to `C:\Windows\System32\wintun.dll`. +> Windows clients have a Wireguard driver requirement. You can download the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to the same path as your binary file or to `C:\Windows\System32\wintun.dll`. To start NetBird the client in the foreground: diff --git a/client/android/preferences_test.go b/client/android/preferences_test.go index 73c8692d7..985175913 100644 --- a/client/android/preferences_test.go +++ b/client/android/preferences_test.go @@ -57,11 +57,11 @@ func TestPreferences_ReadUncommitedValues(t *testing.T) { p.SetManagementURL(exampleString) resp, err = p.GetManagementURL() if err != nil { - t.Fatalf("failed to read managmenet url: %s", err) + t.Fatalf("failed to read management url: %s", err) } if resp != exampleString { - t.Errorf("unexpected managemenet url: %s", resp) + t.Errorf("unexpected management url: %s", resp) } p.SetPreSharedKey(exampleString) @@ -102,11 +102,11 @@ func TestPreferences_Commit(t *testing.T) { resp, err = p.GetManagementURL() if err != nil { - t.Fatalf("failed to read managmenet url: %s", err) + t.Fatalf("failed to read management url: %s", err) } if resp != exampleURL { - t.Errorf("unexpected managemenet url: %s", resp) + t.Errorf("unexpected management url: %s", resp) } resp, err = p.GetPreSharedKey() diff --git a/client/cmd/up.go b/client/cmd/up.go index 8d682c46b..80ed04b57 100644 --- a/client/cmd/up.go +++ b/client/cmd/up.go @@ -123,7 +123,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { defer func() { err := conn.Close() if err != nil { - log.Warnf("failed closing dameon gRPC client connection %v", err) + log.Warnf("failed closing daemon gRPC client connection %v", err) return } }() @@ -200,11 +200,11 @@ func validateNATExternalIPs(list []string) error { subElements := strings.Split(element, "/") if len(subElements) > 2 { - return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"String\" or \"String/String\"", element, externalIPMapFlag) + return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"String\" or \"String/String\"", element, externalIPMapFlag) } if len(subElements) == 1 && !isValidIP(subElements[0]) { - return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag) + return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag) } last := 0 @@ -259,7 +259,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) { var parsed []byte if modified { if !isValidAddrPort(customDNSAddress) { - return nil, fmt.Errorf("%s is invalid, it should be formated as IP:Port string or as an empty string like \"\"", customDNSAddress) + return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress) } if customDNSAddress == "" && logFile != "console" { parsed = []byte("empty") diff --git a/client/firewall/iptables/manager_linux.go b/client/firewall/iptables/manager_linux.go index 048c0fd50..4ce904df6 100644 --- a/client/firewall/iptables/manager_linux.go +++ b/client/firewall/iptables/manager_linux.go @@ -192,7 +192,7 @@ func (m *Manager) AddFiltering( } if ipsetName != "" { // ipset name is defined and it means that this rule was created - // for it, need to assosiate it with ruleset + // for it, need to associate it with ruleset m.rulesets[ipsetName] = ruleset{ rule: rule, ips: map[string]string{rule.ip: ruleID}, @@ -236,7 +236,7 @@ func (m *Manager) DeleteRule(rule fw.Rule) error { } // we delete last IP from the set, that means we need to delete - // set itself and assosiated firewall rule too + // set itself and associated firewall rule too delete(m.rulesets, r.ipsetName) if err := ipset.Destroy(r.ipsetName); err != nil { diff --git a/client/firewall/nftables/manager_linux.go b/client/firewall/nftables/manager_linux.go index 2273f4edc..6c46048b4 100644 --- a/client/firewall/nftables/manager_linux.go +++ b/client/firewall/nftables/manager_linux.go @@ -754,7 +754,7 @@ func (m *Manager) AllowNetbird() error { } if chain == nil { - log.Debugf("chain INPUT not found. Skiping add allow netbird rule") + log.Debugf("chain INPUT not found. Skipping add allow netbird rule") return nil } diff --git a/client/firewall/nftables/manager_linux_test.go b/client/firewall/nftables/manager_linux_test.go index 164d5d0dc..0a5c499b2 100644 --- a/client/firewall/nftables/manager_linux_test.go +++ b/client/firewall/nftables/manager_linux_test.go @@ -148,7 +148,7 @@ func TestNftablesManager(t *testing.T) { // test expectations: // 1) "accept extra routed traffic rule" for the interface // 2) "drop all rule" for the interface - require.Len(t, rules, 2, "expected 2 rules after deleteion") + require.Len(t, rules, 2, "expected 2 rules after deletion") err = manager.Reset() require.NoError(t, err, "failed to reset") diff --git a/client/firewall/uspfilter/uspfilter.go b/client/firewall/uspfilter/uspfilter.go index 50170b46c..6fd11e652 100644 --- a/client/firewall/uspfilter/uspfilter.go +++ b/client/firewall/uspfilter/uspfilter.go @@ -32,7 +32,7 @@ type Manager struct { wgNetwork *net.IPNet decoders sync.Pool wgIface IFaceMapper - resetHook func() error + resetHook func() error mutex sync.RWMutex } @@ -188,7 +188,7 @@ func (m *Manager) DropIncoming(packetData []byte) bool { return m.dropFilter(packetData, m.incomingRules, true) } -// dropFilter imlements same logic for booth direction of the traffic +// dropFilter implements same logic for booth direction of the traffic func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet, isIncomingPacket bool) bool { m.mutex.RLock() defer m.mutex.RUnlock() diff --git a/client/internal/acl/manager.go b/client/internal/acl/manager.go index 9a9e624d6..feaaa7b8b 100644 --- a/client/internal/acl/manager.go +++ b/client/internal/acl/manager.go @@ -53,7 +53,7 @@ func newDefaultManager(fm firewall.Manager) *DefaultManager { // ApplyFiltering firewall rules to the local firewall manager processed by ACL policy. // -// If allowByDefault is ture it appends allow ALL traffic rules to input and output chains. +// If allowByDefault is true it appends allow ALL traffic rules to input and output chains. func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) { d.mutex.Lock() defer d.mutex.Unlock() @@ -366,7 +366,7 @@ func (d *DefaultManager) squashAcceptRules( protocols[r.Protocol] = map[string]int{} } - // special case, when we recieve this all network IP address + // special case, when we receive this all network IP address // it means that rules for that protocol was already optimized on the // management side if r.PeerIP == "0.0.0.0" { @@ -393,7 +393,7 @@ func (d *DefaultManager) squashAcceptRules( } // order of squashing by protocol is important - // only for ther first element ALL, it must be done first + // only for their first element ALL, it must be done first protocolOrders := []mgmProto.FirewallRuleProtocol{ mgmProto.FirewallRule_ALL, mgmProto.FirewallRule_ICMP, diff --git a/client/internal/connect.go b/client/internal/connect.go index 66946c4aa..97fe2cc59 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -99,7 +99,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, cancel() }() - log.Debugf("conecting to the Management service %s", config.ManagementURL.Host) + log.Debugf("connecting to the Management service %s", config.ManagementURL.Host) mgmClient, err := mgm.NewClient(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled) if err != nil { return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err)) diff --git a/client/internal/dns/file_linux.go b/client/internal/dns/file_linux.go index 0172e3413..de6e8b899 100644 --- a/client/internal/dns/file_linux.go +++ b/client/internal/dns/file_linux.go @@ -69,7 +69,7 @@ func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error { } default: // todo improve this and maybe restart DNS manager from scratch - return fmt.Errorf("something happened and file manager is not your prefered host dns configurator, restart the agent") + return fmt.Errorf("something happened and file manager is not your preferred host dns configurator, restart the agent") } var searchDomains string diff --git a/client/internal/dns/resolvconf_linux.go b/client/internal/dns/resolvconf_linux.go index b358d3bd5..e0f72b4bf 100644 --- a/client/internal/dns/resolvconf_linux.go +++ b/client/internal/dns/resolvconf_linux.go @@ -89,7 +89,7 @@ func (r *resolvconf) applyConfig(content string) error { cmd.Stdin = strings.NewReader(content) _, err := cmd.Output() if err != nil { - return fmt.Errorf("got an error while appying resolvconf configuration for %s interface, error: %s", r.ifaceName, err) + return fmt.Errorf("got an error while applying resolvconf configuration for %s interface, error: %s", r.ifaceName, err) } return nil } diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 31946c13e..600b16a6e 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -288,7 +288,7 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam handler := newUpstreamResolver(s.ctx) for _, ns := range nsGroup.NameServers { if ns.NSType != nbdns.UDPNameServerType { - log.Warnf("skiping nameserver %s with type %s, this peer supports only %s", + log.Warnf("skipping nameserver %s with type %s, this peer supports only %s", ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String()) continue } @@ -306,7 +306,7 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam // reapply DNS settings, but it not touch the original configuration and serial number // because it is temporal deactivation until next try // - // after some period defined by upstream it trys to reactivate self by calling this hook + // after some period defined by upstream it tries to reactivate self by calling this hook // everything we need here is just to re-apply current configuration because it already // contains this upstream settings (temporal deactivation not removed it) handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler) diff --git a/client/internal/dns/server_export_test.go b/client/internal/dns/server_export_test.go index 784dcb3ad..1fa343b52 100644 --- a/client/internal/dns/server_export_test.go +++ b/client/internal/dns/server_export_test.go @@ -19,6 +19,6 @@ func TestGetServerDns(t *testing.T) { } if srvB != srv { - t.Errorf("missmatch dns instances") + t.Errorf("mismatch dns instances") } } diff --git a/client/internal/dns/upstream.go b/client/internal/dns/upstream.go index b4febd7a4..d19ac265e 100644 --- a/client/internal/dns/upstream.go +++ b/client/internal/dns/upstream.go @@ -53,7 +53,7 @@ func newUpstreamResolver(parentCTX context.Context) *upstreamResolver { } func (u *upstreamResolver) stop() { - log.Debugf("stoping serving DNS for upstreams %s", u.upstreamServers) + log.Debugf("stopping serving DNS for upstreams %s", u.upstreamServers) u.cancel() } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index b31fe6327..05b725045 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -155,7 +155,7 @@ func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string] // if prefix is too small, lets assume is a possible default route which is not yet supported // we skip this route management if newRoute.Network.Bits() < 7 { - log.Errorf("this agent version: %s, doesn't support default routes, received %s, skiping this route", + log.Errorf("this agent version: %s, doesn't support default routes, received %s, skipping this route", version.NetbirdVersion(), newRoute.Network) continue } diff --git a/client/internal/routemanager/nftables_linux.go b/client/internal/routemanager/nftables_linux.go index d6eac96a6..e62b1a404 100644 --- a/client/internal/routemanager/nftables_linux.go +++ b/client/internal/routemanager/nftables_linux.go @@ -487,7 +487,7 @@ func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error { if len(n.rules) == 2 && n.defaultForwardRules[0] != nil { err := n.eraseDefaultForwardRule() if err != nil { - log.Errorf("failed to delte default fwd rule: %s", err) + log.Errorf("failed to delete default fwd rule: %s", err) } } diff --git a/encryption/encryption.go b/encryption/encryption.go index 1c6ec7806..abdf4cb2f 100644 --- a/encryption/encryption.go +++ b/encryption/encryption.go @@ -30,7 +30,7 @@ func Decrypt(encryptedMsg []byte, peerPublicKey wgtypes.Key, privateKey wgtypes. return nil, err } if len(encryptedMsg) < nonceSize { - return nil, fmt.Errorf("invalid encrypted message lenght") + return nil, fmt.Errorf("invalid encrypted message length") } copy(nonce[:], encryptedMsg[:nonceSize]) opened, ok := box.Open(nil, encryptedMsg[nonceSize:], nonce, toByte32(peerPublicKey), toByte32(privateKey)) diff --git a/iface/bind/udp_mux_universal.go b/iface/bind/udp_mux_universal.go index 9792bba55..772f02b35 100644 --- a/iface/bind/udp_mux_universal.go +++ b/iface/bind/udp_mux_universal.go @@ -282,7 +282,7 @@ func (a *xorMapped) closeWaiters() { // just exit break default: - // notify tha twe have a new addr + // notify that twe have a new addr close(a.waitAddrReceived) } } diff --git a/iface/device_wrapper_test.go b/iface/device_wrapper_test.go index 9f5386587..2d3725ea4 100644 --- a/iface/device_wrapper_test.go +++ b/iface/device_wrapper_test.go @@ -59,7 +59,7 @@ func TestDeviceWrapperRead(t *testing.T) { n, err := wrapped.Read(bufs, sizes, offset) if err != nil { - t.Errorf("unexpeted error: %v", err) + t.Errorf("unexpected error: %v", err) return } if n != 1 { @@ -105,7 +105,7 @@ func TestDeviceWrapperRead(t *testing.T) { n, err := wrapped.Write(bufs, 0) if err != nil { - t.Errorf("unexpeted error: %v", err) + t.Errorf("unexpected error: %v", err) return } if n != 1 { @@ -154,7 +154,7 @@ func TestDeviceWrapperRead(t *testing.T) { n, err := wrapped.Write(bufs, 0) if err != nil { - t.Errorf("unexpeted error: %v", err) + t.Errorf("unexpected error: %v", err) return } if n != 0 { @@ -211,7 +211,7 @@ func TestDeviceWrapperRead(t *testing.T) { n, err := wrapped.Read(bufs, sizes, offset) if err != nil { - t.Errorf("unexpeted error: %v", err) + t.Errorf("unexpected error: %v", err) return } if n != 0 { diff --git a/iface/iface_test.go b/iface/iface_test.go index 3e0759d87..5ce276b75 100644 --- a/iface/iface_test.go +++ b/iface/iface_test.go @@ -13,7 +13,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) -// keep darwin compability +// keep darwin compatibility const ( WgIntNumber = 2000 ) diff --git a/iface/module_linux.go b/iface/module_linux.go index e943c0ba7..11c0482d5 100644 --- a/iface/module_linux.go +++ b/iface/module_linux.go @@ -110,7 +110,7 @@ func canCreateFakeWireGuardInterface() bool { // We willingly try to create a device with an invalid // MTU here as the validation of the MTU will be performed after // the validation of the link kind and hence allows us to check - // for the existance of the wireguard module without actually + // for the existence of the wireguard module without actually // creating a link. // // As a side-effect, this will also let the kernel lazy-load @@ -271,12 +271,12 @@ func moduleStatus(name string) (status, error) { func loadModuleWithDependencies(name, path string) error { deps, err := getModuleDependencies(name) if err != nil { - return fmt.Errorf("couldn't load list of module %s dependecies", name) + return fmt.Errorf("couldn't load list of module %s dependencies", name) } for _, dep := range deps { err = loadModule(dep.name, dep.path) if err != nil { - return fmt.Errorf("couldn't load dependecy module %s for %s", dep.name, name) + return fmt.Errorf("couldn't load dependency module %s for %s", dep.name, name) } } return loadModule(name, path) diff --git a/iface/tun_darwin.go b/iface/tun_darwin.go index 4cf3712bd..a4ab2b4b1 100644 --- a/iface/tun_darwin.go +++ b/iface/tun_darwin.go @@ -20,7 +20,7 @@ func (c *tunDevice) Create() error { func (c *tunDevice) assignAddr() error { cmd := exec.Command("ifconfig", c.name, "inet", c.address.IP.String(), c.address.IP.String()) if out, err := cmd.CombinedOutput(); err != nil { - log.Infof(`adding addreess command "%v" failed with output %s and error: `, cmd.String(), out) + log.Infof(`adding address command "%v" failed with output %s and error: `, cmd.String(), out) return err } diff --git a/infrastructure_files/base.setup.env b/infrastructure_files/base.setup.env index fa337c55d..e254aa6f3 100644 --- a/infrastructure_files/base.setup.env +++ b/infrastructure_files/base.setup.env @@ -6,10 +6,10 @@ NETBIRD_MGMT_API_PORT=${NETBIRD_MGMT_API_PORT:-33073} # Management API endpoint address, used by the Dashboard NETBIRD_MGMT_API_ENDPOINT=https://$NETBIRD_DOMAIN:$NETBIRD_MGMT_API_PORT -# Management Certficate file path. These are generated by the Dashboard container +# Management Certificate file path. These are generated by the Dashboard container NETBIRD_LETSENCRYPT_DOMAIN=$NETBIRD_DOMAIN NETBIRD_MGMT_API_CERT_FILE="/etc/letsencrypt/live/$NETBIRD_LETSENCRYPT_DOMAIN/fullchain.pem" -# Management Certficate key file path. +# Management Certificate key file path. NETBIRD_MGMT_API_CERT_KEY_FILE="/etc/letsencrypt/live/$NETBIRD_LETSENCRYPT_DOMAIN/privkey.pem" # By default Management single account mode is enabled and domain set to $NETBIRD_DOMAIN, you may want to set this to your user's email domain NETBIRD_MGMT_SINGLE_ACCOUNT_MODE_DOMAIN=$NETBIRD_DOMAIN diff --git a/infrastructure_files/configure.sh b/infrastructure_files/configure.sh index 09152d788..6d2902816 100755 --- a/infrastructure_files/configure.sh +++ b/infrastructure_files/configure.sh @@ -125,7 +125,7 @@ if [[ "$NETBIRD_DISABLE_LETSENCRYPT" == "true" ]]; then echo "- $NETBIRD_SIGNAL_ENDPOINT/signalexchange.SignalExchange/ -grpc-> signal:80" echo "You most likely also have to change NETBIRD_MGMT_API_ENDPOINT in base.setup.env and port-mappings in docker-compose.yml.tmpl and rerun this script." echo " The target of the forwards depends on your setup. Beware of the gRPC protocol instead of http for management and signal!" - echo "You are also free to remove any occurences of the Letsencrypt-volume $LETSENCRYPT_VOLUMENAME" + echo "You are also free to remove any occurrences of the Letsencrypt-volume $LETSENCRYPT_VOLUMENAME" echo "" export NETBIRD_SIGNAL_PROTOCOL="https" diff --git a/infrastructure_files/turnserver.conf.tmpl b/infrastructure_files/turnserver.conf.tmpl index e5d3b231d..9b31cb511 100644 --- a/infrastructure_files/turnserver.conf.tmpl +++ b/infrastructure_files/turnserver.conf.tmpl @@ -696,7 +696,7 @@ no-cli #web-admin-port=8080 # Web-admin server listen on STUN/TURN worker threads -# By default it is disabled for security resons! (Not recommended in any production environment!) +# By default it is disabled for security reasons! (Not recommended in any production environment!) # #web-admin-listen-on-workers diff --git a/management/cmd/management.go b/management/cmd/management.go index 9ad2b7274..1a00a0f57 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -101,7 +101,7 @@ var ( _, valid := dns.IsDomainName(dnsDomain) if !valid || len(dnsDomain) > 192 { - return fmt.Errorf("failed parsing the provided dns-domain. Valid status: %t, Lenght: %d", valid, len(dnsDomain)) + return fmt.Errorf("failed parsing the provided dns-domain. Valid status: %t, Length: %d", valid, len(dnsDomain)) } return nil diff --git a/management/cmd/root.go b/management/cmd/root.go index d8a9da53f..de8b5b8b3 100644 --- a/management/cmd/root.go +++ b/management/cmd/root.go @@ -61,7 +61,7 @@ func init() { mgmtCmd.Flags().StringVar(&certFile, "cert-file", "", "Location of your SSL certificate. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect") mgmtCmd.Flags().StringVar(&certKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect") mgmtCmd.Flags().BoolVar(&disableMetrics, "disable-anonymous-metrics", false, "disables push of anonymous usage metrics to NetBird") - mgmtCmd.Flags().StringVar(&dnsDomain, "dns-domain", defaultSingleAccModeDomain, fmt.Sprintf("Domain used for peer resolution. This is appended to the peer's name, e.g. pi-server. %s. Max lenght is 192 characters to allow appending to a peer name with up to 63 characters.", defaultSingleAccModeDomain)) + mgmtCmd.Flags().StringVar(&dnsDomain, "dns-domain", defaultSingleAccModeDomain, fmt.Sprintf("Domain used for peer resolution. This is appended to the peer's name, e.g. pi-server. %s. Max length is 192 characters to allow appending to a peer name with up to 63 characters.", defaultSingleAccModeDomain)) mgmtCmd.Flags().BoolVar(&idpSignKeyRefreshEnabled, "idp-sign-key-refresh-enabled", false, "Enable cache headers evaluation to determine signing key rotation period. This will refresh the signing key upon expiry.") mgmtCmd.Flags().BoolVar(&userDeleteFromIDPEnabled, "user-delete-from-idp", false, "Allows to delete user from IDP when user is deleted from account") rootCmd.MarkFlagRequired("config") //nolint diff --git a/management/server/account_test.go b/management/server/account_test.go index 181e1c3fe..c8a8a5dc9 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -1116,7 +1116,7 @@ func TestAccountManager_DeletePeer(t *testing.T) { } if account.Network.CurrentSerial() != 2 { - t.Errorf("expecting Network Serial=%d to be incremented and be equal to 2 after adding and deleteing a peer", account.Network.CurrentSerial()) + t.Errorf("expecting Network Serial=%d to be incremented and be equal to 2 after adding and deleting a peer", account.Network.CurrentSerial()) } ev := getEvent(t, account.Id, manager, activity.PeerRemovedByUser) diff --git a/management/server/dns.go b/management/server/dns.go index 9c39a00fa..f90a5e9f2 100644 --- a/management/server/dns.go +++ b/management/server/dns.go @@ -216,7 +216,7 @@ func addPeerLabelsToAccount(account *Account, peerLabels lookupMap) { log.Errorf("got an error while generating a peer host label. Peer name %s, error: %v. Trying with the peer's meta hostname", peer.Name, err) label, err = getPeerHostLabel(peer.Meta.Hostname, peerLabels) if err != nil { - log.Errorf("got another error while generating a peer host label with hostname. Peer hostname %s, error: %v. Skiping", peer.Meta.Hostname, err) + log.Errorf("got another error while generating a peer host label with hostname. Peer hostname %s, error: %v. Skipping", peer.Meta.Hostname, err) continue } } diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 383cb0d1f..f32f6347a 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -169,7 +169,7 @@ func (s *GRPCServer) Sync(req *proto.EncryptedMessage, srv proto.ManagementServi s.cancelPeerRoutines(peer) return nil } - log.Debugf("recevied an update for peer %s", peerKey.String()) + log.Debugf("received an update for peer %s", peerKey.String()) encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, update.Update) if err != nil { diff --git a/management/server/http/accounts_handler_test.go b/management/server/http/accounts_handler_test.go index 3b257a703..08c98c830 100644 --- a/management/server/http/accounts_handler_test.go +++ b/management/server/http/accounts_handler_test.go @@ -117,7 +117,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedID: accountID, }, { - name: "PutAccount OK wiht JWT", + name: "PutAccount OK with JWT", expectedBody: true, requestType: http.MethodPut, requestPath: "/api/accounts/" + accountID, @@ -134,7 +134,7 @@ func TestAccounts_AccountsHandler(t *testing.T) { expectedID: accountID, }, { - name: "PutAccount OK wiht JWT Propagation", + name: "PutAccount OK with JWT Propagation", expectedBody: true, requestType: http.MethodPut, requestPath: "/api/accounts/" + accountID, diff --git a/management/server/http/util/util.go b/management/server/http/util/util.go index 277627310..4e2c3d0b3 100644 --- a/management/server/http/util/util.go +++ b/management/server/http/util/util.go @@ -18,7 +18,7 @@ type ErrorResponse struct { Code int `json:"code"` } -// WriteJSONObject simply writes object to the HTTP reponse in JSON format +// WriteJSONObject simply writes object to the HTTP response in JSON format func WriteJSONObject(w http.ResponseWriter, obj interface{}) { w.WriteHeader(http.StatusOK) w.Header().Set("Content-Type", "application/json; charset=UTF-8") diff --git a/management/server/peer.go b/management/server/peer.go index f38e19e87..33c9430fc 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -731,7 +731,7 @@ func checkAuth(loginUserID string, peer *Peer) error { return status.Errorf(status.PermissionDenied, "peer login has expired, please log in once more") } if peer.UserID != loginUserID { - log.Warnf("user mismatch when loggin in peer %s: peer user %s, login user %s ", peer.ID, peer.UserID, loginUserID) + log.Warnf("user mismatch when logging in peer %s: peer user %s, login user %s ", peer.ID, peer.UserID, loginUserID) return status.Errorf(status.Unauthenticated, "can't login") } return nil diff --git a/management/server/policy.go b/management/server/policy.go index d470ab4bf..b7b5b331c 100644 --- a/management/server/policy.go +++ b/management/server/policy.go @@ -43,7 +43,7 @@ const ( ) const ( - // PolicyRuleFlowDirect allows trafic from source to destination + // PolicyRuleFlowDirect allows traffic from source to destination PolicyRuleFlowDirect = PolicyRuleDirection("direct") // PolicyRuleFlowBidirect allows traffic to both directions PolicyRuleFlowBidirect = PolicyRuleDirection("bidirect") diff --git a/management/server/policy_test.go b/management/server/policy_test.go index bf003ffac..971bd27d9 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -111,8 +111,8 @@ func TestAccount_getPeersByPolicy(t *testing.T) { t.Run("check that all peers get map", func(t *testing.T) { for _, p := range account.Peers { peers, firewallRules := account.getPeerConnectionResources(p.ID) - assert.GreaterOrEqual(t, len(peers), 2, "mininum number peers should present") - assert.GreaterOrEqual(t, len(firewallRules), 2, "mininum number of firewall rules should present") + assert.GreaterOrEqual(t, len(peers), 2, "minimum number peers should present") + assert.GreaterOrEqual(t, len(firewallRules), 2, "minimum number of firewall rules should present") } }) diff --git a/management/server/user.go b/management/server/user.go index d3e7b3060..22edd2c2c 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -719,7 +719,7 @@ func (am *DefaultAccountManager) SaveUser(accountID, initiatorUserID string, upd if update.AutoGroups != nil && account.Settings.GroupsPropagationEnabled { removedGroups := difference(oldUser.AutoGroups, update.AutoGroups) - // need force update all auto groups in any case they will not be dublicated + // need force update all auto groups in any case they will not be duplicated account.UserGroupsAddToPeers(oldUser.Id, update.AutoGroups...) account.UserGroupsRemoveFromPeers(oldUser.Id, removedGroups...) diff --git a/release_files/install.sh b/release_files/install.sh index ef5012f0d..a0a9abf98 100755 --- a/release_files/install.sh +++ b/release_files/install.sh @@ -360,7 +360,7 @@ if type uname >/dev/null 2>&1; then echo "NetBird UI installation will be omitted as $ARCH is not a compatible architecture" fi - # Allow netbird UI installation for linux running desktop enviroment + # Allow netbird UI installation for linux running desktop environment if [ -z "$XDG_CURRENT_DESKTOP" ];then SKIP_UI_APP=true echo "NetBird UI installation will be omitted as Linux does not run desktop environment" diff --git a/version/update_test.go b/version/update_test.go index c2b477495..4537ce220 100644 --- a/version/update_test.go +++ b/version/update_test.go @@ -82,7 +82,7 @@ func TestDaemonUpdate(t *testing.T) { waitTimeout(wg) if onUpdate != true { - t.Errorf("invalid dameon version check") + t.Errorf("invalid daemon version check") } } From 8cf2866a6ac27bf133614e225f826dccc0496dba Mon Sep 17 00:00:00 2001 From: Bethuel Mmbaga Date: Thu, 2 Nov 2023 16:33:20 +0300 Subject: [PATCH 36/43] Add integration reference field to group copy method (#1283) The group copy method now includes the IntegrationReference field in its output. This change was made to ensure that the integration reference information is retained when a group instance is copied, which previously was not the case. --- management/server/group.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/management/server/group.go b/management/server/group.go index 201a9cf8e..d626c3538 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -45,10 +45,11 @@ func (g *Group) EventMeta() map[string]any { func (g *Group) Copy() *Group { group := &Group{ - ID: g.ID, - Name: g.Name, - Issued: g.Issued, - Peers: make([]string, len(g.Peers)), + ID: g.ID, + Name: g.Name, + Issued: g.Issued, + Peers: make([]string, len(g.Peers)), + IntegrationReference: g.IntegrationReference, } copy(group.Peers, g.Peers) return group From e2f27502e42029bb9875ec90eaa44757a3f00cf4 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Thu, 2 Nov 2023 19:04:33 +0100 Subject: [PATCH 37/43] Feature/search domain for android (#1256) Support search domain on Android - pass list of search domains to Android SDK - throw notification in case of search domain changes --- client/android/client.go | 44 ++++++++--------- client/internal/connect.go | 14 +++--- client/internal/dns/mockServer.go | 5 ++ client/internal/dns/notifier.go | 57 ++++++++++++++++++++++ client/internal/dns/server.go | 29 ++++++++++- client/internal/dns/server_test.go | 11 +++-- client/internal/engine.go | 25 +++++----- client/internal/listener/network_change.go | 7 +++ client/internal/mobile_dependency.go | 12 ++--- client/internal/routemanager/manager.go | 5 +- client/internal/routemanager/mock.go | 3 +- client/internal/routemanager/notifier.go | 31 +++++------- iface/tun.go | 5 +- iface/tun_adapter.go | 2 +- iface/tun_android.go | 7 ++- 15 files changed, 180 insertions(+), 77 deletions(-) create mode 100644 client/internal/dns/notifier.go create mode 100644 client/internal/listener/network_change.go diff --git a/client/android/client.go b/client/android/client.go index bb15268eb..f2dd9e3f7 100644 --- a/client/android/client.go +++ b/client/android/client.go @@ -8,8 +8,8 @@ import ( "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/dns" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/formatter" @@ -31,9 +31,9 @@ type IFaceDiscover interface { stdnet.ExternalIFaceDiscover } -// RouteListener export internal RouteListener for mobile -type RouteListener interface { - routemanager.RouteListener +// NetworkChangeListener export internal NetworkChangeListener for mobile +type NetworkChangeListener interface { + listener.NetworkChangeListener } // DnsReadyListener export internal dns ReadyListener for mobile @@ -47,26 +47,26 @@ func init() { // Client struct manage the life circle of background service type Client struct { - cfgFile string - tunAdapter iface.TunAdapter - iFaceDiscover IFaceDiscover - recorder *peer.Status - ctxCancel context.CancelFunc - ctxCancelLock *sync.Mutex - deviceName string - routeListener routemanager.RouteListener + cfgFile string + tunAdapter iface.TunAdapter + iFaceDiscover IFaceDiscover + recorder *peer.Status + ctxCancel context.CancelFunc + ctxCancelLock *sync.Mutex + deviceName string + networkChangeListener listener.NetworkChangeListener } // NewClient instantiate a new Client -func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, routeListener RouteListener) *Client { +func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client { return &Client{ - cfgFile: cfgFile, - deviceName: deviceName, - tunAdapter: tunAdapter, - iFaceDiscover: iFaceDiscover, - recorder: peer.NewRecorder(""), - ctxCancelLock: &sync.Mutex{}, - routeListener: routeListener, + cfgFile: cfgFile, + deviceName: deviceName, + tunAdapter: tunAdapter, + iFaceDiscover: iFaceDiscover, + recorder: peer.NewRecorder(""), + ctxCancelLock: &sync.Mutex{}, + networkChangeListener: networkChangeListener, } } @@ -96,7 +96,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener) + return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot). @@ -120,7 +120,7 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener // todo do not throw error in case of cancelled context ctx = internal.CtxInitState(ctx) - return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener) + return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener) } // Stop the internal client and free the resources diff --git a/client/internal/connect.go b/client/internal/connect.go index 97fe2cc59..79f97e87f 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -13,8 +13,8 @@ import ( gstatus "google.golang.org/grpc/status" "github.com/netbirdio/netbird/client/internal/dns" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" - "github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/system" @@ -31,14 +31,14 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) } // RunClientMobile with main logic on mobile system -func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, routeListener routemanager.RouteListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error { +func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, networkChangeListener listener.NetworkChangeListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error { // in case of non Android os these variables will be nil mobileDependency := MobileDependency{ - TunAdapter: tunAdapter, - IFaceDiscover: iFaceDiscover, - RouteListener: routeListener, - HostDNSAddresses: dnsAddresses, - DnsReadyListener: dnsReadyListener, + TunAdapter: tunAdapter, + IFaceDiscover: iFaceDiscover, + NetworkChangeListener: networkChangeListener, + HostDNSAddresses: dnsAddresses, + DnsReadyListener: dnsReadyListener, } return runClient(ctx, config, statusRecorder, mobileDependency) } diff --git a/client/internal/dns/mockServer.go b/client/internal/dns/mockServer.go index 8970eec6e..3534fc0c3 100644 --- a/client/internal/dns/mockServer.go +++ b/client/internal/dns/mockServer.go @@ -2,6 +2,7 @@ package dns import ( "fmt" + nbdns "github.com/netbirdio/netbird/dns" ) @@ -43,3 +44,7 @@ func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error { } return fmt.Errorf("method UpdateDNSServer is not implemented") } + +func (m *MockServer) SearchDomains() []string { + return make([]string, 0) +} diff --git a/client/internal/dns/notifier.go b/client/internal/dns/notifier.go new file mode 100644 index 000000000..85c270e58 --- /dev/null +++ b/client/internal/dns/notifier.go @@ -0,0 +1,57 @@ +package dns + +import ( + "reflect" + "sort" + "sync" + + "github.com/netbirdio/netbird/client/internal/listener" +) + +type notifier struct { + listener listener.NetworkChangeListener + listenerMux sync.Mutex + searchDomains []string +} + +func newNotifier(initialSearchDomains []string) *notifier { + sort.Strings(initialSearchDomains) + return ¬ifier{ + searchDomains: initialSearchDomains, + } +} + +func (n *notifier) setListener(listener listener.NetworkChangeListener) { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + n.listener = listener +} + +func (n *notifier) onNewSearchDomains(searchDomains []string) { + sort.Strings(searchDomains) + + if len(n.searchDomains) != len(searchDomains) { + n.searchDomains = searchDomains + n.notify() + return + } + + if reflect.DeepEqual(n.searchDomains, searchDomains) { + return + } + + n.searchDomains = searchDomains + n.notify() +} + +func (n *notifier) notify() { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + if n.listener == nil { + return + } + + go func(l listener.NetworkChangeListener) { + l.OnNetworkChanged() + }(n.listener) +} diff --git a/client/internal/dns/server.go b/client/internal/dns/server.go index 600b16a6e..6655a6e4e 100644 --- a/client/internal/dns/server.go +++ b/client/internal/dns/server.go @@ -10,6 +10,7 @@ import ( "github.com/mitchellh/hashstructure/v2" log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal/listener" nbdns "github.com/netbirdio/netbird/dns" ) @@ -25,6 +26,7 @@ type Server interface { DnsIP() string UpdateDNSServer(serial uint64, update nbdns.Config) error OnUpdatedHostDNSServer(strings []string) + SearchDomains() []string } type registeredHandlerMap map[string]handlerWithStop @@ -47,6 +49,9 @@ type DefaultServer struct { permanent bool hostsDnsList []string hostsDnsListLock sync.Mutex + + // make sense on mobile only + searchDomainNotifier *notifier } type handlerWithStop interface { @@ -81,12 +86,15 @@ func NewDefaultServer(ctx context.Context, wgInterface WGIface, customAddress st } // NewDefaultServerPermanentUpstream returns a new dns server. It optimized for mobile systems -func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string) *DefaultServer { +func NewDefaultServerPermanentUpstream(ctx context.Context, wgInterface WGIface, hostsDnsList []string, config nbdns.Config, listener listener.NetworkChangeListener) *DefaultServer { log.Debugf("host dns address list is: %v", hostsDnsList) ds := newDefaultServer(ctx, wgInterface, newServiceViaMemory(wgInterface)) ds.permanent = true ds.hostsDnsList = hostsDnsList ds.addHostRootZone() + ds.currentConfig = dnsConfigToHostDNSConfig(config, ds.service.RuntimeIP(), ds.service.RuntimePort()) + ds.searchDomainNotifier = newNotifier(ds.SearchDomains()) + ds.searchDomainNotifier.setListener(listener) setServerDns(ds) return ds } @@ -212,6 +220,21 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro } } +func (s *DefaultServer) SearchDomains() []string { + var searchDomains []string + + for _, dConf := range s.currentConfig.domains { + if dConf.disabled { + continue + } + if dConf.matchOnly { + continue + } + searchDomains = append(searchDomains, dConf.domain) + } + return searchDomains +} + func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { // is the service should be disabled, we stop the listener or fake resolver // and proceed with a regular update to clean up the handlers and records @@ -246,6 +269,10 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { log.Error(err) } + if s.searchDomainNotifier != nil { + s.searchDomainNotifier.onNewSearchDomains(s.SearchDomains()) + } + return nil } diff --git a/client/internal/dns/server_test.go b/client/internal/dns/server_test.go index 119ac684c..cd7932d07 100644 --- a/client/internal/dns/server_test.go +++ b/client/internal/dns/server_test.go @@ -593,7 +593,8 @@ func TestDNSPermanent_updateHostDNS_emptyUpstream(t *testing.T) { defer wgIFace.Close() var dnsList []string - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList) + dnsConfig := nbdns.Config{} + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, dnsList, dnsConfig, nil) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -616,8 +617,8 @@ func TestDNSPermanent_updateUpstream(t *testing.T) { t.Fatal("failed to initialize wg interface") } defer wgIFace.Close() - - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}) + dnsConfig := nbdns.Config{} + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) @@ -708,8 +709,8 @@ func TestDNSPermanent_matchOnly(t *testing.T) { t.Fatal("failed to initialize wg interface") } defer wgIFace.Close() - - dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}) + dnsConfig := nbdns.Config{} + dnsServer := NewDefaultServerPermanentUpstream(context.Background(), wgIFace, []string{"8.8.8.8"}, dnsConfig, nil) err = dnsServer.Initialize() if err != nil { t.Errorf("failed to initialize DNS server: %v", err) diff --git a/client/internal/engine.go b/client/internal/engine.go index aeeddc372..a81fabde6 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -195,12 +195,13 @@ func (e *Engine) Start() error { var routes []*route.Route if runtime.GOOS == "android" { - routes, err = e.readInitialSettings() + var dnsConfig *nbdns.Config + routes, dnsConfig, err = e.readInitialSettings() if err != nil { return err } if e.dnsServer == nil { - e.dnsServer = dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses) + e.dnsServer = dns.NewDefaultServerPermanentUpstream(e.ctx, e.wgInterface, e.mobileDep.HostDNSAddresses, *dnsConfig, e.mobileDep.NetworkChangeListener) go e.mobileDep.DnsReadyListener.OnReady() } } else { @@ -215,15 +216,16 @@ func (e *Engine) Start() error { } e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, routes) - e.routeManager.SetRouteChangeListener(e.mobileDep.RouteListener) + e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener) - if runtime.GOOS != "android" { - err = e.wgInterface.Create() - } else { + if runtime.GOOS == "android" { err = e.wgInterface.CreateOnMobile(iface.MobileIFaceArguments{ - Routes: e.routeManager.InitialRouteRange(), - Dns: e.dnsServer.DnsIP(), + Routes: e.routeManager.InitialRouteRange(), + Dns: e.dnsServer.DnsIP(), + SearchDomains: e.dnsServer.SearchDomains(), }) + } else { + err = e.wgInterface.Create() } if err != nil { log.Errorf("failed creating tunnel interface %s: [%s]", wgIFaceName, err.Error()) @@ -1051,13 +1053,14 @@ func (e *Engine) close() { } } -func (e *Engine) readInitialSettings() ([]*route.Route, error) { +func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) { netMap, err := e.mgmClient.GetNetworkMap() if err != nil { - return nil, err + return nil, nil, err } routes := toRoutes(netMap.GetRoutes()) - return routes, nil + dnsCfg := toDNSConfig(netMap.GetDNSConfig()) + return routes, &dnsCfg, nil } func findIPFromInterfaceName(ifaceName string) (net.IP, error) { diff --git a/client/internal/listener/network_change.go b/client/internal/listener/network_change.go new file mode 100644 index 000000000..ff9cb11f5 --- /dev/null +++ b/client/internal/listener/network_change.go @@ -0,0 +1,7 @@ +package listener + +// NetworkChangeListener is a callback interface for mobile system +type NetworkChangeListener interface { + // OnNetworkChanged invoke when network settings has been changed + OnNetworkChanged() +} diff --git a/client/internal/mobile_dependency.go b/client/internal/mobile_dependency.go index fc8fa61ce..a2bbf2473 100644 --- a/client/internal/mobile_dependency.go +++ b/client/internal/mobile_dependency.go @@ -2,16 +2,16 @@ package internal import ( "github.com/netbirdio/netbird/client/internal/dns" - "github.com/netbirdio/netbird/client/internal/routemanager" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/iface" ) // MobileDependency collect all dependencies for mobile platform type MobileDependency struct { - TunAdapter iface.TunAdapter - IFaceDiscover stdnet.ExternalIFaceDiscover - RouteListener routemanager.RouteListener - HostDNSAddresses []string - DnsReadyListener dns.ReadyListener + TunAdapter iface.TunAdapter + IFaceDiscover stdnet.ExternalIFaceDiscover + NetworkChangeListener listener.NetworkChangeListener + HostDNSAddresses []string + DnsReadyListener dns.ReadyListener } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 05b725045..1f812983c 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" @@ -16,7 +17,7 @@ import ( // Manager is a route manager interface type Manager interface { UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error - SetRouteChangeListener(listener RouteListener) + SetRouteChangeListener(listener listener.NetworkChangeListener) InitialRouteRange() []string Stop() } @@ -96,7 +97,7 @@ func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Ro } // SetRouteChangeListener set RouteListener for route change notifier -func (m *DefaultManager) SetRouteChangeListener(listener RouteListener) { +func (m *DefaultManager) SetRouteChangeListener(listener listener.NetworkChangeListener) { m.notifier.setListener(listener) } diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index f56dbfb17..8970841a2 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/iface" "github.com/netbirdio/netbird/route" ) @@ -32,7 +33,7 @@ func (m *MockManager) Start(ctx context.Context, iface *iface.WGIface) { } // SetRouteChangeListener mock implementation of SetRouteChangeListener from Manager interface -func (m *MockManager) SetRouteChangeListener(listener RouteListener) { +func (m *MockManager) SetRouteChangeListener(listener listener.NetworkChangeListener) { } diff --git a/client/internal/routemanager/notifier.go b/client/internal/routemanager/notifier.go index e37811166..96ca95aa2 100644 --- a/client/internal/routemanager/notifier.go +++ b/client/internal/routemanager/notifier.go @@ -4,31 +4,26 @@ import ( "sort" "sync" + "github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/route" ) -// RouteListener is a callback interface for mobile system -type RouteListener interface { - // OnNewRouteSetting invoke when new route setting has been arrived - OnNewRouteSetting() -} - type notifier struct { initialRouteRangers []string routeRangers []string - routeListener RouteListener - routeListenerMux sync.Mutex + listener listener.NetworkChangeListener + listenerMux sync.Mutex } func newNotifier() *notifier { return ¬ifier{} } -func (n *notifier) setListener(listener RouteListener) { - n.routeListenerMux.Lock() - defer n.routeListenerMux.Unlock() - n.routeListener = listener +func (n *notifier) setListener(listener listener.NetworkChangeListener) { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + n.listener = listener } func (n *notifier) setInitialClientRoutes(clientRoutes []*route.Route) { @@ -62,15 +57,15 @@ func (n *notifier) onNewRoutes(idMap map[string][]*route.Route) { } func (n *notifier) notify() { - n.routeListenerMux.Lock() - defer n.routeListenerMux.Unlock() - if n.routeListener == nil { + n.listenerMux.Lock() + defer n.listenerMux.Unlock() + if n.listener == nil { return } - go func(l RouteListener) { - l.OnNewRouteSetting() - }(n.routeListener) + go func(l listener.NetworkChangeListener) { + l.OnNetworkChanged() + }(n.listener) } func (n *notifier) hasDiff(a []string, b []string) bool { diff --git a/iface/tun.go b/iface/tun.go index 51a7783a1..ec8af4c32 100644 --- a/iface/tun.go +++ b/iface/tun.go @@ -1,8 +1,9 @@ package iface type MobileIFaceArguments struct { - Routes []string - Dns string + Routes []string + Dns string + SearchDomains []string } // NetInterface represents a generic network tunnel interface diff --git a/iface/tun_adapter.go b/iface/tun_adapter.go index 0ba0bde22..da0b1695b 100644 --- a/iface/tun_adapter.go +++ b/iface/tun_adapter.go @@ -2,6 +2,6 @@ package iface // TunAdapter is an interface for create tun device from externel service type TunAdapter interface { - ConfigureInterface(address string, mtu int, dns string, routes string) (int, error) + ConfigureInterface(address string, mtu int, dns string, searchDomains string, routes string) (int, error) UpdateAddr(address string) error } diff --git a/iface/tun_android.go b/iface/tun_android.go index d45f05282..30d86e7e2 100644 --- a/iface/tun_android.go +++ b/iface/tun_android.go @@ -37,7 +37,8 @@ func (t *tunDevice) Create(mIFaceArgs MobileIFaceArguments) error { log.Info("create tun interface") var err error routesString := t.routesToString(mIFaceArgs.Routes) - t.fd, err = t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, mIFaceArgs.Dns, routesString) + searchDomainsToString := t.searchDomainsToString(mIFaceArgs.SearchDomains) + t.fd, err = t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, mIFaceArgs.Dns, searchDomainsToString, routesString) if err != nil { log.Errorf("failed to create Android interface: %s", err) return err @@ -94,3 +95,7 @@ func (t *tunDevice) Close() (err error) { func (t *tunDevice) routesToString(routes []string) string { return strings.Join(routes, ";") } + +func (t *tunDevice) searchDomainsToString(searchDomains []string) string { + return strings.Join(searchDomains, ";") +} From 2c01514259c766798422e51f164b7f5ec5950a5c Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 3 Nov 2023 13:05:07 +0100 Subject: [PATCH 38/43] Fix black icon background on Win (#1269) Fix the black icon background on Windows. Update to the patched systray library. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e186002f3..2c862cf14 100644 --- a/go.mod +++ b/go.mod @@ -159,6 +159,6 @@ require ( replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0 -replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c +replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949 replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20230524172305-5a498a82b33f diff --git a/go.sum b/go.sum index f06eb08d0..e58eb93ef 100644 --- a/go.sum +++ b/go.sum @@ -499,8 +499,8 @@ github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200- github.com/netbirdio/management-integrations/integrations v0.0.0-20231027143200-a966bce7db88/go.mod h1:KSqjzHcqlodTWiuap5lRXxt5KT3vtYRoksL0KIrTK40= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0 h1:hirFRfx3grVA/9eEyjME5/z3nxdJlN9kfQpvWWPk32g= github.com/netbirdio/service v0.0.0-20230215170314-b923b89432b0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= -github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c h1:wK/s4nyZj/GF/kFJQjX6nqNfE0G3gcqd6hhnPCyp4sw= -github.com/netbirdio/systray v0.0.0-20221012095658-dc8eda872c0c/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= +github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949 h1:xbWM9BU6mwZZLHxEjxIX/V8Hv3HurQt4mReIE4mY4DM= +github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949/go.mod h1:AecygODWIsBquJCJFop8MEQcJbWFfw/1yWbVabNgpCM= github.com/netbirdio/wireguard-go v0.0.0-20230524172305-5a498a82b33f h1:WQXGYCKPkNs1KusFTLieV73UVTNfZVyez4CFRvlOruM= github.com/netbirdio/wireguard-go v0.0.0-20230524172305-5a498a82b33f/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= From 9c4bf1e899e93619e2fc1e0734bf76d480b1f946 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Fri, 3 Nov 2023 13:05:39 +0100 Subject: [PATCH 39/43] Parse original resolv.conf (#1270) Handle original search domains in resolv.conf type implementations. - parse the original resolv.conf file - merge the search domains - ignore the domain keyword - append any other config lines (sortstlist, options) - fix read origin resolv.conf from bkp in resolvconf implementation - fix line length validation - fix number of search domains validation --- client/internal/dns/file_linux.go | 232 +++++++++++++++++------- client/internal/dns/file_linux_test.go | 62 +++++++ client/internal/dns/resolvconf_linux.go | 59 +++--- 3 files changed, 253 insertions(+), 100 deletions(-) create mode 100644 client/internal/dns/file_linux_test.go diff --git a/client/internal/dns/file_linux.go b/client/internal/dns/file_linux.go index de6e8b899..81b16459b 100644 --- a/client/internal/dns/file_linux.go +++ b/client/internal/dns/file_linux.go @@ -3,29 +3,25 @@ package dns import ( + "bufio" "bytes" "fmt" "os" + "strings" log "github.com/sirupsen/logrus" ) const ( - fileGeneratedResolvConfContentHeader = "# Generated by NetBird" - fileGeneratedResolvConfSearchBeginContent = "search " - fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader + - "\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" + - fileGeneratedResolvConfSearchBeginContent + "%s\n\n" + - "%s\n" -) + fileGeneratedResolvConfContentHeader = "# Generated by NetBird" + fileGeneratedResolvConfContentHeaderNextLine = fileGeneratedResolvConfContentHeader + ` +# If needed you can restore the original file by copying back ` + fileDefaultResolvConfBackupLocation + "\n\n" -const ( fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird" - fileMaxLineCharsLimit = 256 - fileMaxNumberOfSearchDomains = 6 -) -var fileSearchLineBeginCharCount = len(fileGeneratedResolvConfSearchBeginContent) + fileMaxLineCharsLimit = 256 + fileMaxNumberOfSearchDomains = 6 +) type fileConfigurator struct { originalPerms os.FileMode @@ -55,58 +51,39 @@ func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error { } return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured") } - managerType, err := getOSDNSManagerType() - if err != nil { - return err - } - switch managerType { - case fileManager, netbirdManager: - if !backupFileExist { - err = f.backup() - if err != nil { - return fmt.Errorf("unable to backup the resolv.conf file") - } - } - default: - // todo improve this and maybe restart DNS manager from scratch - return fmt.Errorf("something happened and file manager is not your preferred host dns configurator, restart the agent") - } - var searchDomains string - appendedDomains := 0 - for _, dConf := range config.domains { - if dConf.matchOnly || dConf.disabled { - continue - } - if appendedDomains >= fileMaxNumberOfSearchDomains { - // lets log all skipped domains - log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain) - continue - } - if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit { - // lets log all skipped domains - log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain) - continue - } - - searchDomains += " " + dConf.domain - appendedDomains++ - } - - originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation) - if err != nil { - log.Errorf("Could not read existing resolv.conf") - } - content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent)) - err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms) - if err != nil { - err = f.restore() + if !backupFileExist { + err = f.backup() if err != nil { + return fmt.Errorf("unable to backup the resolv.conf file") + } + } + + searchDomainList := searchDomains(config) + + originalSearchDomains, nameServers, others, err := originalDNSConfigs(fileDefaultResolvConfBackupLocation) + if err != nil { + log.Error(err) + } + + searchDomainList = mergeSearchDomains(searchDomainList, originalSearchDomains) + + buf := prepareResolvConfContent( + searchDomainList, + append([]string{config.serverIP}, nameServers...), + others) + + log.Debugf("creating managed file %s", defaultResolvConfPath) + err = os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms) + if err != nil { + restoreErr := f.restore() + if restoreErr != nil { log.Errorf("attempt to restore default file failed with error: %s", err) } - return err + return fmt.Errorf("got an creating resolver file %s. Error: %s", defaultResolvConfPath, err) } - log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, appendedDomains, searchDomains) + + log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, len(searchDomainList), searchDomainList) return nil } @@ -138,15 +115,138 @@ func (f *fileConfigurator) restore() error { return os.RemoveAll(fileDefaultResolvConfBackupLocation) } -func writeDNSConfig(content, fileName string, permissions os.FileMode) error { - log.Debugf("creating managed file %s", fileName) +func prepareResolvConfContent(searchDomains, nameServers, others []string) bytes.Buffer { var buf bytes.Buffer - buf.WriteString(content) - err := os.WriteFile(fileName, buf.Bytes(), permissions) - if err != nil { - return fmt.Errorf("got an creating resolver file %s. Error: %s", fileName, err) + buf.WriteString(fileGeneratedResolvConfContentHeaderNextLine) + + for _, cfgLine := range others { + buf.WriteString(cfgLine) + buf.WriteString("\n") } - return nil + + if len(searchDomains) > 0 { + buf.WriteString("search ") + buf.WriteString(strings.Join(searchDomains, " ")) + buf.WriteString("\n") + } + + for _, ns := range nameServers { + buf.WriteString("nameserver ") + buf.WriteString(ns) + buf.WriteString("\n") + } + return buf +} + +func searchDomains(config hostDNSConfig) []string { + listOfDomains := make([]string, 0) + for _, dConf := range config.domains { + if dConf.matchOnly || dConf.disabled { + continue + } + + listOfDomains = append(listOfDomains, dConf.domain) + } + return listOfDomains +} + +func originalDNSConfigs(resolvconfFile string) (searchDomains, nameServers, others []string, err error) { + file, err := os.Open(resolvconfFile) + if err != nil { + err = fmt.Errorf(`could not read existing resolv.conf`) + return + } + defer file.Close() + + reader := bufio.NewReader(file) + + for { + lineBytes, isPrefix, readErr := reader.ReadLine() + if readErr != nil { + break + } + + if isPrefix { + err = fmt.Errorf(`resolv.conf line too long`) + return + } + + line := strings.TrimSpace(string(lineBytes)) + + if strings.HasPrefix(line, "#") { + continue + } + + if strings.HasPrefix(line, "domain") { + continue + } + + if strings.HasPrefix(line, "options") && strings.Contains(line, "rotate") { + line = strings.ReplaceAll(line, "rotate", "") + splitLines := strings.Fields(line) + if len(splitLines) == 1 { + continue + } + line = strings.Join(splitLines, " ") + } + + if strings.HasPrefix(line, "search") { + splitLines := strings.Fields(line) + if len(splitLines) < 2 { + continue + } + + searchDomains = splitLines[1:] + continue + } + + if strings.HasPrefix(line, "nameserver") { + splitLines := strings.Fields(line) + if len(splitLines) != 2 { + continue + } + nameServers = append(nameServers, splitLines[1]) + continue + } + + others = append(others, line) + } + return +} + +// merge search domains lists and cut off the list if it is too long +func mergeSearchDomains(searchDomains []string, originalSearchDomains []string) []string { + lineSize := len("search") + searchDomainsList := make([]string, 0, len(searchDomains)+len(originalSearchDomains)) + + lineSize = validateAndFillSearchDomains(lineSize, &searchDomainsList, searchDomains) + _ = validateAndFillSearchDomains(lineSize, &searchDomainsList, originalSearchDomains) + + return searchDomainsList +} + +// validateAndFillSearchDomains checks if the search domains list is not too long and if the line is not too long +// extend s slice with vs elements +// return with the number of characters in the searchDomains line +func validateAndFillSearchDomains(initialLineChars int, s *[]string, vs []string) int { + for _, sd := range vs { + tmpCharsNumber := initialLineChars + 1 + len(sd) + if tmpCharsNumber > fileMaxLineCharsLimit { + // lets log all skipped domains + log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, sd) + continue + } + + initialLineChars = tmpCharsNumber + + if len(*s) >= fileMaxNumberOfSearchDomains { + // lets log all skipped domains + log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, sd) + continue + } + *s = append(*s, sd) + } + return initialLineChars } func copyFile(src, dest string) error { diff --git a/client/internal/dns/file_linux_test.go b/client/internal/dns/file_linux_test.go new file mode 100644 index 000000000..369a47ef4 --- /dev/null +++ b/client/internal/dns/file_linux_test.go @@ -0,0 +1,62 @@ +package dns + +import ( + "fmt" + "testing" +) + +func Test_mergeSearchDomains(t *testing.T) { + searchDomains := []string{"a", "b"} + originDomains := []string{"a", "b"} + mergedDomains := mergeSearchDomains(searchDomains, originDomains) + if len(mergedDomains) != 4 { + t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 4) + } +} + +func Test_mergeSearchTooMuchDomains(t *testing.T) { + searchDomains := []string{"a", "b", "c", "d", "e", "f", "g"} + originDomains := []string{"h", "i"} + mergedDomains := mergeSearchDomains(searchDomains, originDomains) + if len(mergedDomains) != 6 { + t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 6) + } +} + +func Test_mergeSearchTooMuchDomainsInOrigin(t *testing.T) { + searchDomains := []string{"a", "b"} + originDomains := []string{"c", "d", "e", "f", "g"} + mergedDomains := mergeSearchDomains(searchDomains, originDomains) + if len(mergedDomains) != 6 { + t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 6) + } +} + +func Test_mergeSearchTooLongDomain(t *testing.T) { + searchDomains := []string{getLongLine()} + originDomains := []string{"b"} + mergedDomains := mergeSearchDomains(searchDomains, originDomains) + if len(mergedDomains) != 1 { + t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 1) + } + + searchDomains = []string{"b"} + originDomains = []string{getLongLine()} + + mergedDomains = mergeSearchDomains(searchDomains, originDomains) + if len(mergedDomains) != 1 { + t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 1) + } +} + +func getLongLine() string { + x := "search " + for { + for i := 0; i <= 9; i++ { + if len(x) > fileMaxLineCharsLimit { + return x + } + x = fmt.Sprintf("%s%d", x, i) + } + } +} diff --git a/client/internal/dns/resolvconf_linux.go b/client/internal/dns/resolvconf_linux.go index e0f72b4bf..1ae2de3dd 100644 --- a/client/internal/dns/resolvconf_linux.go +++ b/client/internal/dns/resolvconf_linux.go @@ -3,10 +3,9 @@ package dns import ( + "bytes" "fmt" - "os" "os/exec" - "strings" log "github.com/sirupsen/logrus" ) @@ -15,11 +14,24 @@ const resolvconfCommand = "resolvconf" type resolvconf struct { ifaceName string + + originalSearchDomains []string + originalNameServers []string + othersConfigs []string } +// supported "openresolv" only func newResolvConfConfigurator(wgInterface WGIface) (hostManager, error) { + originalSearchDomains, nameServers, others, err := originalDNSConfigs("/etc/resolv.conf") + if err != nil { + log.Error(err) + } + return &resolvconf{ - ifaceName: wgInterface.Name(), + ifaceName: wgInterface.Name(), + originalSearchDomains: originalSearchDomains, + originalNameServers: nameServers, + othersConfigs: others, }, nil } @@ -37,41 +49,20 @@ func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error { return fmt.Errorf("unable to configure DNS for this peer using resolvconf manager without a nameserver group with all domains configured") } - var searchDomains string - appendedDomains := 0 - for _, dConf := range config.domains { - if dConf.matchOnly || dConf.disabled { - continue - } + searchDomainList := searchDomains(config) + searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains) - if appendedDomains >= fileMaxNumberOfSearchDomains { - // lets log all skipped domains - log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain) - continue - } + buf := prepareResolvConfContent( + searchDomainList, + append([]string{config.serverIP}, r.originalNameServers...), + r.othersConfigs) - if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit { - // lets log all skipped domains - log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain) - continue - } - - searchDomains += " " + dConf.domain - appendedDomains++ - } - - originalContent, err := os.ReadFile(fileDefaultResolvConfBackupLocation) - if err != nil { - log.Errorf("Could not read existing resolv.conf") - } - content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains, string(originalContent)) - - err = r.applyConfig(content) + err = r.applyConfig(buf) if err != nil { return err } - log.Infof("added %d search domains. Search list: %s", appendedDomains, searchDomains) + log.Infof("added %d search domains. Search list: %s", len(searchDomainList), searchDomainList) return nil } @@ -84,9 +75,9 @@ func (r *resolvconf) restoreHostDNS() error { return nil } -func (r *resolvconf) applyConfig(content string) error { +func (r *resolvconf) applyConfig(content bytes.Buffer) error { cmd := exec.Command(resolvconfCommand, "-x", "-a", r.ifaceName) - cmd.Stdin = strings.NewReader(content) + cmd.Stdin = &content _, err := cmd.Output() if err != nil { return fmt.Errorf("got an error while applying resolvconf configuration for %s interface, error: %s", r.ifaceName, err) From 63568e5e0e547b7be1833b7b41f7f4ed3c6e0fb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:45:35 +0100 Subject: [PATCH 40/43] Bump golang.org/x/image from 0.5.0 to 0.10.0 (#1285) Bumps [golang.org/x/image](https://github.com/golang/image) from 0.5.0 to 0.10.0. - [Commits](https://github.com/golang/image/compare/v0.5.0...v0.10.0) --- updated-dependencies: - dependency-name: golang.org/x/image dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2c862cf14..8c2ad8e88 100644 --- a/go.mod +++ b/go.mod @@ -140,7 +140,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/sdk v1.11.1 // indirect go.opentelemetry.io/otel/trace v1.11.1 // indirect - golang.org/x/image v0.5.0 // indirect + golang.org/x/image v0.10.0 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect diff --git a/go.sum b/go.sum index e58eb93ef..e7b26a119 100644 --- a/go.sum +++ b/go.sum @@ -747,8 +747,8 @@ golang.org/x/exp v0.0.0-20220518171630-0b5c67f07fdf/go.mod h1:yh0Ynu2b5ZUe3MQfp2 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M= +golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -989,6 +989,7 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 2f5ba96596721ef99dffdda9a6e0b77fef710212 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Nov 2023 14:45:50 +0100 Subject: [PATCH 41/43] Bump google.golang.org/grpc from 1.55.0 to 1.56.3 (#1252) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.55.0 to 1.56.3. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.55.0...v1.56.3) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8c2ad8e88..c6c8221e1 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( golang.zx2c4.com/wireguard v0.0.0-20230223181233-21636207a675 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de golang.zx2c4.com/wireguard/windows v0.5.3 - google.golang.org/grpc v1.55.0 + google.golang.org/grpc v1.56.3 google.golang.org/protobuf v1.30.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) diff --git a/go.sum b/go.sum index e7b26a119..84b8816e9 100644 --- a/go.sum +++ b/go.sum @@ -1146,8 +1146,8 @@ google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.51.0-dev/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From 1568c8aa91a6da73991e5f9feea867be9d10ddb4 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Mon, 6 Nov 2023 11:22:39 +0100 Subject: [PATCH 42/43] Add basic support of devcontainer (#1280) --- .devcontainer/Dockerfile | 14 ++++++++++++++ .devcontainer/devcontainer.json | 10 ++++++++++ .gitignore | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..012830580 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.20-bullseye + +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends\ + gettext-base=0.21-4 \ + iptables=1.8.7-1 \ + libgl1-mesa-dev=20.3.5-1 \ + xorg-dev=1:7.7+22 \ + libayatana-appindicator3-dev=0.5.5-2+deb11u2 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + + +WORKDIR /app diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..d6c9556c9 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "name": "NetBird", + "build": { + "context": "..", + "dockerfile": "Dockerfile" + }, + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "capAdd": ["NET_ADMIN", "SYS_ADMIN", "SYS_RESOURCE"], + "privileged": true +} diff --git a/.gitignore b/.gitignore index 7edcc7087..7f7f53ce8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,4 @@ infrastructure_files/setup.env infrastructure_files/setup-*.env .vscode .DS_Store -*.db +*.db \ No newline at end of file From 9b1a0c2df74c64623a20d4048df80ee7c122d370 Mon Sep 17 00:00:00 2001 From: Yury Gargay Date: Tue, 7 Nov 2023 11:14:07 +0100 Subject: [PATCH 43/43] Extend devcontainer with dind and go features (#1292) --- .devcontainer/Dockerfile | 3 ++- .devcontainer/devcontainer.json | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 012830580..70aebf578 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,7 +8,8 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ xorg-dev=1:7.7+22 \ libayatana-appindicator3-dev=0.5.5-2+deb11u2 \ && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && go install -v golang.org/x/tools/gopls@latest WORKDIR /app diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d6c9556c9..7dce7f058 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,17 @@ "context": "..", "dockerfile": "Dockerfile" }, + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/go:1": { + "version": "1.20" + } + }, "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "capAdd": ["NET_ADMIN", "SYS_ADMIN", "SYS_RESOURCE"], + "capAdd": [ + "NET_ADMIN", + "SYS_ADMIN", + "SYS_RESOURCE" + ], "privileged": true -} +} \ No newline at end of file