From c6641be94b3c120fd49513fa19c4aeabe273a3f5 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:22:01 +0100 Subject: [PATCH 01/17] [tests] Enable benchmark tests on github actions (#2961) --- .github/workflows/golang-test-linux.yml | 41 ++++++++ management/server/account_test.go | 120 +++++++++++++++--------- management/server/peer_test.go | 40 +++++--- 3 files changed, 141 insertions(+), 60 deletions(-) diff --git a/.github/workflows/golang-test-linux.yml b/.github/workflows/golang-test-linux.yml index ef66720024d..36dcb791f76 100644 --- a/.github/workflows/golang-test-linux.yml +++ b/.github/workflows/golang-test-linux.yml @@ -52,6 +52,47 @@ jobs: - name: Test run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m -p 1 ./... + benchmark: + strategy: + fail-fast: false + matrix: + arch: [ '386','amd64' ] + store: [ 'sqlite', 'postgres' ] + runs-on: ubuntu-22.04 + steps: + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.23.x" + + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev + + - name: Install 32-bit libpcap + if: matrix.arch == '386' + run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386 + + - name: Install modules + run: go mod tidy + + - name: check git status + run: git --no-pager diff --exit-code + + - name: Test + run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m -p 1 ./... + test_client_on_docker: runs-on: ubuntu-20.04 steps: diff --git a/management/server/account_test.go b/management/server/account_test.go index dbabfd3663d..4ff81260737 100644 --- a/management/server/account_test.go +++ b/management/server/account_test.go @@ -2989,18 +2989,21 @@ func peerShouldReceiveUpdate(t *testing.T, updateMessage <-chan *UpdateMessage) func BenchmarkSyncAndMarkPeer(b *testing.B) { benchCases := []struct { - name string - peers int - groups int - minMsPerOp float64 - maxMsPerOp float64 + name string + peers int + groups int + // We need different expectations for CI/CD and local runs because of the different performance characteristics + minMsPerOpLocal float64 + maxMsPerOpLocal float64 + minMsPerOpCICD float64 + maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 1, 3}, - {"Medium", 500, 100, 7, 13}, - {"Large", 5000, 200, 65, 80}, - {"Small single", 50, 10, 1, 3}, - {"Medium single", 500, 10, 7, 13}, - {"Large 5", 5000, 15, 65, 80}, + {"Small", 50, 5, 1, 3, 4, 10}, + {"Medium", 500, 100, 7, 13, 10, 60}, + {"Large", 5000, 200, 65, 80, 60, 170}, + {"Small single", 50, 10, 1, 3, 4, 60}, + {"Medium single", 500, 10, 7, 13, 10, 26}, + {"Large 5", 5000, 15, 65, 80, 60, 170}, } log.SetOutput(io.Discard) @@ -3033,12 +3036,19 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { msPerOp := float64(duration.Nanoseconds()) / float64(b.N) / 1e6 b.ReportMetric(msPerOp, "ms/op") - if msPerOp < bc.minMsPerOp { - b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, bc.minMsPerOp) + minExpected := bc.minMsPerOpLocal + maxExpected := bc.maxMsPerOpLocal + if os.Getenv("CI") == "true" { + minExpected = bc.minMsPerOpCICD + maxExpected = bc.maxMsPerOpCICD } - if msPerOp > bc.maxMsPerOp { - b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, bc.maxMsPerOp) + if msPerOp < minExpected { + b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, minExpected) + } + + if msPerOp > maxExpected { + b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, maxExpected) } }) } @@ -3046,18 +3056,21 @@ func BenchmarkSyncAndMarkPeer(b *testing.B) { func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { benchCases := []struct { - name string - peers int - groups int - minMsPerOp float64 - maxMsPerOp float64 + name string + peers int + groups int + // We need different expectations for CI/CD and local runs because of the different performance characteristics + minMsPerOpLocal float64 + maxMsPerOpLocal float64 + minMsPerOpCICD float64 + maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 102, 110}, - {"Medium", 500, 100, 105, 140}, - {"Large", 5000, 200, 160, 200}, - {"Small single", 50, 10, 102, 110}, - {"Medium single", 500, 10, 105, 140}, - {"Large 5", 5000, 15, 160, 200}, + {"Small", 50, 5, 102, 110, 102, 120}, + {"Medium", 500, 100, 105, 140, 105, 170}, + {"Large", 5000, 200, 160, 200, 160, 270}, + {"Small single", 50, 10, 102, 110, 102, 120}, + {"Medium single", 500, 10, 105, 140, 105, 170}, + {"Large 5", 5000, 15, 160, 200, 160, 270}, } log.SetOutput(io.Discard) @@ -3097,12 +3110,19 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { msPerOp := float64(duration.Nanoseconds()) / float64(b.N) / 1e6 b.ReportMetric(msPerOp, "ms/op") - if msPerOp < bc.minMsPerOp { - b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, bc.minMsPerOp) + minExpected := bc.minMsPerOpLocal + maxExpected := bc.maxMsPerOpLocal + if os.Getenv("CI") == "true" { + minExpected = bc.minMsPerOpCICD + maxExpected = bc.maxMsPerOpCICD + } + + if msPerOp < minExpected { + b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, minExpected) } - if msPerOp > bc.maxMsPerOp { - b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, bc.maxMsPerOp) + if msPerOp > maxExpected { + b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, maxExpected) } }) } @@ -3110,18 +3130,21 @@ func BenchmarkLoginPeer_ExistingPeer(b *testing.B) { func BenchmarkLoginPeer_NewPeer(b *testing.B) { benchCases := []struct { - name string - peers int - groups int - minMsPerOp float64 - maxMsPerOp float64 + name string + peers int + groups int + // We need different expectations for CI/CD and local runs because of the different performance characteristics + minMsPerOpLocal float64 + maxMsPerOpLocal float64 + minMsPerOpCICD float64 + maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 107, 120}, - {"Medium", 500, 100, 105, 140}, - {"Large", 5000, 200, 180, 220}, - {"Small single", 50, 10, 107, 120}, - {"Medium single", 500, 10, 105, 140}, - {"Large 5", 5000, 15, 180, 220}, + {"Small", 50, 5, 107, 120, 107, 140}, + {"Medium", 500, 100, 105, 140, 105, 170}, + {"Large", 5000, 200, 180, 220, 180, 320}, + {"Small single", 50, 10, 107, 120, 105, 140}, + {"Medium single", 500, 10, 105, 140, 105, 170}, + {"Large 5", 5000, 15, 180, 220, 180, 320}, } log.SetOutput(io.Discard) @@ -3161,12 +3184,19 @@ func BenchmarkLoginPeer_NewPeer(b *testing.B) { msPerOp := float64(duration.Nanoseconds()) / float64(b.N) / 1e6 b.ReportMetric(msPerOp, "ms/op") - if msPerOp < bc.minMsPerOp { - b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, bc.minMsPerOp) + minExpected := bc.minMsPerOpLocal + maxExpected := bc.maxMsPerOpLocal + if os.Getenv("CI") == "true" { + minExpected = bc.minMsPerOpCICD + maxExpected = bc.maxMsPerOpCICD + } + + if msPerOp < minExpected { + b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, minExpected) } - if msPerOp > bc.maxMsPerOp { - b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, bc.maxMsPerOp) + if msPerOp > maxExpected { + b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, maxExpected) } }) } diff --git a/management/server/peer_test.go b/management/server/peer_test.go index e5eaa20d605..b15315f9870 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -835,18 +835,21 @@ func BenchmarkGetPeers(b *testing.B) { } func BenchmarkUpdateAccountPeers(b *testing.B) { benchCases := []struct { - name string - peers int - groups int - minMsPerOp float64 - maxMsPerOp float64 + name string + peers int + groups int + // We need different expectations for CI/CD and local runs because of the different performance characteristics + minMsPerOpLocal float64 + maxMsPerOpLocal float64 + minMsPerOpCICD float64 + maxMsPerOpCICD float64 }{ - {"Small", 50, 5, 90, 120}, - {"Medium", 500, 100, 110, 140}, - {"Large", 5000, 200, 800, 1300}, - {"Small single", 50, 10, 90, 120}, - {"Medium single", 500, 10, 110, 170}, - {"Large 5", 5000, 15, 1300, 1800}, + {"Small", 50, 5, 90, 120, 90, 120}, + {"Medium", 500, 100, 110, 140, 120, 200}, + {"Large", 5000, 200, 800, 1300, 2500, 3600}, + {"Small single", 50, 10, 90, 120, 90, 120}, + {"Medium single", 500, 10, 110, 170, 120, 200}, + {"Large 5", 5000, 15, 1300, 1800, 5000, 6000}, } log.SetOutput(io.Discard) @@ -885,12 +888,19 @@ func BenchmarkUpdateAccountPeers(b *testing.B) { msPerOp := float64(duration.Nanoseconds()) / float64(b.N) / 1e6 b.ReportMetric(msPerOp, "ms/op") - if msPerOp < bc.minMsPerOp { - b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, bc.minMsPerOp) + minExpected := bc.minMsPerOpLocal + maxExpected := bc.maxMsPerOpLocal + if os.Getenv("CI") == "true" { + minExpected = bc.minMsPerOpCICD + maxExpected = bc.maxMsPerOpCICD + } + + if msPerOp < minExpected { + b.Fatalf("Benchmark %s failed: too fast (%.2f ms/op, minimum %.2f ms/op)", bc.name, msPerOp, minExpected) } - if msPerOp > bc.maxMsPerOp { - b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, bc.maxMsPerOp) + if msPerOp > maxExpected { + b.Fatalf("Benchmark %s failed: too slow (%.2f ms/op, maximum %.2f ms/op)", bc.name, msPerOp, maxExpected) } }) } From 8efad1d170d0c956652cb86fa9a6923a5e1e0f5e Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 29 Nov 2024 10:06:40 +0100 Subject: [PATCH 02/17] Add guide when signing key is not found (#2942) Some users face issues with their IdP due to signing key not being refreshed With this change we advise users to configure key refresh Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * removing leftover --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- management/server/jwtclaims/jwtValidator.go | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/management/server/jwtclaims/jwtValidator.go b/management/server/jwtclaims/jwtValidator.go index d5c1e7c9e93..b91616fa569 100644 --- a/management/server/jwtclaims/jwtValidator.go +++ b/management/server/jwtclaims/jwtValidator.go @@ -77,6 +77,8 @@ type JWTValidator struct { options Options } +var keyNotFound = errors.New("unable to find appropriate key") + // NewJWTValidator constructor func NewJWTValidator(ctx context.Context, issuer string, audienceList []string, keysLocation string, idpSignkeyRefreshEnabled bool) (*JWTValidator, error) { keys, err := getPemKeys(ctx, keysLocation) @@ -124,12 +126,18 @@ func NewJWTValidator(ctx context.Context, issuer string, audienceList []string, } publicKey, err := getPublicKey(ctx, token, keys) - if err != nil { - log.WithContext(ctx).Errorf("getPublicKey error: %s", err) - return nil, err + if err == nil { + return publicKey, nil + } + + msg := fmt.Sprintf("getPublicKey error: %s", err) + if errors.Is(err, keyNotFound) && !idpSignkeyRefreshEnabled { + msg = fmt.Sprintf("getPublicKey error: %s. You can enable key refresh by setting HttpServerConfig.IdpSignKeyRefreshEnabled to true in your management.json file and restart the service", err) } - return publicKey, nil + log.WithContext(ctx).Error(msg) + + return nil, err }, EnableAuthOnOptions: false, } @@ -229,7 +237,7 @@ func getPublicKey(ctx context.Context, token *jwt.Token, jwks *Jwks) (interface{ log.WithContext(ctx).Debugf("Key Type: %s not yet supported, please raise ticket!", jwks.Keys[k].Kty) } - return nil, errors.New("unable to find appropriate key") + return nil, keyNotFound } func getPublicKeyFromECDSA(jwk JSONWebKey) (publicKey *ecdsa.PublicKey, err error) { @@ -310,4 +318,3 @@ func getMaxAgeFromCacheHeader(ctx context.Context, cacheControl string) int { return 0 } - From f9723c9266e17aeafcc7d05d6b0e63f605bbea2f Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Fri, 29 Nov 2024 17:50:35 +0100 Subject: [PATCH 03/17] [client] Account different policiy rules for routes firewall rules (#2939) * Account different policies rules for routes firewall rules This change ensures that route firewall rules will consider source group peers in the rules generation for access control policies. This fixes the behavior where multiple policies with different levels of access was being applied to all peers in a distribution group * split function * avoid unnecessary allocation Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com> --------- Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com> --- management/server/route.go | 79 ++++++++++++++++--- management/server/route_test.go | 132 +++++++++++++++++++++++++++++++- 2 files changed, 196 insertions(+), 15 deletions(-) diff --git a/management/server/route.go b/management/server/route.go index ecb562645e6..23bea87e3b8 100644 --- a/management/server/route.go +++ b/management/server/route.go @@ -417,25 +417,82 @@ func (a *Account) getPeerRoutesFirewallRules(ctx context.Context, peerID string, continue } - policies := getAllRoutePoliciesFromGroups(a, route.AccessControlGroups) - for _, policy := range policies { - if !policy.Enabled { + distributionPeers := a.getDistributionGroupsPeers(route) + + for _, accessGroup := range route.AccessControlGroups { + policies := getAllRoutePoliciesFromGroups(a, []string{accessGroup}) + rules := a.getRouteFirewallRules(ctx, peerID, policies, route, validatedPeersMap, distributionPeers) + routesFirewallRules = append(routesFirewallRules, rules...) + } + } + + return routesFirewallRules +} + +func (a *Account) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, validatedPeersMap map[string]struct{}, distributionPeers map[string]struct{}) []*RouteFirewallRule { + var fwRules []*RouteFirewallRule + for _, policy := range policies { + if !policy.Enabled { + continue + } + + for _, rule := range policy.Rules { + if !rule.Enabled { continue } - for _, rule := range policy.Rules { - if !rule.Enabled { - continue - } + rulePeers := a.getRulePeers(rule, peerID, distributionPeers, validatedPeersMap) + rules := generateRouteFirewallRules(ctx, route, rule, rulePeers, firewallRuleDirectionIN) + fwRules = append(fwRules, rules...) + } + } + return fwRules +} - distributionGroupPeers, _ := a.getAllPeersFromGroups(ctx, route.Groups, peerID, nil, validatedPeersMap) - rules := generateRouteFirewallRules(ctx, route, rule, distributionGroupPeers, firewallRuleDirectionIN) - routesFirewallRules = append(routesFirewallRules, rules...) +func (a *Account) getRulePeers(rule *PolicyRule, peerID string, distributionPeers map[string]struct{}, validatedPeersMap map[string]struct{}) []*nbpeer.Peer { + distPeersWithPolicy := make(map[string]struct{}) + for _, id := range rule.Sources { + group := a.Groups[id] + if group == nil { + continue + } + + for _, pID := range group.Peers { + if pID == peerID { + continue + } + _, distPeer := distributionPeers[pID] + _, valid := validatedPeersMap[pID] + if distPeer && valid { + distPeersWithPolicy[pID] = struct{}{} } } } - return routesFirewallRules + distributionGroupPeers := make([]*nbpeer.Peer, 0, len(distPeersWithPolicy)) + for pID := range distPeersWithPolicy { + peer := a.Peers[pID] + if peer == nil { + continue + } + distributionGroupPeers = append(distributionGroupPeers, peer) + } + return distributionGroupPeers +} + +func (a *Account) getDistributionGroupsPeers(route *route.Route) map[string]struct{} { + distPeers := make(map[string]struct{}) + for _, id := range route.Groups { + group := a.Groups[id] + if group == nil { + continue + } + + for _, pID := range group.Peers { + distPeers[pID] = struct{}{} + } + } + return distPeers } func getDefaultPermit(route *route.Route) []*RouteFirewallRule { diff --git a/management/server/route_test.go b/management/server/route_test.go index 108f791e02c..8bf9a3aebb3 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/netip" + "sort" "testing" "time" @@ -1486,6 +1487,8 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { peerBIp = "100.65.80.39" peerCIp = "100.65.254.139" peerHIp = "100.65.29.55" + peerJIp = "100.65.29.65" + peerKIp = "100.65.29.66" ) account := &Account{ @@ -1541,6 +1544,16 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { IP: net.ParseIP(peerHIp), Status: &nbpeer.PeerStatus{}, }, + "peerJ": { + ID: "peerJ", + IP: net.ParseIP(peerJIp), + Status: &nbpeer.PeerStatus{}, + }, + "peerK": { + ID: "peerK", + IP: net.ParseIP(peerKIp), + Status: &nbpeer.PeerStatus{}, + }, }, Groups: map[string]*nbgroup.Group{ "routingPeer1": { @@ -1567,6 +1580,11 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { Name: "Route2", Peers: []string{}, }, + "route4": { + ID: "route4", + Name: "route4", + Peers: []string{}, + }, "finance": { ID: "finance", Name: "Finance", @@ -1584,6 +1602,28 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { "peerB", }, }, + "qa": { + ID: "qa", + Name: "QA", + Peers: []string{ + "peerJ", + "peerK", + }, + }, + "restrictQA": { + ID: "restrictQA", + Name: "restrictQA", + Peers: []string{ + "peerJ", + }, + }, + "unrestrictedQA": { + ID: "unrestrictedQA", + Name: "unrestrictedQA", + Peers: []string{ + "peerK", + }, + }, "contractors": { ID: "contractors", Name: "Contractors", @@ -1631,6 +1671,19 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { Groups: []string{"contractors"}, AccessControlGroups: []string{}, }, + "route4": { + ID: "route4", + Network: netip.MustParsePrefix("192.168.10.0/16"), + NetID: "route4", + NetworkType: route.IPv4Network, + PeerGroups: []string{"routingPeer1"}, + Description: "Route4", + Masquerade: false, + Metric: 9999, + Enabled: true, + Groups: []string{"qa"}, + AccessControlGroups: []string{"route4"}, + }, }, Policies: []*Policy{ { @@ -1685,6 +1738,49 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { }, }, }, + { + ID: "RuleRoute4", + Name: "RuleRoute4", + Enabled: true, + Rules: []*PolicyRule{ + { + ID: "RuleRoute4", + Name: "RuleRoute4", + Bidirectional: true, + Enabled: true, + Protocol: PolicyRuleProtocolTCP, + Action: PolicyTrafficActionAccept, + Ports: []string{"80"}, + Sources: []string{ + "restrictQA", + }, + Destinations: []string{ + "route4", + }, + }, + }, + }, + { + ID: "RuleRoute5", + Name: "RuleRoute5", + Enabled: true, + Rules: []*PolicyRule{ + { + ID: "RuleRoute5", + Name: "RuleRoute5", + Bidirectional: true, + Enabled: true, + Protocol: PolicyRuleProtocolALL, + Action: PolicyTrafficActionAccept, + Sources: []string{ + "unrestrictedQA", + }, + Destinations: []string{ + "route4", + }, + }, + }, + }, }, } @@ -1709,7 +1805,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { t.Run("check peer routes firewall rules", func(t *testing.T) { routesFirewallRules := account.getPeerRoutesFirewallRules(context.Background(), "peerA", validatedPeers) - assert.Len(t, routesFirewallRules, 2) + assert.Len(t, routesFirewallRules, 4) expectedRoutesFirewallRules := []*RouteFirewallRule{ { @@ -1735,12 +1831,32 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { Port: 320, }, } - assert.ElementsMatch(t, routesFirewallRules, expectedRoutesFirewallRules) + additionalFirewallRule := []*RouteFirewallRule{ + { + SourceRanges: []string{ + fmt.Sprintf(AllowedIPsFormat, peerJIp), + }, + Action: "accept", + Destination: "192.168.10.0/16", + Protocol: "tcp", + Port: 80, + }, + { + SourceRanges: []string{ + fmt.Sprintf(AllowedIPsFormat, peerKIp), + }, + Action: "accept", + Destination: "192.168.10.0/16", + Protocol: "all", + }, + } + + assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(append(expectedRoutesFirewallRules, additionalFirewallRule...))) // peerD is also the routing peer for route1, should contain same routes firewall rules as peerA routesFirewallRules = account.getPeerRoutesFirewallRules(context.Background(), "peerD", validatedPeers) assert.Len(t, routesFirewallRules, 2) - assert.ElementsMatch(t, routesFirewallRules, expectedRoutesFirewallRules) + assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules)) // peerE is a single routing peer for route 2 and route 3 routesFirewallRules = account.getPeerRoutesFirewallRules(context.Background(), "peerE", validatedPeers) @@ -1769,7 +1885,7 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { IsDynamic: true, }, } - assert.ElementsMatch(t, routesFirewallRules, expectedRoutesFirewallRules) + assert.ElementsMatch(t, orderRuleSourceRanges(routesFirewallRules), orderRuleSourceRanges(expectedRoutesFirewallRules)) // peerC is part of route1 distribution groups but should not receive the routes firewall rules routesFirewallRules = account.getPeerRoutesFirewallRules(context.Background(), "peerC", validatedPeers) @@ -1778,6 +1894,14 @@ func TestAccount_getPeersRoutesFirewall(t *testing.T) { } +// orderList is a helper function to sort a list of strings +func orderRuleSourceRanges(ruleList []*RouteFirewallRule) []*RouteFirewallRule { + for _, rule := range ruleList { + sort.Strings(rule.SourceRanges) + } + return ruleList +} + func TestRouteAccountPeersUpdate(t *testing.T) { manager, err := createRouterManager(t) require.NoError(t, err, "failed to create account manager") From e52d352a48aac55caada063ef44bd7e79e173bdb Mon Sep 17 00:00:00 2001 From: v1rusnl <18641204+v1rusnl@users.noreply.github.com> Date: Sat, 30 Nov 2024 10:26:31 +0100 Subject: [PATCH 04/17] Update Caddyfile and Docker Compose to support HTTP3 (#2822) --- infrastructure_files/getting-started-with-zitadel.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infrastructure_files/getting-started-with-zitadel.sh b/infrastructure_files/getting-started-with-zitadel.sh index 0b2b651429e..7793d1fda1d 100644 --- a/infrastructure_files/getting-started-with-zitadel.sh +++ b/infrastructure_files/getting-started-with-zitadel.sh @@ -530,7 +530,7 @@ renderCaddyfile() { { debug servers :80,:443 { - protocols h1 h2c + protocols h1 h2c h3 } } @@ -788,6 +788,7 @@ services: networks: [ netbird ] ports: - '443:443' + - '443:443/udp' - '80:80' - '8080:8080' volumes: From e4a5fb3e91bfbe202d3f13f66599f6ebfd9fe77e Mon Sep 17 00:00:00 2001 From: victorserbu2709 Date: Sat, 30 Nov 2024 11:34:52 +0200 Subject: [PATCH 05/17] Unspecified address: default NetworkTypeUDP4+NetworkTypeUDP6 (#2804) --- client/iface/bind/udp_mux.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/iface/bind/udp_mux.go b/client/iface/bind/udp_mux.go index 12f7a812970..00a91f0ecad 100644 --- a/client/iface/bind/udp_mux.go +++ b/client/iface/bind/udp_mux.go @@ -162,12 +162,13 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") var networks []ice.NetworkType switch { - case addr.IP.To4() != nil: - networks = []ice.NetworkType{ice.NetworkTypeUDP4} case addr.IP.To16() != nil: networks = []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6} + case addr.IP.To4() != nil: + networks = []ice.NetworkType{ice.NetworkTypeUDP4} + default: params.Logger.Errorf("LocalAddr expected IPV4 or IPV6, got %T", params.UDPConn.LocalAddr()) } From ecb44ff3065156ba0d548b4c4fe04c927b0d947e Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 1 Dec 2024 19:22:52 +0100 Subject: [PATCH 06/17] [client] Add pprof build tag (#2964) * Add pprof build tag * Change env handling --- client/cmd/pprof.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 client/cmd/pprof.go diff --git a/client/cmd/pprof.go b/client/cmd/pprof.go new file mode 100644 index 00000000000..37efd35f0cd --- /dev/null +++ b/client/cmd/pprof.go @@ -0,0 +1,33 @@ +//go:build pprof +// +build pprof + +package cmd + +import ( + "net/http" + _ "net/http/pprof" + "os" + + log "github.com/sirupsen/logrus" +) + +func init() { + addr := pprofAddr() + go pprof(addr) +} + +func pprofAddr() string { + listenAddr := os.Getenv("NB_PPROF_ADDR") + if listenAddr == "" { + return "localhost:6969" + } + + return listenAddr +} + +func pprof(listenAddr string) { + log.Infof("listening pprof on: %s\n", listenAddr) + if err := http.ListenAndServe(listenAddr, nil); err != nil { + log.Fatalf("Failed to start pprof: %v", err) + } +} From 5142dc52c11ad9841fe8ae229ed585d27f976503 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 2 Dec 2024 17:55:02 +0100 Subject: [PATCH 07/17] [client] Persist route selection (#2810) --- client/firewall/iptables/rulestore_linux.go | 10 ++ client/internal/engine.go | 13 +- client/internal/engine_test.go | 5 +- client/internal/routemanager/manager.go | 38 ++++- client/internal/routemanager/manager_test.go | 4 +- client/internal/routemanager/mock.go | 2 +- .../routemanager/refcounter/refcounter.go | 13 ++ client/internal/routemanager/state.go | 19 +++ .../internal/routeselector/routeselector.go | 67 +++++++++ client/internal/statemanager/manager.go | 142 ++++++++++++++---- 10 files changed, 273 insertions(+), 40 deletions(-) create mode 100644 client/internal/routemanager/state.go diff --git a/client/firewall/iptables/rulestore_linux.go b/client/firewall/iptables/rulestore_linux.go index bfd08bee27d..004c512a4b4 100644 --- a/client/firewall/iptables/rulestore_linux.go +++ b/client/firewall/iptables/rulestore_linux.go @@ -37,6 +37,11 @@ func (s *ipList) UnmarshalJSON(data []byte) error { return err } s.ips = temp.IPs + + if temp.IPs == nil { + temp.IPs = make(map[string]struct{}) + } + return nil } @@ -89,5 +94,10 @@ func (s *ipsetStore) UnmarshalJSON(data []byte) error { return err } s.ipsets = temp.IPSets + + if temp.IPSets == nil { + temp.IPSets = make(map[string]*ipList) + } + return nil } diff --git a/client/internal/engine.go b/client/internal/engine.go index dc4499e17f3..920c295cdde 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -349,8 +349,17 @@ func (e *Engine) Start() error { } e.dnsServer = dnsServer - e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.config.DNSRouteInterval, e.wgInterface, e.statusRecorder, e.relayManager, initialRoutes) - beforePeerHook, afterPeerHook, err := e.routeManager.Init(e.stateManager) + e.routeManager = routemanager.NewManager( + e.ctx, + e.config.WgPrivateKey.PublicKey().String(), + e.config.DNSRouteInterval, + e.wgInterface, + e.statusRecorder, + e.relayManager, + initialRoutes, + e.stateManager, + ) + beforePeerHook, afterPeerHook, err := e.routeManager.Init() if err != nil { log.Errorf("Failed to initialize route manager: %s", err) } else { diff --git a/client/internal/engine_test.go b/client/internal/engine_test.go index b6c6186ea83..b58c1f7e93a 100644 --- a/client/internal/engine_test.go +++ b/client/internal/engine_test.go @@ -245,12 +245,15 @@ func TestEngine_UpdateNetworkMap(t *testing.T) { nil) wgIface := &iface.MockWGIface{ + NameFunc: func() string { return "utun102" }, RemovePeerFunc: func(peerKey string) error { return nil }, } engine.wgInterface = wgIface - engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), time.Minute, engine.wgInterface, engine.statusRecorder, relayMgr, nil) + engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), time.Minute, engine.wgInterface, engine.statusRecorder, relayMgr, nil, nil) + _, _, err = engine.routeManager.Init() + require.NoError(t, err) engine.dnsServer = &dns.MockServer{ UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil }, } diff --git a/client/internal/routemanager/manager.go b/client/internal/routemanager/manager.go index 0a1c7dc56b8..f1c4ae5ef75 100644 --- a/client/internal/routemanager/manager.go +++ b/client/internal/routemanager/manager.go @@ -32,7 +32,7 @@ import ( // Manager is a route manager interface type Manager interface { - Init(*statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) + Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) (map[route.ID]*route.Route, route.HAMap, error) TriggerSelection(route.HAMap) GetRouteSelector() *routeselector.RouteSelector @@ -59,6 +59,7 @@ type DefaultManager struct { routeRefCounter *refcounter.RouteRefCounter allowedIPsRefCounter *refcounter.AllowedIPsRefCounter dnsRouteInterval time.Duration + stateManager *statemanager.Manager } func NewManager( @@ -69,6 +70,7 @@ func NewManager( statusRecorder *peer.Status, relayMgr *relayClient.Manager, initialRoutes []*route.Route, + stateManager *statemanager.Manager, ) *DefaultManager { mCTX, cancel := context.WithCancel(ctx) notifier := notifier.NewNotifier() @@ -80,12 +82,12 @@ func NewManager( dnsRouteInterval: dnsRouteInterval, clientNetworks: make(map[route.HAUniqueID]*clientNetwork), relayMgr: relayMgr, - routeSelector: routeselector.NewRouteSelector(), sysOps: sysOps, statusRecorder: statusRecorder, wgInterface: wgInterface, pubKey: pubKey, notifier: notifier, + stateManager: stateManager, } dm.routeRefCounter = refcounter.New( @@ -121,7 +123,7 @@ func NewManager( } // Init sets up the routing -func (m *DefaultManager) Init(stateManager *statemanager.Manager) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { +func (m *DefaultManager) Init() (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) { if nbnet.CustomRoutingDisabled() { return nil, nil, nil } @@ -137,14 +139,38 @@ func (m *DefaultManager) Init(stateManager *statemanager.Manager) (nbnet.AddHook ips := resolveURLsToIPs(initialAddresses) - beforePeerHook, afterPeerHook, err := m.sysOps.SetupRouting(ips, stateManager) + beforePeerHook, afterPeerHook, err := m.sysOps.SetupRouting(ips, m.stateManager) if err != nil { return nil, nil, fmt.Errorf("setup routing: %w", err) } + + m.routeSelector = m.initSelector() + log.Info("Routing setup complete") return beforePeerHook, afterPeerHook, nil } +func (m *DefaultManager) initSelector() *routeselector.RouteSelector { + var state *SelectorState + m.stateManager.RegisterState(state) + + // restore selector state if it exists + if err := m.stateManager.LoadState(state); err != nil { + log.Warnf("failed to load state: %v", err) + return routeselector.NewRouteSelector() + } + + if state := m.stateManager.GetState(state); state != nil { + if selector, ok := state.(*SelectorState); ok { + return (*routeselector.RouteSelector)(selector) + } + + log.Warnf("failed to convert state with type %T to SelectorState", state) + } + + return routeselector.NewRouteSelector() +} + func (m *DefaultManager) EnableServerRouter(firewall firewall.Manager) error { var err error m.serverRouter, err = newServerRouter(m.ctx, m.wgInterface, firewall, m.statusRecorder) @@ -252,6 +278,10 @@ func (m *DefaultManager) TriggerSelection(networks route.HAMap) { go clientNetworkWatcher.peersStateAndUpdateWatcher() clientNetworkWatcher.sendUpdateToClientNetworkWatcher(routesUpdate{routes: routes}) } + + if err := m.stateManager.UpdateState((*SelectorState)(m.routeSelector)); err != nil { + log.Errorf("failed to update state: %v", err) + } } // stopObsoleteClients stops the client network watcher for the networks that are not in the new list diff --git a/client/internal/routemanager/manager_test.go b/client/internal/routemanager/manager_test.go index e669bc44a08..07dac21b819 100644 --- a/client/internal/routemanager/manager_test.go +++ b/client/internal/routemanager/manager_test.go @@ -424,9 +424,9 @@ func TestManagerUpdateRoutes(t *testing.T) { statusRecorder := peer.NewRecorder("https://mgm") ctx := context.TODO() - routeManager := NewManager(ctx, localPeerKey, 0, wgInterface, statusRecorder, nil, nil) + routeManager := NewManager(ctx, localPeerKey, 0, wgInterface, statusRecorder, nil, nil, nil) - _, _, err = routeManager.Init(nil) + _, _, err = routeManager.Init() require.NoError(t, err, "should init route manager") defer routeManager.Stop(nil) diff --git a/client/internal/routemanager/mock.go b/client/internal/routemanager/mock.go index 503185f0311..556a6235138 100644 --- a/client/internal/routemanager/mock.go +++ b/client/internal/routemanager/mock.go @@ -21,7 +21,7 @@ type MockManager struct { StopFunc func(manager *statemanager.Manager) } -func (m *MockManager) Init(*statemanager.Manager) (net.AddHookFunc, net.RemoveHookFunc, error) { +func (m *MockManager) Init() (net.AddHookFunc, net.RemoveHookFunc, error) { return nil, nil, nil } diff --git a/client/internal/routemanager/refcounter/refcounter.go b/client/internal/routemanager/refcounter/refcounter.go index f2f0a169df0..27a724f5062 100644 --- a/client/internal/routemanager/refcounter/refcounter.go +++ b/client/internal/routemanager/refcounter/refcounter.go @@ -71,11 +71,14 @@ func New[Key comparable, I, O any](add AddFunc[Key, I, O], remove RemoveFunc[Key } // LoadData loads the data from the existing counter +// The passed counter should not be used any longer after calling this function. func (rm *Counter[Key, I, O]) LoadData( existingCounter *Counter[Key, I, O], ) { rm.mu.Lock() defer rm.mu.Unlock() + existingCounter.mu.Lock() + defer existingCounter.mu.Unlock() rm.refCountMap = existingCounter.refCountMap rm.idMap = existingCounter.idMap @@ -231,6 +234,9 @@ func (rm *Counter[Key, I, O]) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the json.Unmarshaler interface for Counter. func (rm *Counter[Key, I, O]) UnmarshalJSON(data []byte) error { + rm.mu.Lock() + defer rm.mu.Unlock() + var temp struct { RefCountMap map[Key]Ref[O] `json:"refCountMap"` IDMap map[string][]Key `json:"idMap"` @@ -241,6 +247,13 @@ func (rm *Counter[Key, I, O]) UnmarshalJSON(data []byte) error { rm.refCountMap = temp.RefCountMap rm.idMap = temp.IDMap + if temp.RefCountMap == nil { + temp.RefCountMap = map[Key]Ref[O]{} + } + if temp.IDMap == nil { + temp.IDMap = map[string][]Key{} + } + return nil } diff --git a/client/internal/routemanager/state.go b/client/internal/routemanager/state.go new file mode 100644 index 00000000000..a45c32b506d --- /dev/null +++ b/client/internal/routemanager/state.go @@ -0,0 +1,19 @@ +package routemanager + +import ( + "github.com/netbirdio/netbird/client/internal/routeselector" +) + +type SelectorState routeselector.RouteSelector + +func (s *SelectorState) Name() string { + return "routeselector_state" +} + +func (s *SelectorState) MarshalJSON() ([]byte, error) { + return (*routeselector.RouteSelector)(s).MarshalJSON() +} + +func (s *SelectorState) UnmarshalJSON(data []byte) error { + return (*routeselector.RouteSelector)(s).UnmarshalJSON(data) +} diff --git a/client/internal/routeselector/routeselector.go b/client/internal/routeselector/routeselector.go index 00128a27b03..2874604fdd3 100644 --- a/client/internal/routeselector/routeselector.go +++ b/client/internal/routeselector/routeselector.go @@ -1,8 +1,10 @@ package routeselector import ( + "encoding/json" "fmt" "slices" + "sync" "github.com/hashicorp/go-multierror" "golang.org/x/exp/maps" @@ -12,6 +14,7 @@ import ( ) type RouteSelector struct { + mu sync.RWMutex selectedRoutes map[route.NetID]struct{} selectAll bool } @@ -26,6 +29,9 @@ func NewRouteSelector() *RouteSelector { // SelectRoutes updates the selected routes based on the provided route IDs. func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, allRoutes []route.NetID) error { + rs.mu.Lock() + defer rs.mu.Unlock() + if !appendRoute { rs.selectedRoutes = map[route.NetID]struct{}{} } @@ -46,6 +52,9 @@ func (rs *RouteSelector) SelectRoutes(routes []route.NetID, appendRoute bool, al // SelectAllRoutes sets the selector to select all routes. func (rs *RouteSelector) SelectAllRoutes() { + rs.mu.Lock() + defer rs.mu.Unlock() + rs.selectAll = true rs.selectedRoutes = map[route.NetID]struct{}{} } @@ -53,6 +62,9 @@ func (rs *RouteSelector) SelectAllRoutes() { // DeselectRoutes removes specific routes from the selection. // If the selector is in "select all" mode, it will transition to "select specific" mode. func (rs *RouteSelector) DeselectRoutes(routes []route.NetID, allRoutes []route.NetID) error { + rs.mu.Lock() + defer rs.mu.Unlock() + if rs.selectAll { rs.selectAll = false rs.selectedRoutes = map[route.NetID]struct{}{} @@ -76,12 +88,18 @@ func (rs *RouteSelector) DeselectRoutes(routes []route.NetID, allRoutes []route. // DeselectAllRoutes deselects all routes, effectively disabling route selection. func (rs *RouteSelector) DeselectAllRoutes() { + rs.mu.Lock() + defer rs.mu.Unlock() + rs.selectAll = false rs.selectedRoutes = map[route.NetID]struct{}{} } // IsSelected checks if a specific route is selected. func (rs *RouteSelector) IsSelected(routeID route.NetID) bool { + rs.mu.RLock() + defer rs.mu.RUnlock() + if rs.selectAll { return true } @@ -91,6 +109,9 @@ func (rs *RouteSelector) IsSelected(routeID route.NetID) bool { // FilterSelected removes unselected routes from the provided map. func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap { + rs.mu.RLock() + defer rs.mu.RUnlock() + if rs.selectAll { return maps.Clone(routes) } @@ -103,3 +124,49 @@ func (rs *RouteSelector) FilterSelected(routes route.HAMap) route.HAMap { } return filtered } + +// MarshalJSON implements the json.Marshaler interface +func (rs *RouteSelector) MarshalJSON() ([]byte, error) { + rs.mu.RLock() + defer rs.mu.RUnlock() + + return json.Marshal(struct { + SelectedRoutes map[route.NetID]struct{} `json:"selected_routes"` + SelectAll bool `json:"select_all"` + }{ + SelectAll: rs.selectAll, + SelectedRoutes: rs.selectedRoutes, + }) +} + +// UnmarshalJSON implements the json.Unmarshaler interface +// If the JSON is empty or null, it will initialize like a NewRouteSelector. +func (rs *RouteSelector) UnmarshalJSON(data []byte) error { + rs.mu.Lock() + defer rs.mu.Unlock() + + // Check for null or empty JSON + if len(data) == 0 || string(data) == "null" { + rs.selectedRoutes = map[route.NetID]struct{}{} + rs.selectAll = true + return nil + } + + var temp struct { + SelectedRoutes map[route.NetID]struct{} `json:"selected_routes"` + SelectAll bool `json:"select_all"` + } + + if err := json.Unmarshal(data, &temp); err != nil { + return err + } + + rs.selectedRoutes = temp.SelectedRoutes + rs.selectAll = temp.SelectAll + + if rs.selectedRoutes == nil { + rs.selectedRoutes = map[route.NetID]struct{}{} + } + + return nil +} diff --git a/client/internal/statemanager/manager.go b/client/internal/statemanager/manager.go index da6dd022fc2..aae73b79f5e 100644 --- a/client/internal/statemanager/manager.go +++ b/client/internal/statemanager/manager.go @@ -22,9 +22,28 @@ import ( // State interface defines the methods that all state types must implement type State interface { Name() string +} + +// CleanableState interface extends State with cleanup capability +type CleanableState interface { + State Cleanup() error } +// RawState wraps raw JSON data for unregistered states +type RawState struct { + data json.RawMessage +} + +func (r *RawState) Name() string { + return "" // This is a placeholder implementation +} + +// MarshalJSON implements json.Marshaler to preserve the original JSON +func (r *RawState) MarshalJSON() ([]byte, error) { + return r.data, nil +} + // Manager handles the persistence and management of various states type Manager struct { mu sync.Mutex @@ -209,15 +228,15 @@ func (m *Manager) PersistState(ctx context.Context) error { return nil } -// loadState loads the existing state from the state file -func (m *Manager) loadState() error { +// loadStateFile reads and unmarshals the state file into a map of raw JSON messages +func (m *Manager) loadStateFile() (map[string]json.RawMessage, error) { data, err := os.ReadFile(m.filePath) if err != nil { if errors.Is(err, fs.ErrNotExist) { log.Debug("state file does not exist") - return nil + return nil, nil // nolint:nilnil } - return fmt.Errorf("read state file: %w", err) + return nil, fmt.Errorf("read state file: %w", err) } var rawStates map[string]json.RawMessage @@ -228,37 +247,69 @@ func (m *Manager) loadState() error { } else { log.Info("State file deleted") } - return fmt.Errorf("unmarshal states: %w", err) + return nil, fmt.Errorf("unmarshal states: %w", err) } - var merr *multierror.Error + return rawStates, nil +} - for name, rawState := range rawStates { - stateType, ok := m.stateTypes[name] - if !ok { - merr = multierror.Append(merr, fmt.Errorf("unknown state type: %s", name)) - continue - } +// loadSingleRawState unmarshals a raw state into a concrete state object +func (m *Manager) loadSingleRawState(name string, rawState json.RawMessage) (State, error) { + stateType, ok := m.stateTypes[name] + if !ok { + return nil, fmt.Errorf("state %s not registered", name) + } - if string(rawState) == "null" { - continue - } + if string(rawState) == "null" { + return nil, nil //nolint:nilnil + } - statePtr := reflect.New(stateType).Interface().(State) - if err := json.Unmarshal(rawState, statePtr); err != nil { - merr = multierror.Append(merr, fmt.Errorf("unmarshal state %s: %w", name, err)) - continue - } + statePtr := reflect.New(stateType).Interface().(State) + if err := json.Unmarshal(rawState, statePtr); err != nil { + return nil, fmt.Errorf("unmarshal state %s: %w", name, err) + } + + return statePtr, nil +} + +// LoadState loads a specific state from the state file +func (m *Manager) LoadState(state State) error { + if m == nil { + return nil + } + + m.mu.Lock() + defer m.mu.Unlock() - m.states[name] = statePtr + rawStates, err := m.loadStateFile() + if err != nil { + return err + } + if rawStates == nil { + return nil + } + + name := state.Name() + rawState, exists := rawStates[name] + if !exists { + return nil + } + + loadedState, err := m.loadSingleRawState(name, rawState) + if err != nil { + return err + } + + m.states[name] = loadedState + if loadedState != nil { log.Debugf("loaded state: %s", name) } - return nberrors.FormatErrorOrNil(merr) + return nil } -// PerformCleanup retrieves all states from the state file for the registered states and calls Cleanup on them. -// If the cleanup is successful, the state is marked for deletion. +// PerformCleanup retrieves all states from the state file and calls Cleanup on registered states that support it. +// Unregistered states are preserved in their original state. func (m *Manager) PerformCleanup() error { if m == nil { return nil @@ -267,22 +318,53 @@ func (m *Manager) PerformCleanup() error { m.mu.Lock() defer m.mu.Unlock() - if err := m.loadState(); err != nil { + // Load raw states from file + rawStates, err := m.loadStateFile() + if err != nil { log.Warnf("Failed to load state during cleanup: %v", err) + return err + } + if rawStates == nil { + return nil } var merr *multierror.Error - for name, state := range m.states { - if state == nil { - // If no state was found in the state file, we don't mark the state dirty nor return an error + + // Process each state in the file + for name, rawState := range rawStates { + // For unregistered states, preserve the raw JSON + if _, registered := m.stateTypes[name]; !registered { + m.states[name] = &RawState{data: rawState} + continue + } + + // Load the registered state + loadedState, err := m.loadSingleRawState(name, rawState) + if err != nil { + merr = multierror.Append(merr, err) + continue + } + + if loadedState == nil { + continue + } + + // Check if state supports cleanup + cleanableState, isCleanable := loadedState.(CleanableState) + if !isCleanable { + // If it doesn't support cleanup, keep it as-is + m.states[name] = loadedState continue } + // Perform cleanup for cleanable states log.Infof("client was not shut down properly, cleaning up %s", name) - if err := state.Cleanup(); err != nil { + if err := cleanableState.Cleanup(); err != nil { merr = multierror.Append(merr, fmt.Errorf("cleanup state for %s: %w", name, err)) + // On cleanup error, preserve the state + m.states[name] = loadedState } else { - // mark for deletion on cleanup success + // Successfully cleaned up - mark for deletion m.states[name] = nil m.dirty[name] = struct{}{} } From c7e7ad5030cd695f1ba1ad2c819b2a8a551a90d8 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:04:02 +0100 Subject: [PATCH 08/17] [client] Add state file to debug bundle (#2969) --- client/anonymize/anonymize.go | 4 +- client/anonymize/anonymize_test.go | 8 + client/server/debug.go | 178 +++++++++++++++++--- client/server/debug_test.go | 258 +++++++++++++++++++++++++++++ 4 files changed, 425 insertions(+), 23 deletions(-) create mode 100644 client/server/debug_test.go diff --git a/client/anonymize/anonymize.go b/client/anonymize/anonymize.go index 7ebe0442dfa..ad31682f25a 100644 --- a/client/anonymize/anonymize.go +++ b/client/anonymize/anonymize.go @@ -152,9 +152,9 @@ func (a *Anonymizer) AnonymizeString(str string) string { return str } -// AnonymizeSchemeURI finds and anonymizes URIs with stun, stuns, turn, and turns schemes. +// AnonymizeSchemeURI finds and anonymizes URIs with ws, wss, rel, rels, stun, stuns, turn, and turns schemes. func (a *Anonymizer) AnonymizeSchemeURI(text string) string { - re := regexp.MustCompile(`(?i)\b(stuns?:|turns?:|https?://)\S+\b`) + re := regexp.MustCompile(`(?i)\b(wss?://|rels?://|stuns?:|turns?:|https?://)\S+\b`) return re.ReplaceAllStringFunc(text, a.AnonymizeURI) } diff --git a/client/anonymize/anonymize_test.go b/client/anonymize/anonymize_test.go index e660749ec5d..605788ab54a 100644 --- a/client/anonymize/anonymize_test.go +++ b/client/anonymize/anonymize_test.go @@ -140,8 +140,16 @@ func TestAnonymizeSchemeURI(t *testing.T) { expect string }{ {"STUN URI in text", "Connection made via stun:example.com", `Connection made via stun:anon-[a-zA-Z0-9]+\.domain`}, + {"STUNS URI in message", "Secure connection to stuns:example.com:443", `Secure connection to stuns:anon-[a-zA-Z0-9]+\.domain:443`}, {"TURN URI in log", "Failed attempt turn:some.example.com:3478?transport=tcp: retrying", `Failed attempt turn:some.anon-[a-zA-Z0-9]+\.domain:3478\?transport=tcp: retrying`}, + {"TURNS URI in message", "Secure connection to turns:example.com:5349", `Secure connection to turns:anon-[a-zA-Z0-9]+\.domain:5349`}, + {"HTTP URI in text", "Visit http://example.com for more", `Visit http://anon-[a-zA-Z0-9]+\.domain for more`}, + {"HTTPS URI in CAPS", "Visit HTTPS://example.com for more", `Visit https://anon-[a-zA-Z0-9]+\.domain for more`}, {"HTTPS URI in message", "Visit https://example.com for more", `Visit https://anon-[a-zA-Z0-9]+\.domain for more`}, + {"WS URI in log", "Connection established to ws://example.com:8080", `Connection established to ws://anon-[a-zA-Z0-9]+\.domain:8080`}, + {"WSS URI in message", "Secure connection to wss://example.com", `Secure connection to wss://anon-[a-zA-Z0-9]+\.domain`}, + {"Rel URI in text", "Relaying to rel://example.com", `Relaying to rel://anon-[a-zA-Z0-9]+\.domain`}, + {"Rels URI in message", "Relaying to rels://example.com", `Relaying to rels://anon-[a-zA-Z0-9]+\.domain`}, } for _, tc := range tests { diff --git a/client/server/debug.go b/client/server/debug.go index 5ed43293b4a..1bad907ba56 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -5,9 +5,13 @@ package server import ( "archive/zip" "bufio" + "bytes" "context" + "encoding/json" + "errors" "fmt" "io" + "io/fs" "net" "net/netip" "os" @@ -20,6 +24,7 @@ import ( "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" + "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/proto" ) @@ -31,6 +36,7 @@ client.log: Most recent, anonymized log file of the NetBird client. routes.txt: Anonymized system routes, if --system-info flag was provided. interfaces.txt: Anonymized network interface information, if --system-info flag was provided. config.txt: Anonymized configuration information of the NetBird client. +state.json: Anonymized client state dump containing netbird states. Anonymization Process @@ -50,8 +56,22 @@ Domains All domain names (except for the netbird domains) are replaced with randomly generated strings ending in ".domain". Anonymized domains are consistent across all files in the bundle. Reoccuring domain names are replaced with the same anonymized domain. +State File +The state.json file contains anonymized internal state information of the NetBird client, including: +- DNS settings and configuration +- Firewall rules +- Exclusion routes +- Route selection +- Other internal states that may be present + +The state file follows the same anonymization rules as other files: +- IP addresses (both individual and CIDR ranges) are anonymized while preserving their structure +- Domain names are consistently anonymized +- Technical identifiers and non-sensitive data remain unchanged + Routes For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. + Network Interfaces The interfaces.txt file contains information about network interfaces, including: - Interface name @@ -132,6 +152,10 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques } } + if err := s.addStateFile(req, anonymizer, archive); err != nil { + log.Errorf("Failed to add state file to debug bundle: %v", err) + } + if err := s.addLogfile(req, anonymizer, archive); err != nil { return fmt.Errorf("add log file: %w", err) } @@ -248,6 +272,44 @@ func (s *Server) addInterfaces(req *proto.DebugBundleRequest, anonymizer *anonym return nil } +func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + path := statemanager.GetDefaultStatePath() + if path == "" { + return nil + } + + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return fmt.Errorf("read state file: %w", err) + } + + if req.GetAnonymize() { + var rawStates map[string]json.RawMessage + if err := json.Unmarshal(data, &rawStates); err != nil { + return fmt.Errorf("unmarshal states: %w", err) + } + + if err := anonymizeStateFile(&rawStates, anonymizer); err != nil { + return fmt.Errorf("anonymize state file: %w", err) + } + + bs, err := json.MarshalIndent(rawStates, "", " ") + if err != nil { + return fmt.Errorf("marshal states: %w", err) + } + data = bs + } + + if err := addFileToZip(archive, bytes.NewReader(data), "state.json"); err != nil { + return fmt.Errorf("add state file to zip: %w", err) + } + + return nil +} + func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) (err error) { logFile, err := os.Open(s.logFile) if err != nil { @@ -264,7 +326,7 @@ func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize var writer *io.PipeWriter logReader, writer = io.Pipe() - go s.anonymize(logFile, writer, anonymizer) + go anonymizeLog(logFile, writer, anonymizer) } else { logReader = logFile } @@ -275,26 +337,6 @@ func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize return nil } -func (s *Server) anonymize(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) { - defer func() { - // always nil - _ = writer.Close() - }() - - scanner := bufio.NewScanner(reader) - for scanner.Scan() { - line := anonymizer.AnonymizeString(scanner.Text()) - if _, err := writer.Write([]byte(line + "\n")); err != nil { - writer.CloseWithError(fmt.Errorf("anonymize write: %w", err)) - return - } - } - if err := scanner.Err(); err != nil { - writer.CloseWithError(fmt.Errorf("anonymize scan: %w", err)) - return - } -} - // GetLogLevel gets the current logging level for the server. func (s *Server) GetLogLevel(_ context.Context, _ *proto.GetLogLevelRequest) (*proto.GetLogLevelResponse, error) { level := ParseLogLevel(log.GetLevel().String()) @@ -458,6 +500,26 @@ func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *an return builder.String() } +func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) { + defer func() { + // always nil + _ = writer.Close() + }() + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := anonymizer.AnonymizeString(scanner.Text()) + if _, err := writer.Write([]byte(line + "\n")); err != nil { + writer.CloseWithError(fmt.Errorf("anonymize write: %w", err)) + return + } + } + if err := scanner.Err(); err != nil { + writer.CloseWithError(fmt.Errorf("anonymize scan: %w", err)) + return + } +} + func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []string { anonymizedIPs := make([]string, len(ips)) for i, ip := range ips { @@ -484,3 +546,77 @@ func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []s } return anonymizedIPs } + +func anonymizeStateFile(rawStates *map[string]json.RawMessage, anonymizer *anonymize.Anonymizer) error { + for name, rawState := range *rawStates { + if string(rawState) == "null" { + continue + } + + var state map[string]any + if err := json.Unmarshal(rawState, &state); err != nil { + return fmt.Errorf("unmarshal state %s: %w", name, err) + } + + state = anonymizeValue(state, anonymizer).(map[string]any) + + bs, err := json.Marshal(state) + if err != nil { + return fmt.Errorf("marshal state %s: %w", name, err) + } + + (*rawStates)[name] = bs + } + + return nil +} + +func anonymizeValue(value any, anonymizer *anonymize.Anonymizer) any { + switch v := value.(type) { + case string: + return anonymizeString(v, anonymizer) + case map[string]any: + return anonymizeMap(v, anonymizer) + case []any: + return anonymizeSlice(v, anonymizer) + } + return value +} + +func anonymizeString(v string, anonymizer *anonymize.Anonymizer) string { + if prefix, err := netip.ParsePrefix(v); err == nil { + anonIP := anonymizer.AnonymizeIP(prefix.Addr()) + return fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) + } + if ip, err := netip.ParseAddr(v); err == nil { + return anonymizer.AnonymizeIP(ip).String() + } + return anonymizer.AnonymizeString(v) +} + +func anonymizeMap(v map[string]any, anonymizer *anonymize.Anonymizer) map[string]any { + result := make(map[string]any, len(v)) + for key, val := range v { + newKey := anonymizeMapKey(key, anonymizer) + result[newKey] = anonymizeValue(val, anonymizer) + } + return result +} + +func anonymizeMapKey(key string, anonymizer *anonymize.Anonymizer) string { + if prefix, err := netip.ParsePrefix(key); err == nil { + anonIP := anonymizer.AnonymizeIP(prefix.Addr()) + return fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) + } + if ip, err := netip.ParseAddr(key); err == nil { + return anonymizer.AnonymizeIP(ip).String() + } + return key +} + +func anonymizeSlice(v []any, anonymizer *anonymize.Anonymizer) []any { + for i, val := range v { + v[i] = anonymizeValue(val, anonymizer) + } + return v +} diff --git a/client/server/debug_test.go b/client/server/debug_test.go new file mode 100644 index 00000000000..303e5e66166 --- /dev/null +++ b/client/server/debug_test.go @@ -0,0 +1,258 @@ +package server + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/client/anonymize" +) + +func TestAnonymizeStateFile(t *testing.T) { + testState := map[string]json.RawMessage{ + "null_state": json.RawMessage("null"), + "test_state": mustMarshal(map[string]any{ + // Test simple fields + "public_ip": "203.0.113.1", + "private_ip": "192.168.1.1", + "protected_ip": "100.64.0.1", + "well_known_ip": "8.8.8.8", + "ipv6_addr": "2001:db8::1", + "private_ipv6": "fd00::1", + "domain": "test.example.com", + "uri": "stun:stun.example.com:3478", + "uri_with_ip": "turn:203.0.113.1:3478", + "netbird_domain": "device.netbird.cloud", + + // Test CIDR ranges + "public_cidr": "203.0.113.0/24", + "private_cidr": "192.168.0.0/16", + "protected_cidr": "100.64.0.0/10", + "ipv6_cidr": "2001:db8::/32", + "private_ipv6_cidr": "fd00::/8", + + // Test nested structures + "nested": map[string]any{ + "ip": "203.0.113.2", + "domain": "nested.example.com", + "more_nest": map[string]any{ + "ip": "203.0.113.3", + "domain": "deep.example.com", + }, + }, + + // Test arrays + "string_array": []any{ + "203.0.113.4", + "test1.example.com", + "test2.example.com", + }, + "object_array": []any{ + map[string]any{ + "ip": "203.0.113.5", + "domain": "array1.example.com", + }, + map[string]any{ + "ip": "203.0.113.6", + "domain": "array2.example.com", + }, + }, + + // Test multiple occurrences of same value + "duplicate_ip": "203.0.113.1", // Same as public_ip + "duplicate_domain": "test.example.com", // Same as domain + + // Test URIs with various schemes + "stun_uri": "stun:stun.example.com:3478", + "turns_uri": "turns:turns.example.com:5349", + "http_uri": "http://web.example.com:80", + "https_uri": "https://secure.example.com:443", + + // Test strings that might look like IPs but aren't + "not_ip": "300.300.300.300", + "partial_ip": "192.168", + "ip_like_string": "1234.5678", + + // Test mixed content strings + "mixed_content": "Server at 203.0.113.1 (test.example.com) on port 80", + + // Test empty and special values + "empty_string": "", + "null_value": nil, + "numeric_value": 42, + "boolean_value": true, + }), + "route_state": mustMarshal(map[string]any{ + "routes": []any{ + map[string]any{ + "network": "203.0.113.0/24", + "gateway": "203.0.113.1", + "domains": []any{ + "route1.example.com", + "route2.example.com", + }, + }, + map[string]any{ + "network": "2001:db8::/32", + "gateway": "2001:db8::1", + "domains": []any{ + "route3.example.com", + "route4.example.com", + }, + }, + }, + // Test map with IP/CIDR keys + "refCountMap": map[string]any{ + "203.0.113.1/32": map[string]any{ + "Count": 1, + "Out": map[string]any{ + "IP": "192.168.0.1", + "Intf": map[string]any{ + "Name": "eth0", + "Index": 1, + }, + }, + }, + "2001:db8::1/128": map[string]any{ + "Count": 1, + "Out": map[string]any{ + "IP": "fe80::1", + "Intf": map[string]any{ + "Name": "eth0", + "Index": 1, + }, + }, + }, + "10.0.0.1/32": map[string]any{ // private IP should remain unchanged + "Count": 1, + "Out": map[string]any{ + "IP": "192.168.0.1", + }, + }, + }, + }), + } + + anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) + err := anonymizeStateFile(&testState, anonymizer) + require.NoError(t, err) + + // Helper function to unmarshal and get nested values + var state map[string]any + err = json.Unmarshal(testState["test_state"], &state) + require.NoError(t, err) + + // Test null state remains unchanged + require.Equal(t, "null", string(testState["null_state"])) + + // Basic assertions + assert.NotEqual(t, "203.0.113.1", state["public_ip"]) + assert.Equal(t, "192.168.1.1", state["private_ip"]) // Private IP unchanged + assert.Equal(t, "100.64.0.1", state["protected_ip"]) // Protected IP unchanged + assert.Equal(t, "8.8.8.8", state["well_known_ip"]) // Well-known IP unchanged + assert.NotEqual(t, "2001:db8::1", state["ipv6_addr"]) + assert.Equal(t, "fd00::1", state["private_ipv6"]) // Private IPv6 unchanged + assert.NotEqual(t, "test.example.com", state["domain"]) + assert.True(t, strings.HasSuffix(state["domain"].(string), ".domain")) + assert.Equal(t, "device.netbird.cloud", state["netbird_domain"]) // Netbird domain unchanged + + // CIDR ranges + assert.NotEqual(t, "203.0.113.0/24", state["public_cidr"]) + assert.Contains(t, state["public_cidr"], "/24") // Prefix preserved + assert.Equal(t, "192.168.0.0/16", state["private_cidr"]) // Private CIDR unchanged + assert.Equal(t, "100.64.0.0/10", state["protected_cidr"]) // Protected CIDR unchanged + assert.NotEqual(t, "2001:db8::/32", state["ipv6_cidr"]) + assert.Contains(t, state["ipv6_cidr"], "/32") // IPv6 prefix preserved + + // Nested structures + nested := state["nested"].(map[string]any) + assert.NotEqual(t, "203.0.113.2", nested["ip"]) + assert.NotEqual(t, "nested.example.com", nested["domain"]) + moreNest := nested["more_nest"].(map[string]any) + assert.NotEqual(t, "203.0.113.3", moreNest["ip"]) + assert.NotEqual(t, "deep.example.com", moreNest["domain"]) + + // Arrays + strArray := state["string_array"].([]any) + assert.NotEqual(t, "203.0.113.4", strArray[0]) + assert.NotEqual(t, "test1.example.com", strArray[1]) + assert.True(t, strings.HasSuffix(strArray[1].(string), ".domain")) + + objArray := state["object_array"].([]any) + firstObj := objArray[0].(map[string]any) + assert.NotEqual(t, "203.0.113.5", firstObj["ip"]) + assert.NotEqual(t, "array1.example.com", firstObj["domain"]) + + // Duplicate values should be anonymized consistently + assert.Equal(t, state["public_ip"], state["duplicate_ip"]) + assert.Equal(t, state["domain"], state["duplicate_domain"]) + + // URIs + assert.NotContains(t, state["stun_uri"], "stun.example.com") + assert.NotContains(t, state["turns_uri"], "turns.example.com") + assert.NotContains(t, state["http_uri"], "web.example.com") + assert.NotContains(t, state["https_uri"], "secure.example.com") + + // Non-IP strings should remain unchanged + assert.Equal(t, "300.300.300.300", state["not_ip"]) + assert.Equal(t, "192.168", state["partial_ip"]) + assert.Equal(t, "1234.5678", state["ip_like_string"]) + + // Mixed content should have IPs and domains replaced + mixedContent := state["mixed_content"].(string) + assert.NotContains(t, mixedContent, "203.0.113.1") + assert.NotContains(t, mixedContent, "test.example.com") + assert.Contains(t, mixedContent, "Server at ") + assert.Contains(t, mixedContent, " on port 80") + + // Special values should remain unchanged + assert.Equal(t, "", state["empty_string"]) + assert.Nil(t, state["null_value"]) + assert.Equal(t, float64(42), state["numeric_value"]) + assert.Equal(t, true, state["boolean_value"]) + + // Check route state + var routeState map[string]any + err = json.Unmarshal(testState["route_state"], &routeState) + require.NoError(t, err) + + routes := routeState["routes"].([]any) + route1 := routes[0].(map[string]any) + assert.NotEqual(t, "203.0.113.0/24", route1["network"]) + assert.Contains(t, route1["network"], "/24") + assert.NotEqual(t, "203.0.113.1", route1["gateway"]) + domains := route1["domains"].([]any) + assert.True(t, strings.HasSuffix(domains[0].(string), ".domain")) + assert.True(t, strings.HasSuffix(domains[1].(string), ".domain")) + + // Check map keys are anonymized + refCountMap := routeState["refCountMap"].(map[string]any) + hasPublicIPKey := false + hasIPv6Key := false + hasPrivateIPKey := false + for key := range refCountMap { + if strings.Contains(key, "203.0.113.1") { + hasPublicIPKey = true + } + if strings.Contains(key, "2001:db8::1") { + hasIPv6Key = true + } + if key == "10.0.0.1/32" { + hasPrivateIPKey = true + } + } + assert.False(t, hasPublicIPKey, "public IP in key should be anonymized") + assert.False(t, hasIPv6Key, "IPv6 in key should be anonymized") + assert.True(t, hasPrivateIPKey, "private IP in key should remain unchanged") +} + +func mustMarshal(v any) json.RawMessage { + data, err := json.Marshal(v) + if err != nil { + panic(err) + } + return data +} From dffce78a8c8e78161e8fe1f8d7f01ba232c0d8e7 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:19:34 +0100 Subject: [PATCH 09/17] [client] Fix debug bundle state anonymization test (#2976) --- client/server/debug_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/server/debug_test.go b/client/server/debug_test.go index 303e5e66166..1515036bd02 100644 --- a/client/server/debug_test.go +++ b/client/server/debug_test.go @@ -137,6 +137,13 @@ func TestAnonymizeStateFile(t *testing.T) { } anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) + + // Pre-seed the domains we need to verify in the test assertions + anonymizer.AnonymizeDomain("test.example.com") + anonymizer.AnonymizeDomain("nested.example.com") + anonymizer.AnonymizeDomain("deep.example.com") + anonymizer.AnonymizeDomain("array1.example.com") + err := anonymizeStateFile(&testState, anonymizer) require.NoError(t, err) From a0bf0bdcc077e54294c6e31a24efcd9047f3f97f Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 3 Dec 2024 10:13:27 +0100 Subject: [PATCH 10/17] Pass IP instead of net to Rosenpass (#2975) --- client/internal/peer/conn.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/internal/peer/conn.go b/client/internal/peer/conn.go index 81c456db747..a8de2fccb73 100644 --- a/client/internal/peer/conn.go +++ b/client/internal/peer/conn.go @@ -83,7 +83,6 @@ type Conn struct { signaler *Signaler relayManager *relayClient.Manager allowedIP net.IP - allowedNet string handshaker *Handshaker onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string) @@ -111,7 +110,7 @@ type Conn struct { // NewConn creates a new not opened Conn to the remote peer. // To establish a connection run Conn.Open func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Status, signaler *Signaler, iFaceDiscover stdnet.ExternalIFaceDiscover, relayManager *relayClient.Manager, srWatcher *guard.SRWatcher) (*Conn, error) { - allowedIP, allowedNet, err := net.ParseCIDR(config.WgConfig.AllowedIps) + allowedIP, _, err := net.ParseCIDR(config.WgConfig.AllowedIps) if err != nil { log.Errorf("failed to parse allowedIPS: %v", err) return nil, err @@ -129,7 +128,6 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu signaler: signaler, relayManager: relayManager, allowedIP: allowedIP, - allowedNet: allowedNet.String(), statusRelay: NewAtomicConnStatus(), statusICE: NewAtomicConnStatus(), } @@ -594,7 +592,7 @@ func (conn *Conn) doOnConnected(remoteRosenpassPubKey []byte, remoteRosenpassAdd } if conn.onConnected != nil { - conn.onConnected(conn.config.Key, remoteRosenpassPubKey, conn.allowedNet, remoteRosenpassAddr) + conn.onConnected(conn.config.Key, remoteRosenpassPubKey, conn.allowedIP.String(), remoteRosenpassAddr) } } From a4826cfb5fb5e0510f485644583bb14da1aa2ae8 Mon Sep 17 00:00:00 2001 From: Maycon Santos Date: Tue, 3 Dec 2024 10:22:04 +0100 Subject: [PATCH 11/17] [client] Get static system info once (#2965) Get static system info once for Windows, Darwin, and Linux nodes This should improve startup and peer authentication times --- client/system/info.go | 8 ++++ client/system/info_darwin.go | 20 +++++----- client/system/info_linux.go | 61 +++++++++++++---------------- client/system/info_windows.go | 53 +++++++++++++------------ client/system/static_info.go | 46 ++++++++++++++++++++++ client/system/sysinfo_linux_test.go | 5 ++- 6 files changed, 123 insertions(+), 70 deletions(-) create mode 100644 client/system/static_info.go diff --git a/client/system/info.go b/client/system/info.go index 2af2e637b92..200d835df31 100644 --- a/client/system/info.go +++ b/client/system/info.go @@ -61,6 +61,14 @@ type Info struct { Files []File // for posture checks } +// StaticInfo is an object that contains machine information that does not change +type StaticInfo struct { + SystemSerialNumber string + SystemProductName string + SystemManufacturer string + Environment Environment +} + // extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context func extractUserAgent(ctx context.Context) string { md, hasMeta := metadata.FromOutgoingContext(ctx) diff --git a/client/system/info_darwin.go b/client/system/info_darwin.go index 6f4ed173b4b..13b0a446bd3 100644 --- a/client/system/info_darwin.go +++ b/client/system/info_darwin.go @@ -10,13 +10,12 @@ import ( "os/exec" "runtime" "strings" + "time" "golang.org/x/sys/unix" log "github.com/sirupsen/logrus" - "github.com/netbirdio/netbird/client/system/detect_cloud" - "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" ) @@ -41,11 +40,10 @@ func GetInfo(ctx context.Context) *Info { log.Warnf("failed to discover network addresses: %s", err) } - serialNum, prodName, manufacturer := sysInfo() - - env := Environment{ - Cloud: detect_cloud.Detect(ctx), - Platform: detect_platform.Detect(ctx), + start := time.Now() + si := updateStaticInfo() + if time.Since(start) > 1*time.Second { + log.Warnf("updateStaticInfo took %s", time.Since(start)) } gio := &Info{ @@ -57,10 +55,10 @@ func GetInfo(ctx context.Context) *Info { CPUs: runtime.NumCPU(), KernelVersion: release, NetworkAddresses: addrs, - SystemSerialNumber: serialNum, - SystemProductName: prodName, - SystemManufacturer: manufacturer, - Environment: env, + SystemSerialNumber: si.SystemSerialNumber, + SystemProductName: si.SystemProductName, + SystemManufacturer: si.SystemManufacturer, + Environment: si.Environment, } systemHostname, _ := os.Hostname() diff --git a/client/system/info_linux.go b/client/system/info_linux.go index b6a142bce28..bfc77be1915 100644 --- a/client/system/info_linux.go +++ b/client/system/info_linux.go @@ -1,5 +1,4 @@ //go:build !android -// +build !android package system @@ -16,30 +15,13 @@ import ( log "github.com/sirupsen/logrus" "github.com/zcalusic/sysinfo" - "github.com/netbirdio/netbird/client/system/detect_cloud" - "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" ) -type SysInfoGetter interface { - GetSysInfo() SysInfo -} - -type SysInfoWrapper struct { - si sysinfo.SysInfo -} - -func (s SysInfoWrapper) GetSysInfo() SysInfo { - s.si.GetSysInfo() - return SysInfo{ - ChassisSerial: s.si.Chassis.Serial, - ProductSerial: s.si.Product.Serial, - BoardSerial: s.si.Board.Serial, - ProductName: s.si.Product.Name, - BoardName: s.si.Board.Name, - ProductVendor: s.si.Product.Vendor, - } -} +var ( + // it is override in tests + getSystemInfo = defaultSysInfoImplementation +) // GetInfo retrieves and parses the system information func GetInfo(ctx context.Context) *Info { @@ -65,12 +47,10 @@ func GetInfo(ctx context.Context) *Info { log.Warnf("failed to discover network addresses: %s", err) } - si := SysInfoWrapper{} - serialNum, prodName, manufacturer := sysInfo(si.GetSysInfo()) - - env := Environment{ - Cloud: detect_cloud.Detect(ctx), - Platform: detect_platform.Detect(ctx), + start := time.Now() + si := updateStaticInfo() + if time.Since(start) > 1*time.Second { + log.Warnf("updateStaticInfo took %s", time.Since(start)) } gio := &Info{ @@ -85,10 +65,10 @@ func GetInfo(ctx context.Context) *Info { UIVersion: extractUserAgent(ctx), KernelVersion: osInfo[1], NetworkAddresses: addrs, - SystemSerialNumber: serialNum, - SystemProductName: prodName, - SystemManufacturer: manufacturer, - Environment: env, + SystemSerialNumber: si.SystemSerialNumber, + SystemProductName: si.SystemProductName, + SystemManufacturer: si.SystemManufacturer, + Environment: si.Environment, } return gio @@ -108,9 +88,9 @@ func _getInfo() string { return out.String() } -func sysInfo(si SysInfo) (string, string, string) { +func sysInfo() (string, string, string) { isascii := regexp.MustCompile("^[[:ascii:]]+$") - + si := getSystemInfo() serials := []string{si.ChassisSerial, si.ProductSerial} serial := "" @@ -141,3 +121,16 @@ func sysInfo(si SysInfo) (string, string, string) { } return serial, name, manufacturer } + +func defaultSysInfoImplementation() SysInfo { + si := sysinfo.SysInfo{} + si.GetSysInfo() + return SysInfo{ + ChassisSerial: si.Chassis.Serial, + ProductSerial: si.Product.Serial, + BoardSerial: si.Board.Serial, + ProductName: si.Product.Name, + BoardName: si.Board.Name, + ProductVendor: si.Product.Vendor, + } +} diff --git a/client/system/info_windows.go b/client/system/info_windows.go index 68631fe164d..28bd3d3007c 100644 --- a/client/system/info_windows.go +++ b/client/system/info_windows.go @@ -6,13 +6,12 @@ import ( "os" "runtime" "strings" + "time" log "github.com/sirupsen/logrus" "github.com/yusufpapurcu/wmi" "golang.org/x/sys/windows/registry" - "github.com/netbirdio/netbird/client/system/detect_cloud" - "github.com/netbirdio/netbird/client/system/detect_platform" "github.com/netbirdio/netbird/version" ) @@ -42,24 +41,10 @@ func GetInfo(ctx context.Context) *Info { log.Warnf("failed to discover network addresses: %s", err) } - serialNum, err := sysNumber() - if err != nil { - log.Warnf("failed to get system serial number: %s", err) - } - - prodName, err := sysProductName() - if err != nil { - log.Warnf("failed to get system product name: %s", err) - } - - manufacturer, err := sysManufacturer() - if err != nil { - log.Warnf("failed to get system manufacturer: %s", err) - } - - env := Environment{ - Cloud: detect_cloud.Detect(ctx), - Platform: detect_platform.Detect(ctx), + start := time.Now() + si := updateStaticInfo() + if time.Since(start) > 1*time.Second { + log.Warnf("updateStaticInfo took %s", time.Since(start)) } gio := &Info{ @@ -71,10 +56,10 @@ func GetInfo(ctx context.Context) *Info { CPUs: runtime.NumCPU(), KernelVersion: buildVersion, NetworkAddresses: addrs, - SystemSerialNumber: serialNum, - SystemProductName: prodName, - SystemManufacturer: manufacturer, - Environment: env, + SystemSerialNumber: si.SystemSerialNumber, + SystemProductName: si.SystemProductName, + SystemManufacturer: si.SystemManufacturer, + Environment: si.Environment, } systemHostname, _ := os.Hostname() @@ -85,6 +70,26 @@ func GetInfo(ctx context.Context) *Info { return gio } +func sysInfo() (serialNumber string, productName string, manufacturer string) { + var err error + serialNumber, err = sysNumber() + if err != nil { + log.Warnf("failed to get system serial number: %s", err) + } + + productName, err = sysProductName() + if err != nil { + log.Warnf("failed to get system product name: %s", err) + } + + manufacturer, err = sysManufacturer() + if err != nil { + log.Warnf("failed to get system manufacturer: %s", err) + } + + return serialNumber, productName, manufacturer +} + func getOSNameAndVersion() (string, string) { var dst []Win32_OperatingSystem query := wmi.CreateQuery(&dst, "") diff --git a/client/system/static_info.go b/client/system/static_info.go new file mode 100644 index 00000000000..fabe65a6806 --- /dev/null +++ b/client/system/static_info.go @@ -0,0 +1,46 @@ +//go:build (linux && !android) || windows || (darwin && !ios) + +package system + +import ( + "context" + "sync" + "time" + + "github.com/netbirdio/netbird/client/system/detect_cloud" + "github.com/netbirdio/netbird/client/system/detect_platform" +) + +var ( + staticInfo StaticInfo + once sync.Once +) + +func init() { + go func() { + _ = updateStaticInfo() + }() +} + +func updateStaticInfo() StaticInfo { + once.Do(func() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + wg := sync.WaitGroup{} + wg.Add(3) + go func() { + staticInfo.SystemSerialNumber, staticInfo.SystemProductName, staticInfo.SystemManufacturer = sysInfo() + wg.Done() + }() + go func() { + staticInfo.Environment.Cloud = detect_cloud.Detect(ctx) + wg.Done() + }() + go func() { + staticInfo.Environment.Platform = detect_platform.Detect(ctx) + wg.Done() + }() + wg.Wait() + }) + return staticInfo +} diff --git a/client/system/sysinfo_linux_test.go b/client/system/sysinfo_linux_test.go index f6a0b70587b..ae89bfcf974 100644 --- a/client/system/sysinfo_linux_test.go +++ b/client/system/sysinfo_linux_test.go @@ -183,7 +183,10 @@ func Test_sysInfo(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotSerialNum, gotProdName, gotManufacturer := sysInfo(tt.sysInfo) + getSystemInfo = func() SysInfo { + return tt.sysInfo + } + gotSerialNum, gotProdName, gotManufacturer := sysInfo() if gotSerialNum != tt.wantSerialNum { t.Errorf("sysInfo() gotSerialNum = %v, want %v", gotSerialNum, tt.wantSerialNum) } From 6285e0d23ef95d98abc815315c67a24371f7a2a4 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Dec 2024 12:43:17 +0100 Subject: [PATCH 12/17] [client] Add netbird.err and netbird.out to debug bundle (#2971) --- client/server/debug.go | 45 +++++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/client/server/debug.go b/client/server/debug.go index 1bad907ba56..d87fc9b6a2d 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -15,6 +15,7 @@ import ( "net" "net/netip" "os" + "path/filepath" "sort" "strings" "time" @@ -32,7 +33,9 @@ const readmeContent = `Netbird debug bundle This debug bundle contains the following files: status.txt: Anonymized status information of the NetBird client. -client.log: Most recent, anonymized log file of the NetBird client. +client.log: Most recent, anonymized client log file of the NetBird client. +netbird.err: Most recent, anonymized stderr log file of the NetBird client. +netbird.out: Most recent, anonymized stdout log file of the NetBird client. routes.txt: Anonymized system routes, if --system-info flag was provided. interfaces.txt: Anonymized network interface information, if --system-info flag was provided. config.txt: Anonymized configuration information of the NetBird client. @@ -92,6 +95,12 @@ The config.txt file contains anonymized configuration information of the NetBird Other non-sensitive configuration options are included without anonymization. ` +const ( + clientLogFile = "client.log" + errorLogFile = "netbird.err" + stdoutLogFile = "netbird.out" +) + // DebugBundle creates a debug bundle and returns the location. func (s *Server) DebugBundle(_ context.Context, req *proto.DebugBundleRequest) (resp *proto.DebugBundleResponse, err error) { s.mutex.Lock() @@ -310,14 +319,35 @@ func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymi return nil } -func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) (err error) { - logFile, err := os.Open(s.logFile) +func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + logDir := filepath.Dir(s.logFile) + + if err := s.addSingleLogfile(s.logFile, clientLogFile, req, anonymizer, archive); err != nil { + return fmt.Errorf("add client log file to zip: %w", err) + } + + errLogPath := filepath.Join(logDir, errorLogFile) + if err := s.addSingleLogfile(errLogPath, errorLogFile, req, anonymizer, archive); err != nil { + log.Warnf("Failed to add %s to zip: %v", errorLogFile, err) + } + + stdoutLogPath := filepath.Join(logDir, stdoutLogFile) + if err := s.addSingleLogfile(stdoutLogPath, stdoutLogFile, req, anonymizer, archive); err != nil { + log.Warnf("Failed to add %s to zip: %v", stdoutLogFile, err) + } + + return nil +} + +// addSingleLogfile adds a single log file to the archive +func (s *Server) addSingleLogfile(logPath, targetName string, req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + logFile, err := os.Open(logPath) if err != nil { - return fmt.Errorf("open log file: %w", err) + return fmt.Errorf("open log file %s: %w", targetName, err) } defer func() { if err := logFile.Close(); err != nil { - log.Errorf("Failed to close original log file: %v", err) + log.Errorf("Failed to close log file %s: %v", targetName, err) } }() @@ -330,8 +360,9 @@ func (s *Server) addLogfile(req *proto.DebugBundleRequest, anonymizer *anonymize } else { logReader = logFile } - if err := addFileToZip(archive, logReader, "client.log"); err != nil { - return fmt.Errorf("add log file to zip: %w", err) + + if err := addFileToZip(archive, logReader, targetName); err != nil { + return fmt.Errorf("add %s to zip: %w", targetName, err) } return nil From 7dacd9cb235cc7c7501cd444bc0f76633900177b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Nohlg=C3=A5rd?= Date: Tue, 3 Dec 2024 13:49:02 +0100 Subject: [PATCH 13/17] [management] Add missing parentheses on iphone hostname generation condition (#2977) --- management/server/peer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/management/server/peer.go b/management/server/peer.go index d45bb1a507e..a301544d5c3 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -477,7 +477,7 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, setupKey, userID s setupKeyName = sk.Name } - if strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad" && userID != "" { + if (strings.ToLower(peer.Meta.Hostname) == "iphone" || strings.ToLower(peer.Meta.Hostname) == "ipad") && userID != "" { if am.idpManager != nil { userdata, err := am.idpManager.GetUserDataByID(ctx, userID, idp.AppMetadata{WTAccountID: accountID}) if err == nil && userdata != nil { From 17c20b45ce9a029f7180bbef49eb5cd3ada000b4 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:50:12 +0100 Subject: [PATCH 14/17] [client] Add network map to debug bundle (#2966) --- client/anonymize/anonymize.go | 38 ++-- client/anonymize/anonymize_test.go | 18 ++ client/cmd/debug.go | 52 +++++ client/cmd/root.go | 1 + client/internal/connect.go | 20 +- client/internal/engine.go | 66 +++++- client/proto/daemon.pb.go | 352 ++++++++++++++++++++--------- client/proto/daemon.proto | 14 +- client/proto/daemon_grpc.pb.go | 38 ++++ client/server/debug.go | 289 ++++++++++++++++++++++- client/server/debug_test.go | 165 ++++++++++++++ client/server/server.go | 3 + go.mod | 4 +- go.sum | 8 +- 14 files changed, 915 insertions(+), 153 deletions(-) diff --git a/client/anonymize/anonymize.go b/client/anonymize/anonymize.go index ad31682f25a..9a6d9720794 100644 --- a/client/anonymize/anonymize.go +++ b/client/anonymize/anonymize.go @@ -12,6 +12,8 @@ import ( "strings" ) +const anonTLD = ".domain" + type Anonymizer struct { ipAnonymizer map[netip.Addr]netip.Addr domainAnonymizer map[string]string @@ -83,29 +85,39 @@ func (a *Anonymizer) AnonymizeIPString(ip string) string { } func (a *Anonymizer) AnonymizeDomain(domain string) string { - if strings.HasSuffix(domain, "netbird.io") || - strings.HasSuffix(domain, "netbird.selfhosted") || - strings.HasSuffix(domain, "netbird.cloud") || - strings.HasSuffix(domain, "netbird.stage") || - strings.HasSuffix(domain, ".domain") { + baseDomain := domain + hasDot := strings.HasSuffix(domain, ".") + if hasDot { + baseDomain = domain[:len(domain)-1] + } + + if strings.HasSuffix(baseDomain, "netbird.io") || + strings.HasSuffix(baseDomain, "netbird.selfhosted") || + strings.HasSuffix(baseDomain, "netbird.cloud") || + strings.HasSuffix(baseDomain, "netbird.stage") || + strings.HasSuffix(baseDomain, anonTLD) { return domain } - parts := strings.Split(domain, ".") + parts := strings.Split(baseDomain, ".") if len(parts) < 2 { return domain } - baseDomain := parts[len(parts)-2] + "." + parts[len(parts)-1] + baseForLookup := parts[len(parts)-2] + "." + parts[len(parts)-1] - anonymized, ok := a.domainAnonymizer[baseDomain] + anonymized, ok := a.domainAnonymizer[baseForLookup] if !ok { - anonymizedBase := "anon-" + generateRandomString(5) + ".domain" - a.domainAnonymizer[baseDomain] = anonymizedBase + anonymizedBase := "anon-" + generateRandomString(5) + anonTLD + a.domainAnonymizer[baseForLookup] = anonymizedBase anonymized = anonymizedBase } - return strings.Replace(domain, baseDomain, anonymized, 1) + result := strings.Replace(baseDomain, baseForLookup, anonymized, 1) + if hasDot { + result += "." + } + return result } func (a *Anonymizer) AnonymizeURI(uri string) string { @@ -168,10 +180,10 @@ func (a *Anonymizer) AnonymizeDNSLogLine(logEntry string) string { parts := strings.Split(match, `"`) if len(parts) >= 2 { domain := parts[1] - if strings.HasSuffix(domain, ".domain") { + if strings.HasSuffix(domain, anonTLD) { return match } - randomDomain := generateRandomString(10) + ".domain" + randomDomain := generateRandomString(10) + anonTLD return strings.Replace(match, domain, randomDomain, 1) } return match diff --git a/client/anonymize/anonymize_test.go b/client/anonymize/anonymize_test.go index 605788ab54a..a3aae1ee982 100644 --- a/client/anonymize/anonymize_test.go +++ b/client/anonymize/anonymize_test.go @@ -67,18 +67,36 @@ func TestAnonymizeDomain(t *testing.T) { `^anon-[a-zA-Z0-9]+\.domain$`, true, }, + { + "Domain with Trailing Dot", + "example.com.", + `^anon-[a-zA-Z0-9]+\.domain.$`, + true, + }, { "Subdomain", "sub.example.com", `^sub\.anon-[a-zA-Z0-9]+\.domain$`, true, }, + { + "Subdomain with Trailing Dot", + "sub.example.com.", + `^sub\.anon-[a-zA-Z0-9]+\.domain.$`, + true, + }, { "Protected Domain", "netbird.io", `^netbird\.io$`, false, }, + { + "Protected Domain with Trailing Dot", + "netbird.io.", + `^netbird\.io.$`, + false, + }, } for _, tc := range tests { diff --git a/client/cmd/debug.go b/client/cmd/debug.go index 9abd2039dd5..c7ab87b4766 100644 --- a/client/cmd/debug.go +++ b/client/cmd/debug.go @@ -3,6 +3,7 @@ package cmd import ( "context" "fmt" + "strings" "time" log "github.com/sirupsen/logrus" @@ -61,6 +62,15 @@ var forCmd = &cobra.Command{ RunE: runForDuration, } +var persistenceCmd = &cobra.Command{ + Use: "persistence [on|off]", + Short: "Set network map memory persistence", + Long: `Configure whether the latest network map should persist in memory. When enabled, the last known network map will be kept in memory.`, + Example: " netbird debug persistence on", + Args: cobra.ExactArgs(1), + RunE: setNetworkMapPersistence, +} + func debugBundle(cmd *cobra.Command, _ []string) error { conn, err := getClient(cmd) if err != nil { @@ -171,6 +181,13 @@ func runForDuration(cmd *cobra.Command, args []string) error { time.Sleep(1 * time.Second) + // Enable network map persistence before bringing the service up + if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{ + Enabled: true, + }); err != nil { + return fmt.Errorf("failed to enable network map persistence: %v", status.Convert(err).Message()) + } + if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil { return fmt.Errorf("failed to up: %v", status.Convert(err).Message()) } @@ -200,6 +217,13 @@ func runForDuration(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message()) } + // Disable network map persistence after creating the debug bundle + if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{ + Enabled: false, + }); err != nil { + return fmt.Errorf("failed to disable network map persistence: %v", status.Convert(err).Message()) + } + if stateWasDown { if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil { return fmt.Errorf("failed to down: %v", status.Convert(err).Message()) @@ -219,6 +243,34 @@ func runForDuration(cmd *cobra.Command, args []string) error { return nil } +func setNetworkMapPersistence(cmd *cobra.Command, args []string) error { + conn, err := getClient(cmd) + if err != nil { + return err + } + defer func() { + if err := conn.Close(); err != nil { + log.Errorf(errCloseConnection, err) + } + }() + + persistence := strings.ToLower(args[0]) + if persistence != "on" && persistence != "off" { + return fmt.Errorf("invalid persistence value: %s. Use 'on' or 'off'", args[0]) + } + + client := proto.NewDaemonServiceClient(conn) + _, err = client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{ + Enabled: persistence == "on", + }) + if err != nil { + return fmt.Errorf("failed to set network map persistence: %v", status.Convert(err).Message()) + } + + cmd.Printf("Network map persistence set to: %s\n", persistence) + return nil +} + func getStatusOutput(cmd *cobra.Command) string { var statusOutputString string statusResp, err := getStatus(cmd.Context()) diff --git a/client/cmd/root.go b/client/cmd/root.go index 8dae6e27374..3f2d04ef304 100644 --- a/client/cmd/root.go +++ b/client/cmd/root.go @@ -155,6 +155,7 @@ func init() { debugCmd.AddCommand(logCmd) logCmd.AddCommand(logLevelCmd) debugCmd.AddCommand(forCmd) + debugCmd.AddCommand(persistenceCmd) upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil, `Sets external IPs maps between local addresses and interfaces.`+ diff --git a/client/internal/connect.go b/client/internal/connect.go index 8c2ad4aa1db..21f6d3ec869 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -40,6 +40,8 @@ type ConnectClient struct { statusRecorder *peer.Status engine *Engine engineMutex sync.Mutex + + persistNetworkMap bool } func NewConnectClient( @@ -258,7 +260,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold c.engineMutex.Lock() c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, probes, checks) - + c.engine.SetNetworkMapPersistence(c.persistNetworkMap) c.engineMutex.Unlock() if err := c.engine.Start(); err != nil { @@ -362,6 +364,22 @@ func (c *ConnectClient) isContextCancelled() bool { } } +// SetNetworkMapPersistence enables or disables network map persistence. +// When enabled, the last received network map will be stored and can be retrieved +// through the Engine's getLatestNetworkMap method. When disabled, any stored +// network map will be cleared. This functionality is primarily used for debugging +// and should not be enabled during normal operation. +func (c *ConnectClient) SetNetworkMapPersistence(enabled bool) { + c.engineMutex.Lock() + c.persistNetworkMap = enabled + c.engineMutex.Unlock() + + engine := c.Engine() + if engine != nil { + engine.SetNetworkMapPersistence(enabled) + } +} + // createEngineConfig converts configuration received from Management Service to EngineConfig func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) { nm := false diff --git a/client/internal/engine.go b/client/internal/engine.go index 920c295cdde..fc9620d801e 100644 --- a/client/internal/engine.go +++ b/client/internal/engine.go @@ -21,6 +21,7 @@ import ( "github.com/pion/stun/v2" log "github.com/sirupsen/logrus" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "google.golang.org/protobuf/proto" "github.com/netbirdio/netbird/client/firewall" "github.com/netbirdio/netbird/client/firewall/manager" @@ -172,6 +173,10 @@ type Engine struct { relayManager *relayClient.Manager stateManager *statemanager.Manager srWatcher *guard.SRWatcher + + // Network map persistence + persistNetworkMap bool + latestNetworkMap *mgmProto.NetworkMap } // Peer is an instance of the Connection Peer @@ -573,13 +578,22 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error { return err } - if update.GetNetworkMap() != nil { - // only apply new changes and ignore old ones - err := e.updateNetworkMap(update.GetNetworkMap()) - if err != nil { - return err - } + nm := update.GetNetworkMap() + if nm == nil { + return nil + } + + // Store network map if persistence is enabled + if e.persistNetworkMap { + e.latestNetworkMap = nm + log.Debugf("network map persisted with serial %d", nm.GetSerial()) + } + + // only apply new changes and ignore old ones + if err := e.updateNetworkMap(nm); err != nil { + return err } + return nil } @@ -1500,6 +1514,46 @@ func (e *Engine) stopDNSServer() { e.statusRecorder.UpdateDNSStates(nsGroupStates) } +// SetNetworkMapPersistence enables or disables network map persistence +func (e *Engine) SetNetworkMapPersistence(enabled bool) { + e.syncMsgMux.Lock() + defer e.syncMsgMux.Unlock() + + if enabled == e.persistNetworkMap { + return + } + e.persistNetworkMap = enabled + log.Debugf("Network map persistence is set to %t", enabled) + + if !enabled { + e.latestNetworkMap = nil + } +} + +// GetLatestNetworkMap returns the stored network map if persistence is enabled +func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) { + e.syncMsgMux.Lock() + defer e.syncMsgMux.Unlock() + + if !e.persistNetworkMap { + return nil, errors.New("network map persistence is disabled") + } + + if e.latestNetworkMap == nil { + //nolint:nilnil + return nil, nil + } + + // Create a deep copy to avoid external modifications + nm, ok := proto.Clone(e.latestNetworkMap).(*mgmProto.NetworkMap) + if !ok { + + return nil, fmt.Errorf("failed to clone network map") + } + + return nm, nil +} + // isChecksEqual checks if two slices of checks are equal. func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool { for _, check := range checks { diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index b942d8b6e0a..0eae4e0d618 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.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 v4.23.4 // source: daemon.proto package proto @@ -2103,6 +2103,91 @@ func (*SetLogLevelResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{30} } +type SetNetworkMapPersistenceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` +} + +func (x *SetNetworkMapPersistenceRequest) Reset() { + *x = SetNetworkMapPersistenceRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetNetworkMapPersistenceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetNetworkMapPersistenceRequest) ProtoMessage() {} + +func (x *SetNetworkMapPersistenceRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[31] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetNetworkMapPersistenceRequest.ProtoReflect.Descriptor instead. +func (*SetNetworkMapPersistenceRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{31} +} + +func (x *SetNetworkMapPersistenceRequest) GetEnabled() bool { + if x != nil { + return x.Enabled + } + return false +} + +type SetNetworkMapPersistenceResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SetNetworkMapPersistenceResponse) Reset() { + *x = SetNetworkMapPersistenceResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SetNetworkMapPersistenceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetNetworkMapPersistenceResponse) ProtoMessage() {} + +func (x *SetNetworkMapPersistenceResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[32] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetNetworkMapPersistenceResponse.ProtoReflect.Descriptor instead. +func (*SetNetworkMapPersistenceResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{32} +} + var File_daemon_proto protoreflect.FileDescriptor var file_daemon_proto_rawDesc = []byte{ @@ -2399,66 +2484,79 @@ var file_daemon_proto_rawDesc = []byte{ 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, - 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, - 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, - 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, - 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, - 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, - 0x43, 0x45, 0x10, 0x07, 0x32, 0xb8, 0x06, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, - 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, - 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, - 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, - 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, - 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, + 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, + 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, + 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, + 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, + 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, + 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, + 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, + 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, + 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, + 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xa9, 0x07, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, + 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, + 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, + 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, + 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, + 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, + 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, + 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, + 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, - 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, - 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, - 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, - 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, - 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, - 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, - 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, - 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, - 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, + 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, + 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, + 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, + 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, + 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, + 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, + 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, + 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -2474,50 +2572,52 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 34) var file_daemon_proto_goTypes = []interface{}{ - (LogLevel)(0), // 0: daemon.LogLevel - (*LoginRequest)(nil), // 1: daemon.LoginRequest - (*LoginResponse)(nil), // 2: daemon.LoginResponse - (*WaitSSOLoginRequest)(nil), // 3: daemon.WaitSSOLoginRequest - (*WaitSSOLoginResponse)(nil), // 4: daemon.WaitSSOLoginResponse - (*UpRequest)(nil), // 5: daemon.UpRequest - (*UpResponse)(nil), // 6: daemon.UpResponse - (*StatusRequest)(nil), // 7: daemon.StatusRequest - (*StatusResponse)(nil), // 8: daemon.StatusResponse - (*DownRequest)(nil), // 9: daemon.DownRequest - (*DownResponse)(nil), // 10: daemon.DownResponse - (*GetConfigRequest)(nil), // 11: daemon.GetConfigRequest - (*GetConfigResponse)(nil), // 12: daemon.GetConfigResponse - (*PeerState)(nil), // 13: daemon.PeerState - (*LocalPeerState)(nil), // 14: daemon.LocalPeerState - (*SignalState)(nil), // 15: daemon.SignalState - (*ManagementState)(nil), // 16: daemon.ManagementState - (*RelayState)(nil), // 17: daemon.RelayState - (*NSGroupState)(nil), // 18: daemon.NSGroupState - (*FullStatus)(nil), // 19: daemon.FullStatus - (*ListRoutesRequest)(nil), // 20: daemon.ListRoutesRequest - (*ListRoutesResponse)(nil), // 21: daemon.ListRoutesResponse - (*SelectRoutesRequest)(nil), // 22: daemon.SelectRoutesRequest - (*SelectRoutesResponse)(nil), // 23: daemon.SelectRoutesResponse - (*IPList)(nil), // 24: daemon.IPList - (*Route)(nil), // 25: daemon.Route - (*DebugBundleRequest)(nil), // 26: daemon.DebugBundleRequest - (*DebugBundleResponse)(nil), // 27: daemon.DebugBundleResponse - (*GetLogLevelRequest)(nil), // 28: daemon.GetLogLevelRequest - (*GetLogLevelResponse)(nil), // 29: daemon.GetLogLevelResponse - (*SetLogLevelRequest)(nil), // 30: daemon.SetLogLevelRequest - (*SetLogLevelResponse)(nil), // 31: daemon.SetLogLevelResponse - nil, // 32: daemon.Route.ResolvedIPsEntry - (*durationpb.Duration)(nil), // 33: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp + (LogLevel)(0), // 0: daemon.LogLevel + (*LoginRequest)(nil), // 1: daemon.LoginRequest + (*LoginResponse)(nil), // 2: daemon.LoginResponse + (*WaitSSOLoginRequest)(nil), // 3: daemon.WaitSSOLoginRequest + (*WaitSSOLoginResponse)(nil), // 4: daemon.WaitSSOLoginResponse + (*UpRequest)(nil), // 5: daemon.UpRequest + (*UpResponse)(nil), // 6: daemon.UpResponse + (*StatusRequest)(nil), // 7: daemon.StatusRequest + (*StatusResponse)(nil), // 8: daemon.StatusResponse + (*DownRequest)(nil), // 9: daemon.DownRequest + (*DownResponse)(nil), // 10: daemon.DownResponse + (*GetConfigRequest)(nil), // 11: daemon.GetConfigRequest + (*GetConfigResponse)(nil), // 12: daemon.GetConfigResponse + (*PeerState)(nil), // 13: daemon.PeerState + (*LocalPeerState)(nil), // 14: daemon.LocalPeerState + (*SignalState)(nil), // 15: daemon.SignalState + (*ManagementState)(nil), // 16: daemon.ManagementState + (*RelayState)(nil), // 17: daemon.RelayState + (*NSGroupState)(nil), // 18: daemon.NSGroupState + (*FullStatus)(nil), // 19: daemon.FullStatus + (*ListRoutesRequest)(nil), // 20: daemon.ListRoutesRequest + (*ListRoutesResponse)(nil), // 21: daemon.ListRoutesResponse + (*SelectRoutesRequest)(nil), // 22: daemon.SelectRoutesRequest + (*SelectRoutesResponse)(nil), // 23: daemon.SelectRoutesResponse + (*IPList)(nil), // 24: daemon.IPList + (*Route)(nil), // 25: daemon.Route + (*DebugBundleRequest)(nil), // 26: daemon.DebugBundleRequest + (*DebugBundleResponse)(nil), // 27: daemon.DebugBundleResponse + (*GetLogLevelRequest)(nil), // 28: daemon.GetLogLevelRequest + (*GetLogLevelResponse)(nil), // 29: daemon.GetLogLevelResponse + (*SetLogLevelRequest)(nil), // 30: daemon.SetLogLevelRequest + (*SetLogLevelResponse)(nil), // 31: daemon.SetLogLevelResponse + (*SetNetworkMapPersistenceRequest)(nil), // 32: daemon.SetNetworkMapPersistenceRequest + (*SetNetworkMapPersistenceResponse)(nil), // 33: daemon.SetNetworkMapPersistenceResponse + nil, // 34: daemon.Route.ResolvedIPsEntry + (*durationpb.Duration)(nil), // 35: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 33, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 35, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 19, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 34, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 34, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 33, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 36, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 36, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 35, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 16, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 15, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState 14, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState @@ -2525,7 +2625,7 @@ var file_daemon_proto_depIdxs = []int32{ 17, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState 18, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState 25, // 11: daemon.ListRoutesResponse.routes:type_name -> daemon.Route - 32, // 12: daemon.Route.resolvedIPs:type_name -> daemon.Route.ResolvedIPsEntry + 34, // 12: daemon.Route.resolvedIPs:type_name -> daemon.Route.ResolvedIPsEntry 0, // 13: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel 0, // 14: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel 24, // 15: daemon.Route.ResolvedIPsEntry.value:type_name -> daemon.IPList @@ -2541,20 +2641,22 @@ var file_daemon_proto_depIdxs = []int32{ 26, // 25: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest 28, // 26: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest 30, // 27: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 2, // 28: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 4, // 29: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 6, // 30: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 8, // 31: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 10, // 32: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 12, // 33: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 21, // 34: daemon.DaemonService.ListRoutes:output_type -> daemon.ListRoutesResponse - 23, // 35: daemon.DaemonService.SelectRoutes:output_type -> daemon.SelectRoutesResponse - 23, // 36: daemon.DaemonService.DeselectRoutes:output_type -> daemon.SelectRoutesResponse - 27, // 37: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 29, // 38: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 31, // 39: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 28, // [28:40] is the sub-list for method output_type - 16, // [16:28] is the sub-list for method input_type + 32, // 28: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest + 2, // 29: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 4, // 30: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 6, // 31: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 8, // 32: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 10, // 33: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 12, // 34: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 21, // 35: daemon.DaemonService.ListRoutes:output_type -> daemon.ListRoutesResponse + 23, // 36: daemon.DaemonService.SelectRoutes:output_type -> daemon.SelectRoutesResponse + 23, // 37: daemon.DaemonService.DeselectRoutes:output_type -> daemon.SelectRoutesResponse + 27, // 38: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 29, // 39: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 31, // 40: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 33, // 41: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse + 29, // [29:42] is the sub-list for method output_type + 16, // [16:29] is the sub-list for method input_type 16, // [16:16] is the sub-list for extension type_name 16, // [16:16] is the sub-list for extension extendee 0, // [0:16] is the sub-list for field type_name @@ -2938,6 +3040,30 @@ func file_daemon_proto_init() { return nil } } + file_daemon_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetNetworkMapPersistenceRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetNetworkMapPersistenceResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_daemon_proto_msgTypes[0].OneofWrappers = []interface{}{} type x struct{} @@ -2946,7 +3072,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 1, - NumMessages: 32, + NumMessages: 34, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 384bc0e6206..04aa6688262 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -45,7 +45,11 @@ service DaemonService { // SetLogLevel sets the log level of the daemon rpc SetLogLevel(SetLogLevelRequest) returns (SetLogLevelResponse) {} -}; + + // SetNetworkMapPersistence enables or disables network map persistence + rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {} +} + message LoginRequest { // setupKey wiretrustee setup key. @@ -293,4 +297,10 @@ message SetLogLevelRequest { } message SetLogLevelResponse { -} \ No newline at end of file +} + +message SetNetworkMapPersistenceRequest { + bool enabled = 1; +} + +message SetNetworkMapPersistenceResponse {} diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index e0bc117e52d..31645763e0c 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -43,6 +43,8 @@ type DaemonServiceClient interface { GetLogLevel(ctx context.Context, in *GetLogLevelRequest, opts ...grpc.CallOption) (*GetLogLevelResponse, error) // SetLogLevel sets the log level of the daemon SetLogLevel(ctx context.Context, in *SetLogLevelRequest, opts ...grpc.CallOption) (*SetLogLevelResponse, error) + // SetNetworkMapPersistence enables or disables network map persistence + SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) } type daemonServiceClient struct { @@ -161,6 +163,15 @@ func (c *daemonServiceClient) SetLogLevel(ctx context.Context, in *SetLogLevelRe return out, nil } +func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) { + out := new(SetNetworkMapPersistenceResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetNetworkMapPersistence", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // DaemonServiceServer is the server API for DaemonService service. // All implementations must embed UnimplementedDaemonServiceServer // for forward compatibility @@ -190,6 +201,8 @@ type DaemonServiceServer interface { GetLogLevel(context.Context, *GetLogLevelRequest) (*GetLogLevelResponse, error) // SetLogLevel sets the log level of the daemon SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error) + // SetNetworkMapPersistence enables or disables network map persistence + SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) mustEmbedUnimplementedDaemonServiceServer() } @@ -233,6 +246,9 @@ func (UnimplementedDaemonServiceServer) GetLogLevel(context.Context, *GetLogLeve func (UnimplementedDaemonServiceServer) SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetLogLevel not implemented") } +func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetNetworkMapPersistence not implemented") +} func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {} // UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service. @@ -462,6 +478,24 @@ func _DaemonService_SetLogLevel_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetNetworkMapPersistenceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/SetNetworkMapPersistence", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).SetNetworkMapPersistence(ctx, req.(*SetNetworkMapPersistenceRequest)) + } + return interceptor(ctx, in, info, handler) +} + // DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -517,6 +551,10 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetLogLevel", Handler: _DaemonService_SetLogLevel_Handler, }, + { + MethodName: "SetNetworkMapPersistence", + Handler: _DaemonService_SetNetworkMapPersistence_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "daemon.proto", diff --git a/client/server/debug.go b/client/server/debug.go index d87fc9b6a2d..c12fd99dbf2 100644 --- a/client/server/debug.go +++ b/client/server/debug.go @@ -21,12 +21,14 @@ import ( "time" log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/encoding/protojson" "github.com/netbirdio/netbird/client/anonymize" "github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/routemanager/systemops" "github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/proto" + mgmProto "github.com/netbirdio/netbird/management/proto" ) const readmeContent = `Netbird debug bundle @@ -39,6 +41,7 @@ netbird.out: Most recent, anonymized stdout log file of the NetBird client. routes.txt: Anonymized system routes, if --system-info flag was provided. interfaces.txt: Anonymized network interface information, if --system-info flag was provided. config.txt: Anonymized configuration information of the NetBird client. +network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules. state.json: Anonymized client state dump containing netbird states. @@ -59,6 +62,16 @@ Domains All domain names (except for the netbird domains) are replaced with randomly generated strings ending in ".domain". Anonymized domains are consistent across all files in the bundle. Reoccuring domain names are replaced with the same anonymized domain. +Network Map +The network_map.json file contains the following anonymized information: +- Peer configurations (addresses, FQDNs, DNS settings) +- Remote and offline peer information (allowed IPs, FQDNs) +- Routes (network ranges, associated domains) +- DNS configuration (nameservers, domains, custom zones) +- Firewall rules (peer IPs, source/destination ranges) + +SSH keys in the network map are replaced with a placeholder value. All IP addresses and domains in the network map follow the same anonymization rules as described above. + State File The state.json file contains anonymized internal state information of the NetBird client, including: - DNS settings and configuration @@ -148,19 +161,23 @@ func (s *Server) createArchive(bundlePath *os.File, req *proto.DebugBundleReques seedFromStatus(anonymizer, &status) if err := s.addConfig(req, anonymizer, archive); err != nil { - return fmt.Errorf("add config: %w", err) + log.Errorf("Failed to add config to debug bundle: %v", err) } if req.GetSystemInfo() { if err := s.addRoutes(req, anonymizer, archive); err != nil { - return fmt.Errorf("add routes: %w", err) + log.Errorf("Failed to add routes to debug bundle: %v", err) } if err := s.addInterfaces(req, anonymizer, archive); err != nil { - return fmt.Errorf("add interfaces: %w", err) + log.Errorf("Failed to add interfaces to debug bundle: %v", err) } } + if err := s.addNetworkMap(req, anonymizer, archive); err != nil { + return fmt.Errorf("add network map: %w", err) + } + if err := s.addStateFile(req, anonymizer, archive); err != nil { log.Errorf("Failed to add state file to debug bundle: %v", err) } @@ -253,15 +270,16 @@ func (s *Server) addCommonConfigFields(configContent *strings.Builder) { } func (s *Server) addRoutes(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { - if routes, err := systemops.GetRoutesFromTable(); err != nil { - log.Errorf("Failed to get routes: %v", err) - } else { - // TODO: get routes including nexthop - routesContent := formatRoutes(routes, req.GetAnonymize(), anonymizer) - routesReader := strings.NewReader(routesContent) - if err := addFileToZip(archive, routesReader, "routes.txt"); err != nil { - return fmt.Errorf("add routes file to zip: %w", err) - } + routes, err := systemops.GetRoutesFromTable() + if err != nil { + return fmt.Errorf("get routes: %w", err) + } + + // TODO: get routes including nexthop + routesContent := formatRoutes(routes, req.GetAnonymize(), anonymizer) + routesReader := strings.NewReader(routesContent) + if err := addFileToZip(archive, routesReader, "routes.txt"); err != nil { + return fmt.Errorf("add routes file to zip: %w", err) } return nil } @@ -281,6 +299,39 @@ func (s *Server) addInterfaces(req *proto.DebugBundleRequest, anonymizer *anonym return nil } +func (s *Server) addNetworkMap(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { + networkMap, err := s.getLatestNetworkMap() + if err != nil { + // Skip if network map is not available, but log it + log.Debugf("skipping empty network map in debug bundle: %v", err) + return nil + } + + if req.GetAnonymize() { + if err := anonymizeNetworkMap(networkMap, anonymizer); err != nil { + return fmt.Errorf("anonymize network map: %w", err) + } + } + + options := protojson.MarshalOptions{ + EmitUnpopulated: true, + UseProtoNames: true, + Indent: " ", + AllowPartial: true, + } + + jsonBytes, err := options.Marshal(networkMap) + if err != nil { + return fmt.Errorf("generate json: %w", err) + } + + if err := addFileToZip(archive, bytes.NewReader(jsonBytes), "network_map.json"); err != nil { + return fmt.Errorf("add network map to zip: %w", err) + } + + return nil +} + func (s *Server) addStateFile(req *proto.DebugBundleRequest, anonymizer *anonymize.Anonymizer, archive *zip.Writer) error { path := statemanager.GetDefaultStatePath() if path == "" { @@ -368,14 +419,43 @@ func (s *Server) addSingleLogfile(logPath, targetName string, req *proto.DebugBu return nil } +// getLatestNetworkMap returns the latest network map from the engine if network map persistence is enabled +func (s *Server) getLatestNetworkMap() (*mgmProto.NetworkMap, error) { + if s.connectClient == nil { + return nil, errors.New("connect client is not initialized") + } + + engine := s.connectClient.Engine() + if engine == nil { + return nil, errors.New("engine is not initialized") + } + + networkMap, err := engine.GetLatestNetworkMap() + if err != nil { + return nil, fmt.Errorf("get latest network map: %w", err) + } + + if networkMap == nil { + return nil, errors.New("network map is not available") + } + + return networkMap, nil +} + // GetLogLevel gets the current logging level for the server. func (s *Server) GetLogLevel(_ context.Context, _ *proto.GetLogLevelRequest) (*proto.GetLogLevelResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + level := ParseLogLevel(log.GetLevel().String()) return &proto.GetLogLevelResponse{Level: level}, nil } // SetLogLevel sets the logging level for the server. func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) (*proto.SetLogLevelResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + level, err := log.ParseLevel(req.Level.String()) if err != nil { return nil, fmt.Errorf("invalid log level: %w", err) @@ -386,6 +466,20 @@ func (s *Server) SetLogLevel(_ context.Context, req *proto.SetLogLevelRequest) ( return &proto.SetLogLevelResponse{}, nil } +// SetNetworkMapPersistence sets the network map persistence for the server. +func (s *Server) SetNetworkMapPersistence(_ context.Context, req *proto.SetNetworkMapPersistenceRequest) (*proto.SetNetworkMapPersistenceResponse, error) { + s.mutex.Lock() + defer s.mutex.Unlock() + + enabled := req.GetEnabled() + s.persistNetworkMap = enabled + if s.connectClient != nil { + s.connectClient.SetNetworkMapPersistence(enabled) + } + + return &proto.SetNetworkMapPersistenceResponse{}, nil +} + func addFileToZip(archive *zip.Writer, reader io.Reader, filename string) error { header := &zip.FileHeader{ Name: filename, @@ -578,6 +672,177 @@ func anonymizeNATExternalIPs(ips []string, anonymizer *anonymize.Anonymizer) []s return anonymizedIPs } +func anonymizeNetworkMap(networkMap *mgmProto.NetworkMap, anonymizer *anonymize.Anonymizer) error { + if networkMap.PeerConfig != nil { + anonymizePeerConfig(networkMap.PeerConfig, anonymizer) + } + + for _, peer := range networkMap.RemotePeers { + anonymizeRemotePeer(peer, anonymizer) + } + + for _, peer := range networkMap.OfflinePeers { + anonymizeRemotePeer(peer, anonymizer) + } + + for _, r := range networkMap.Routes { + anonymizeRoute(r, anonymizer) + } + + if networkMap.DNSConfig != nil { + anonymizeDNSConfig(networkMap.DNSConfig, anonymizer) + } + + for _, rule := range networkMap.FirewallRules { + anonymizeFirewallRule(rule, anonymizer) + } + + for _, rule := range networkMap.RoutesFirewallRules { + anonymizeRouteFirewallRule(rule, anonymizer) + } + + return nil +} + +func anonymizePeerConfig(config *mgmProto.PeerConfig, anonymizer *anonymize.Anonymizer) { + if config == nil { + return + } + + if addr, err := netip.ParseAddr(config.Address); err == nil { + config.Address = anonymizer.AnonymizeIP(addr).String() + } + + if config.SshConfig != nil && len(config.SshConfig.SshPubKey) > 0 { + config.SshConfig.SshPubKey = []byte("ssh-placeholder-key") + } + + config.Dns = anonymizer.AnonymizeString(config.Dns) + config.Fqdn = anonymizer.AnonymizeDomain(config.Fqdn) +} + +func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.Anonymizer) { + if peer == nil { + return + } + + for i, ip := range peer.AllowedIps { + // Try to parse as prefix first (CIDR) + if prefix, err := netip.ParsePrefix(ip); err == nil { + anonIP := anonymizer.AnonymizeIP(prefix.Addr()) + peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) + } else if addr, err := netip.ParseAddr(ip); err == nil { + peer.AllowedIps[i] = anonymizer.AnonymizeIP(addr).String() + } + } + + peer.Fqdn = anonymizer.AnonymizeDomain(peer.Fqdn) + + if peer.SshConfig != nil && len(peer.SshConfig.SshPubKey) > 0 { + peer.SshConfig.SshPubKey = []byte("ssh-placeholder-key") + } +} + +func anonymizeRoute(route *mgmProto.Route, anonymizer *anonymize.Anonymizer) { + if route == nil { + return + } + + if prefix, err := netip.ParsePrefix(route.Network); err == nil { + anonIP := anonymizer.AnonymizeIP(prefix.Addr()) + route.Network = fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) + } + + for i, domain := range route.Domains { + route.Domains[i] = anonymizer.AnonymizeDomain(domain) + } + + route.NetID = anonymizer.AnonymizeString(route.NetID) +} + +func anonymizeDNSConfig(config *mgmProto.DNSConfig, anonymizer *anonymize.Anonymizer) { + if config == nil { + return + } + + anonymizeNameServerGroups(config.NameServerGroups, anonymizer) + anonymizeCustomZones(config.CustomZones, anonymizer) +} + +func anonymizeNameServerGroups(groups []*mgmProto.NameServerGroup, anonymizer *anonymize.Anonymizer) { + for _, group := range groups { + anonymizeServers(group.NameServers, anonymizer) + anonymizeDomains(group.Domains, anonymizer) + } +} + +func anonymizeServers(servers []*mgmProto.NameServer, anonymizer *anonymize.Anonymizer) { + for _, server := range servers { + if addr, err := netip.ParseAddr(server.IP); err == nil { + server.IP = anonymizer.AnonymizeIP(addr).String() + } + } +} + +func anonymizeDomains(domains []string, anonymizer *anonymize.Anonymizer) { + for i, domain := range domains { + domains[i] = anonymizer.AnonymizeDomain(domain) + } +} + +func anonymizeCustomZones(zones []*mgmProto.CustomZone, anonymizer *anonymize.Anonymizer) { + for _, zone := range zones { + zone.Domain = anonymizer.AnonymizeDomain(zone.Domain) + anonymizeRecords(zone.Records, anonymizer) + } +} + +func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) { + for _, record := range records { + record.Name = anonymizer.AnonymizeDomain(record.Name) + anonymizeRData(record, anonymizer) + } +} + +func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) { + switch record.Type { + case 1, 28: // A or AAAA record + if addr, err := netip.ParseAddr(record.RData); err == nil { + record.RData = anonymizer.AnonymizeIP(addr).String() + } + default: + record.RData = anonymizer.AnonymizeString(record.RData) + } +} + +func anonymizeFirewallRule(rule *mgmProto.FirewallRule, anonymizer *anonymize.Anonymizer) { + if rule == nil { + return + } + + if addr, err := netip.ParseAddr(rule.PeerIP); err == nil { + rule.PeerIP = anonymizer.AnonymizeIP(addr).String() + } +} + +func anonymizeRouteFirewallRule(rule *mgmProto.RouteFirewallRule, anonymizer *anonymize.Anonymizer) { + if rule == nil { + return + } + + for i, sourceRange := range rule.SourceRanges { + if prefix, err := netip.ParsePrefix(sourceRange); err == nil { + anonIP := anonymizer.AnonymizeIP(prefix.Addr()) + rule.SourceRanges[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) + } + } + + if prefix, err := netip.ParsePrefix(rule.Destination); err == nil { + anonIP := anonymizer.AnonymizeIP(prefix.Addr()) + rule.Destination = fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) + } +} + func anonymizeStateFile(rawStates *map[string]json.RawMessage, anonymizer *anonymize.Anonymizer) error { for name, rawState := range *rawStates { if string(rawState) == "null" { diff --git a/client/server/debug_test.go b/client/server/debug_test.go index 1515036bd02..c8f7bae5d38 100644 --- a/client/server/debug_test.go +++ b/client/server/debug_test.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "net" "strings" "testing" @@ -9,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "github.com/netbirdio/netbird/client/anonymize" + mgmProto "github.com/netbirdio/netbird/management/proto" ) func TestAnonymizeStateFile(t *testing.T) { @@ -263,3 +265,166 @@ func mustMarshal(v any) json.RawMessage { } return data } + +func TestAnonymizeNetworkMap(t *testing.T) { + networkMap := &mgmProto.NetworkMap{ + PeerConfig: &mgmProto.PeerConfig{ + Address: "203.0.113.5", + Dns: "1.2.3.4", + Fqdn: "peer1.corp.example.com", + SshConfig: &mgmProto.SSHConfig{ + SshPubKey: []byte("ssh-rsa AAAAB3NzaC1..."), + }, + }, + RemotePeers: []*mgmProto.RemotePeerConfig{ + { + AllowedIps: []string{ + "203.0.113.1/32", + "2001:db8:1234::1/128", + "192.168.1.1/32", + "100.64.0.1/32", + "10.0.0.1/32", + }, + Fqdn: "peer2.corp.example.com", + SshConfig: &mgmProto.SSHConfig{ + SshPubKey: []byte("ssh-rsa AAAAB3NzaC2..."), + }, + }, + }, + Routes: []*mgmProto.Route{ + { + Network: "197.51.100.0/24", + Domains: []string{"prod.example.com", "staging.example.com"}, + NetID: "net-123abc", + }, + }, + DNSConfig: &mgmProto.DNSConfig{ + NameServerGroups: []*mgmProto.NameServerGroup{ + { + NameServers: []*mgmProto.NameServer{ + {IP: "8.8.8.8"}, + {IP: "1.1.1.1"}, + {IP: "203.0.113.53"}, + }, + Domains: []string{"example.com", "internal.example.com"}, + }, + }, + CustomZones: []*mgmProto.CustomZone{ + { + Domain: "custom.example.com", + Records: []*mgmProto.SimpleRecord{ + { + Name: "www.custom.example.com", + Type: 1, + RData: "203.0.113.10", + }, + { + Name: "internal.custom.example.com", + Type: 1, + RData: "192.168.1.10", + }, + }, + }, + }, + }, + } + + // Create anonymizer with test addresses + anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses()) + + // Anonymize the network map + err := anonymizeNetworkMap(networkMap, anonymizer) + require.NoError(t, err) + + // Test PeerConfig anonymization + peerCfg := networkMap.PeerConfig + require.NotEqual(t, "203.0.113.5", peerCfg.Address) + + // Verify DNS and FQDN are properly anonymized + require.NotEqual(t, "1.2.3.4", peerCfg.Dns) + require.NotEqual(t, "peer1.corp.example.com", peerCfg.Fqdn) + require.True(t, strings.HasSuffix(peerCfg.Fqdn, ".domain")) + + // Verify SSH key is replaced + require.Equal(t, []byte("ssh-placeholder-key"), peerCfg.SshConfig.SshPubKey) + + // Test RemotePeers anonymization + remotePeer := networkMap.RemotePeers[0] + + // Verify FQDN is anonymized + require.NotEqual(t, "peer2.corp.example.com", remotePeer.Fqdn) + require.True(t, strings.HasSuffix(remotePeer.Fqdn, ".domain")) + + // Check that public IPs are anonymized but private IPs are preserved + for _, allowedIP := range remotePeer.AllowedIps { + ip, _, err := net.ParseCIDR(allowedIP) + require.NoError(t, err) + + if ip.IsPrivate() || isInCGNATRange(ip) { + require.Contains(t, []string{ + "192.168.1.1/32", + "100.64.0.1/32", + "10.0.0.1/32", + }, allowedIP) + } else { + require.NotContains(t, []string{ + "203.0.113.1/32", + "2001:db8:1234::1/128", + }, allowedIP) + } + } + + // Test Routes anonymization + route := networkMap.Routes[0] + require.NotEqual(t, "197.51.100.0/24", route.Network) + for _, domain := range route.Domains { + require.True(t, strings.HasSuffix(domain, ".domain")) + require.NotContains(t, domain, "example.com") + } + + // Test DNS config anonymization + dnsConfig := networkMap.DNSConfig + nameServerGroup := dnsConfig.NameServerGroups[0] + + // Verify well-known DNS servers are preserved + require.Equal(t, "8.8.8.8", nameServerGroup.NameServers[0].IP) + require.Equal(t, "1.1.1.1", nameServerGroup.NameServers[1].IP) + + // Verify public DNS server is anonymized + require.NotEqual(t, "203.0.113.53", nameServerGroup.NameServers[2].IP) + + // Verify domains are anonymized + for _, domain := range nameServerGroup.Domains { + require.True(t, strings.HasSuffix(domain, ".domain")) + require.NotContains(t, domain, "example.com") + } + + // Test CustomZones anonymization + customZone := dnsConfig.CustomZones[0] + require.True(t, strings.HasSuffix(customZone.Domain, ".domain")) + require.NotContains(t, customZone.Domain, "example.com") + + // Verify records are properly anonymized + for _, record := range customZone.Records { + require.True(t, strings.HasSuffix(record.Name, ".domain")) + require.NotContains(t, record.Name, "example.com") + + ip := net.ParseIP(record.RData) + if ip != nil { + if !ip.IsPrivate() { + require.NotEqual(t, "203.0.113.10", record.RData) + } else { + require.Equal(t, "192.168.1.10", record.RData) + } + } + } +} + +// Helper function to check if IP is in CGNAT range +func isInCGNATRange(ip net.IP) bool { + cgnat := net.IPNet{ + IP: net.ParseIP("100.64.0.0"), + Mask: net.CIDRMask(10, 32), + } + return cgnat.Contains(ip) +} diff --git a/client/server/server.go b/client/server/server.go index 106bdf32bbf..71eb58a66bc 100644 --- a/client/server/server.go +++ b/client/server/server.go @@ -68,6 +68,8 @@ type Server struct { relayProbe *internal.Probe wgProbe *internal.Probe lastProbe time.Time + + persistNetworkMap bool } type oauthAuthFlow struct { @@ -196,6 +198,7 @@ func (s *Server) connectWithRetryRuns(ctx context.Context, config *internal.Conf runOperation := func() error { log.Tracef("running client connection") s.connectClient = internal.NewConnectClient(ctx, config, statusRecorder) + s.connectClient.SetNetworkMapPersistence(s.persistNetworkMap) probes := internal.ProbeHolder{ MgmProbe: s.mgmProbe, diff --git a/go.mod b/go.mod index e8c65542280..c08713f2bd4 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 google.golang.org/grpc v1.64.1 - google.golang.org/protobuf v1.34.1 + google.golang.org/protobuf v1.34.2 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -224,7 +224,7 @@ require ( golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect diff --git a/go.sum b/go.sum index 47975d4eab4..5519d6ded99 100644 --- a/go.sum +++ b/go.sum @@ -1151,8 +1151,8 @@ google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaE google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434 h1:OpXbo8JnN8+jZGPrL4SSfaDjSCjupr8lXyBAbexEm/U= google.golang.org/genproto/googleapis/api v0.0.0-20240509183442-62759503f434/go.mod h1:FfiGhwUm6CJviekPrc0oJ+7h29e+DmWU6UtjX0ZvI7Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -1189,8 +1189,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 8866394eb6879aa94d5fbd259b64830fdca45569 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:33:41 +0100 Subject: [PATCH 15/17] [client] Don't choke on non-existent interface in route updates (#2922) --- client/firewall/nftables/state.go | 1 - .../internal/routemanager/systemops/systemops_windows.go | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) delete mode 100644 client/firewall/nftables/state.go diff --git a/client/firewall/nftables/state.go b/client/firewall/nftables/state.go deleted file mode 100644 index 7027fe98719..00000000000 --- a/client/firewall/nftables/state.go +++ /dev/null @@ -1 +0,0 @@ -package nftables diff --git a/client/internal/routemanager/systemops/systemops_windows.go b/client/internal/routemanager/systemops/systemops_windows.go index b1732a08001..ad325e12377 100644 --- a/client/internal/routemanager/systemops/systemops_windows.go +++ b/client/internal/routemanager/systemops/systemops_windows.go @@ -230,10 +230,13 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI if idx != 0 { intf, err := net.InterfaceByIndex(idx) if err != nil { - return update, fmt.Errorf("get interface name: %w", err) + log.Warnf("failed to get interface name for index %d: %v", idx, err) + update.Interface = &net.Interface{ + Index: idx, + } + } else { + update.Interface = intf } - - update.Interface = intf } log.Tracef("Received route update with destination %v, next hop %v, interface %v", row.DestinationPrefix, row.NextHop, update.Interface) From e5d42bc96330b8b10e7704c0f5a80fbcf960cb55 Mon Sep 17 00:00:00 2001 From: Viktor Liu <17948409+lixmal@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:07:18 +0100 Subject: [PATCH 16/17] [client] Add state handling cmdline options (#2821) --- client/cmd/state.go | 181 ++++++ client/internal/connect.go | 13 + client/internal/statemanager/manager.go | 220 +++++-- client/proto/daemon.pb.go | 726 ++++++++++++++++++++---- client/proto/daemon.proto | 45 ++ client/proto/daemon_grpc.pb.go | 114 ++++ client/server/state.go | 103 +++- 7 files changed, 1233 insertions(+), 169 deletions(-) create mode 100644 client/cmd/state.go diff --git a/client/cmd/state.go b/client/cmd/state.go new file mode 100644 index 00000000000..21a5508f4bd --- /dev/null +++ b/client/cmd/state.go @@ -0,0 +1,181 @@ +package cmd + +import ( + "fmt" + + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/client/proto" +) + +var ( + allFlag bool +) + +var stateCmd = &cobra.Command{ + Use: "state", + Short: "Manage daemon state", + Long: "Provides commands for managing and inspecting the Netbird daemon state.", +} + +var stateListCmd = &cobra.Command{ + Use: "list", + Aliases: []string{"ls"}, + Short: "List all stored states", + Long: "Lists all registered states with their status and basic information.", + Example: " netbird state list", + RunE: stateList, +} + +var stateCleanCmd = &cobra.Command{ + Use: "clean [state-name]", + Short: "Clean stored states", + Long: `Clean specific state or all states. The daemon must not be running. +This will perform cleanup operations and remove the state.`, + Example: ` netbird state clean dns_state + netbird state clean --all`, + RunE: stateClean, + PreRunE: func(cmd *cobra.Command, args []string) error { + // Check mutual exclusivity between --all flag and state-name argument + if allFlag && len(args) > 0 { + return fmt.Errorf("cannot specify both --all flag and state name") + } + if !allFlag && len(args) != 1 { + return fmt.Errorf("requires a state name argument or --all flag") + } + return nil + }, +} + +var stateDeleteCmd = &cobra.Command{ + Use: "delete [state-name]", + Short: "Delete stored states", + Long: `Delete specific state or all states from storage. The daemon must not be running. +This will remove the state without performing any cleanup operations.`, + Example: ` netbird state delete dns_state + netbird state delete --all`, + RunE: stateDelete, + PreRunE: func(cmd *cobra.Command, args []string) error { + // Check mutual exclusivity between --all flag and state-name argument + if allFlag && len(args) > 0 { + return fmt.Errorf("cannot specify both --all flag and state name") + } + if !allFlag && len(args) != 1 { + return fmt.Errorf("requires a state name argument or --all flag") + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(stateCmd) + stateCmd.AddCommand(stateListCmd, stateCleanCmd, stateDeleteCmd) + + stateCleanCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Clean all states") + stateDeleteCmd.Flags().BoolVarP(&allFlag, "all", "a", false, "Delete all states") +} + +func stateList(cmd *cobra.Command, _ []string) error { + conn, err := getClient(cmd) + if err != nil { + return err + } + defer func() { + if err := conn.Close(); err != nil { + log.Errorf(errCloseConnection, err) + } + }() + + client := proto.NewDaemonServiceClient(conn) + resp, err := client.ListStates(cmd.Context(), &proto.ListStatesRequest{}) + if err != nil { + return fmt.Errorf("failed to list states: %v", status.Convert(err).Message()) + } + + cmd.Printf("\nStored states:\n\n") + for _, state := range resp.States { + cmd.Printf("- %s\n", state.Name) + } + + return nil +} + +func stateClean(cmd *cobra.Command, args []string) error { + var stateName string + if !allFlag { + stateName = args[0] + } + + conn, err := getClient(cmd) + if err != nil { + return err + } + defer func() { + if err := conn.Close(); err != nil { + log.Errorf(errCloseConnection, err) + } + }() + + client := proto.NewDaemonServiceClient(conn) + resp, err := client.CleanState(cmd.Context(), &proto.CleanStateRequest{ + StateName: stateName, + All: allFlag, + }) + if err != nil { + return fmt.Errorf("failed to clean state: %v", status.Convert(err).Message()) + } + + if resp.CleanedStates == 0 { + cmd.Println("No states were cleaned") + return nil + } + + if allFlag { + cmd.Printf("Successfully cleaned %d states\n", resp.CleanedStates) + } else { + cmd.Printf("Successfully cleaned state %q\n", stateName) + } + + return nil +} + +func stateDelete(cmd *cobra.Command, args []string) error { + var stateName string + if !allFlag { + stateName = args[0] + } + + conn, err := getClient(cmd) + if err != nil { + return err + } + defer func() { + if err := conn.Close(); err != nil { + log.Errorf(errCloseConnection, err) + } + }() + + client := proto.NewDaemonServiceClient(conn) + resp, err := client.DeleteState(cmd.Context(), &proto.DeleteStateRequest{ + StateName: stateName, + All: allFlag, + }) + if err != nil { + return fmt.Errorf("failed to delete state: %v", status.Convert(err).Message()) + } + + if resp.DeletedStates == 0 { + cmd.Println("No states were deleted") + return nil + } + + if allFlag { + cmd.Printf("Successfully deleted %d states\n", resp.DeletedStates) + } else { + cmd.Printf("Successfully deleted state %q\n", stateName) + } + + return nil +} diff --git a/client/internal/connect.go b/client/internal/connect.go index 21f6d3ec869..4848b1c1143 100644 --- a/client/internal/connect.go +++ b/client/internal/connect.go @@ -338,6 +338,19 @@ func (c *ConnectClient) Engine() *Engine { return e } +// Status returns the current client status +func (c *ConnectClient) Status() StatusType { + if c == nil { + return StatusIdle + } + status, err := CtxGetState(c.ctx).Status() + if err != nil { + return StatusIdle + } + + return status +} + func (c *ConnectClient) Stop() error { if c == nil { return nil diff --git a/client/internal/statemanager/manager.go b/client/internal/statemanager/manager.go index aae73b79f5e..9a99c76f167 100644 --- a/client/internal/statemanager/manager.go +++ b/client/internal/statemanager/manager.go @@ -19,6 +19,11 @@ import ( "github.com/netbirdio/netbird/util" ) +const ( + errStateNotRegistered = "state %s not registered" + errLoadStateFile = "load state file: %w" +) + // State interface defines the methods that all state types must implement type State interface { Name() string @@ -159,7 +164,7 @@ func (m *Manager) setState(name string, state State) error { defer m.mu.Unlock() if _, exists := m.states[name]; !exists { - return fmt.Errorf("state %s not registered", name) + return fmt.Errorf(errStateNotRegistered, name) } m.states[name] = state @@ -168,6 +173,63 @@ func (m *Manager) setState(name string, state State) error { return nil } +// DeleteStateByName handles deletion of states without cleanup. +// It doesn't require the state to be registered. +func (m *Manager) DeleteStateByName(stateName string) error { + if m == nil { + return nil + } + + m.mu.Lock() + defer m.mu.Unlock() + + rawStates, err := m.loadStateFile(false) + if err != nil { + return fmt.Errorf(errLoadStateFile, err) + } + if rawStates == nil { + return nil + } + + if _, exists := rawStates[stateName]; !exists { + return fmt.Errorf("state %s not found", stateName) + } + + // Mark state as deleted by setting it to nil and marking it dirty + m.states[stateName] = nil + m.dirty[stateName] = struct{}{} + + return nil +} + +// DeleteAllStates removes all states. +func (m *Manager) DeleteAllStates() (int, error) { + if m == nil { + return 0, nil + } + + m.mu.Lock() + defer m.mu.Unlock() + + rawStates, err := m.loadStateFile(false) + if err != nil { + return 0, fmt.Errorf(errLoadStateFile, err) + } + if rawStates == nil { + return 0, nil + } + + count := len(rawStates) + + // Mark all states as deleted and dirty + for name := range rawStates { + m.states[name] = nil + m.dirty[name] = struct{}{} + } + + return count, nil +} + func (m *Manager) periodicStateSave(ctx context.Context) { ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() @@ -221,7 +283,7 @@ func (m *Manager) PersistState(ctx context.Context) error { } } - log.Debugf("persisted shutdown states: %v, took %v", maps.Keys(m.dirty), time.Since(start)) + log.Debugf("persisted states: %v, took %v", maps.Keys(m.dirty), time.Since(start)) clear(m.dirty) @@ -229,7 +291,7 @@ func (m *Manager) PersistState(ctx context.Context) error { } // loadStateFile reads and unmarshals the state file into a map of raw JSON messages -func (m *Manager) loadStateFile() (map[string]json.RawMessage, error) { +func (m *Manager) loadStateFile(deleteCorrupt bool) (map[string]json.RawMessage, error) { data, err := os.ReadFile(m.filePath) if err != nil { if errors.Is(err, fs.ErrNotExist) { @@ -241,11 +303,13 @@ func (m *Manager) loadStateFile() (map[string]json.RawMessage, error) { var rawStates map[string]json.RawMessage if err := json.Unmarshal(data, &rawStates); err != nil { - log.Warn("State file appears to be corrupted, attempting to delete it") - if err := os.Remove(m.filePath); err != nil { - log.Errorf("Failed to delete corrupted state file: %v", err) - } else { - log.Info("State file deleted") + if deleteCorrupt { + log.Warn("State file appears to be corrupted, attempting to delete it", err) + if err := os.Remove(m.filePath); err != nil { + log.Errorf("Failed to delete corrupted state file: %v", err) + } else { + log.Info("State file deleted") + } } return nil, fmt.Errorf("unmarshal states: %w", err) } @@ -257,7 +321,7 @@ func (m *Manager) loadStateFile() (map[string]json.RawMessage, error) { func (m *Manager) loadSingleRawState(name string, rawState json.RawMessage) (State, error) { stateType, ok := m.stateTypes[name] if !ok { - return nil, fmt.Errorf("state %s not registered", name) + return nil, fmt.Errorf(errStateNotRegistered, name) } if string(rawState) == "null" { @@ -281,7 +345,7 @@ func (m *Manager) LoadState(state State) error { m.mu.Lock() defer m.mu.Unlock() - rawStates, err := m.loadStateFile() + rawStates, err := m.loadStateFile(false) if err != nil { return err } @@ -308,6 +372,84 @@ func (m *Manager) LoadState(state State) error { return nil } +// cleanupSingleState handles the cleanup of a specific state and returns any error. +// The caller must hold the mutex. +func (m *Manager) cleanupSingleState(name string, rawState json.RawMessage) error { + // For unregistered states, preserve the raw JSON + if _, registered := m.stateTypes[name]; !registered { + m.states[name] = &RawState{data: rawState} + return nil + } + + // Load the state + loadedState, err := m.loadSingleRawState(name, rawState) + if err != nil { + return err + } + + if loadedState == nil { + return nil + } + + // Check if state supports cleanup + cleanableState, isCleanable := loadedState.(CleanableState) + if !isCleanable { + // If it doesn't support cleanup, keep it as-is + m.states[name] = loadedState + return nil + } + + // Perform cleanup + log.Infof("cleaning up state %s", name) + if err := cleanableState.Cleanup(); err != nil { + // On cleanup error, preserve the state + m.states[name] = loadedState + return fmt.Errorf("cleanup state: %w", err) + } + + // Successfully cleaned up - mark for deletion + m.states[name] = nil + m.dirty[name] = struct{}{} + return nil +} + +// CleanupStateByName loads and cleans up a specific state by name if it implements CleanableState. +// Returns an error if the state doesn't exist, isn't registered, or cleanup fails. +func (m *Manager) CleanupStateByName(name string) error { + if m == nil { + return nil + } + + m.mu.Lock() + defer m.mu.Unlock() + + // Check if state is registered + if _, registered := m.stateTypes[name]; !registered { + return fmt.Errorf(errStateNotRegistered, name) + } + + // Load raw states from file + rawStates, err := m.loadStateFile(false) + if err != nil { + return err + } + if rawStates == nil { + return nil + } + + // Check if state exists in file + rawState, exists := rawStates[name] + if !exists { + return nil + } + + if err := m.cleanupSingleState(name, rawState); err != nil { + return fmt.Errorf("%s: %w", name, err) + } + + return nil +} + // PerformCleanup retrieves all states from the state file and calls Cleanup on registered states that support it. // Unregistered states are preserved in their original state. func (m *Manager) PerformCleanup() error { @@ -319,10 +461,9 @@ func (m *Manager) PerformCleanup() error { defer m.mu.Unlock() // Load raw states from file - rawStates, err := m.loadStateFile() + rawStates, err := m.loadStateFile(true) if err != nil { - log.Warnf("Failed to load state during cleanup: %v", err) - return err + return fmt.Errorf(errLoadStateFile, err) } if rawStates == nil { return nil @@ -332,45 +473,36 @@ func (m *Manager) PerformCleanup() error { // Process each state in the file for name, rawState := range rawStates { - // For unregistered states, preserve the raw JSON - if _, registered := m.stateTypes[name]; !registered { - m.states[name] = &RawState{data: rawState} - continue + if err := m.cleanupSingleState(name, rawState); err != nil { + merr = multierror.Append(merr, fmt.Errorf("%s: %w", name, err)) } + } - // Load the registered state - loadedState, err := m.loadSingleRawState(name, rawState) - if err != nil { - merr = multierror.Append(merr, err) - continue - } + return nberrors.FormatErrorOrNil(merr) +} - if loadedState == nil { - continue - } +// GetSavedStateNames returns all state names that are currently saved in the state file. +func (m *Manager) GetSavedStateNames() ([]string, error) { + if m == nil { + return nil, nil + } - // Check if state supports cleanup - cleanableState, isCleanable := loadedState.(CleanableState) - if !isCleanable { - // If it doesn't support cleanup, keep it as-is - m.states[name] = loadedState - continue - } + rawStates, err := m.loadStateFile(false) + if err != nil { + return nil, fmt.Errorf(errLoadStateFile, err) + } + if rawStates == nil { + return nil, nil + } - // Perform cleanup for cleanable states - log.Infof("client was not shut down properly, cleaning up %s", name) - if err := cleanableState.Cleanup(); err != nil { - merr = multierror.Append(merr, fmt.Errorf("cleanup state for %s: %w", name, err)) - // On cleanup error, preserve the state - m.states[name] = loadedState - } else { - // Successfully cleaned up - mark for deletion - m.states[name] = nil - m.dirty[name] = struct{}{} + var states []string + for name, state := range rawStates { + if len(state) != 0 && string(state) != "null" { + states = append(states, name) } } - return nberrors.FormatErrorOrNil(merr) + return states, nil } func marshalWithPanicRecovery(v any) ([]byte, error) { diff --git a/client/proto/daemon.pb.go b/client/proto/daemon.pb.go index 0eae4e0d618..98ce2c4a289 100644 --- a/client/proto/daemon.pb.go +++ b/client/proto/daemon.pb.go @@ -2103,6 +2103,349 @@ func (*SetLogLevelResponse) Descriptor() ([]byte, []int) { return file_daemon_proto_rawDescGZIP(), []int{30} } +// State represents a daemon state entry +type State struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *State) Reset() { + *x = State{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *State) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*State) ProtoMessage() {} + +func (x *State) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[31] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use State.ProtoReflect.Descriptor instead. +func (*State) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{31} +} + +func (x *State) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// ListStatesRequest is empty as it requires no parameters +type ListStatesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *ListStatesRequest) Reset() { + *x = ListStatesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListStatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStatesRequest) ProtoMessage() {} + +func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[32] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListStatesRequest.ProtoReflect.Descriptor instead. +func (*ListStatesRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{32} +} + +// ListStatesResponse contains a list of states +type ListStatesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + States []*State `protobuf:"bytes,1,rep,name=states,proto3" json:"states,omitempty"` +} + +func (x *ListStatesResponse) Reset() { + *x = ListStatesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ListStatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStatesResponse) ProtoMessage() {} + +func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[33] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListStatesResponse.ProtoReflect.Descriptor instead. +func (*ListStatesResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{33} +} + +func (x *ListStatesResponse) GetStates() []*State { + if x != nil { + return x.States + } + return nil +} + +// CleanStateRequest for cleaning states +type CleanStateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StateName string `protobuf:"bytes,1,opt,name=state_name,json=stateName,proto3" json:"state_name,omitempty"` + All bool `protobuf:"varint,2,opt,name=all,proto3" json:"all,omitempty"` +} + +func (x *CleanStateRequest) Reset() { + *x = CleanStateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CleanStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CleanStateRequest) ProtoMessage() {} + +func (x *CleanStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[34] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CleanStateRequest.ProtoReflect.Descriptor instead. +func (*CleanStateRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{34} +} + +func (x *CleanStateRequest) GetStateName() string { + if x != nil { + return x.StateName + } + return "" +} + +func (x *CleanStateRequest) GetAll() bool { + if x != nil { + return x.All + } + return false +} + +// CleanStateResponse contains the result of the clean operation +type CleanStateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + CleanedStates int32 `protobuf:"varint,1,opt,name=cleaned_states,json=cleanedStates,proto3" json:"cleaned_states,omitempty"` +} + +func (x *CleanStateResponse) Reset() { + *x = CleanStateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CleanStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CleanStateResponse) ProtoMessage() {} + +func (x *CleanStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[35] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CleanStateResponse.ProtoReflect.Descriptor instead. +func (*CleanStateResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{35} +} + +func (x *CleanStateResponse) GetCleanedStates() int32 { + if x != nil { + return x.CleanedStates + } + return 0 +} + +// DeleteStateRequest for deleting states +type DeleteStateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StateName string `protobuf:"bytes,1,opt,name=state_name,json=stateName,proto3" json:"state_name,omitempty"` + All bool `protobuf:"varint,2,opt,name=all,proto3" json:"all,omitempty"` +} + +func (x *DeleteStateRequest) Reset() { + *x = DeleteStateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteStateRequest) ProtoMessage() {} + +func (x *DeleteStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[36] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteStateRequest.ProtoReflect.Descriptor instead. +func (*DeleteStateRequest) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{36} +} + +func (x *DeleteStateRequest) GetStateName() string { + if x != nil { + return x.StateName + } + return "" +} + +func (x *DeleteStateRequest) GetAll() bool { + if x != nil { + return x.All + } + return false +} + +// DeleteStateResponse contains the result of the delete operation +type DeleteStateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DeletedStates int32 `protobuf:"varint,1,opt,name=deleted_states,json=deletedStates,proto3" json:"deleted_states,omitempty"` +} + +func (x *DeleteStateResponse) Reset() { + *x = DeleteStateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_daemon_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteStateResponse) ProtoMessage() {} + +func (x *DeleteStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_daemon_proto_msgTypes[37] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteStateResponse.ProtoReflect.Descriptor instead. +func (*DeleteStateResponse) Descriptor() ([]byte, []int) { + return file_daemon_proto_rawDescGZIP(), []int{37} +} + +func (x *DeleteStateResponse) GetDeletedStates() int32 { + if x != nil { + return x.DeletedStates + } + return 0 +} + type SetNetworkMapPersistenceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2114,7 +2457,7 @@ type SetNetworkMapPersistenceRequest struct { func (x *SetNetworkMapPersistenceRequest) Reset() { *x = SetNetworkMapPersistenceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[31] + mi := &file_daemon_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2127,7 +2470,7 @@ func (x *SetNetworkMapPersistenceRequest) String() string { func (*SetNetworkMapPersistenceRequest) ProtoMessage() {} func (x *SetNetworkMapPersistenceRequest) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[31] + mi := &file_daemon_proto_msgTypes[38] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2140,7 +2483,7 @@ func (x *SetNetworkMapPersistenceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetNetworkMapPersistenceRequest.ProtoReflect.Descriptor instead. func (*SetNetworkMapPersistenceRequest) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{31} + return file_daemon_proto_rawDescGZIP(), []int{38} } func (x *SetNetworkMapPersistenceRequest) GetEnabled() bool { @@ -2159,7 +2502,7 @@ type SetNetworkMapPersistenceResponse struct { func (x *SetNetworkMapPersistenceResponse) Reset() { *x = SetNetworkMapPersistenceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_daemon_proto_msgTypes[32] + mi := &file_daemon_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2172,7 +2515,7 @@ func (x *SetNetworkMapPersistenceResponse) String() string { func (*SetNetworkMapPersistenceResponse) ProtoMessage() {} func (x *SetNetworkMapPersistenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_daemon_proto_msgTypes[32] + mi := &file_daemon_proto_msgTypes[39] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2185,7 +2528,7 @@ func (x *SetNetworkMapPersistenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetNetworkMapPersistenceResponse.ProtoReflect.Descriptor instead. func (*SetNetworkMapPersistenceResponse) Descriptor() ([]byte, []int) { - return file_daemon_proto_rawDescGZIP(), []int{32} + return file_daemon_proto_rawDescGZIP(), []int{39} } var File_daemon_proto protoreflect.FileDescriptor @@ -2484,79 +2827,116 @@ var file_daemon_proto_rawDesc = []byte{ 0x32, 0x10, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, - 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, - 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, - 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, - 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, - 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, - 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, - 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, - 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, - 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0xa9, 0x07, 0x0a, 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, - 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, - 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, - 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, - 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, - 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, - 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, - 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, - 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, - 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, - 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, - 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x65, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, - 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, - 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, - 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, - 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, - 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, - 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, - 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, - 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, - 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, - 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, - 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, - 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, - 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, - 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, - 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x00, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x22, 0x1b, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x13, 0x0a, + 0x11, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x3b, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, + 0x44, 0x0a, 0x11, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x74, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3b, 0x0a, 0x12, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, + 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0d, 0x63, 0x6c, 0x65, 0x61, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x22, 0x45, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x61, 0x6c, 0x6c, 0x22, 0x3c, 0x0a, 0x13, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x22, 0x3b, 0x0a, 0x1f, 0x53, 0x65, 0x74, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, + 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x22, 0x22, 0x0a, 0x20, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x62, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, + 0x65, 0x76, 0x65, 0x6c, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x41, 0x4e, 0x49, 0x43, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, + 0x46, 0x41, 0x54, 0x41, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x03, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x04, 0x12, 0x08, 0x0a, 0x04, + 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x05, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, + 0x06, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x07, 0x32, 0x81, 0x09, 0x0a, + 0x0d, 0x44, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x36, + 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x14, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, 0x0c, 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, + 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, + 0x57, 0x61, 0x69, 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x57, 0x61, 0x69, + 0x74, 0x53, 0x53, 0x4f, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x02, 0x55, 0x70, 0x12, 0x11, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x12, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x55, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x39, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x15, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, + 0x04, 0x44, 0x6f, 0x77, 0x6e, 0x12, 0x13, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, + 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x6f, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x42, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x18, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, + 0x75, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4b, 0x0a, + 0x0c, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, + 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x4d, 0x0a, 0x0e, 0x44, 0x65, + 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1b, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x64, 0x61, 0x65, 0x6d, + 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x62, + 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, + 0x62, 0x75, 0x67, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, + 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, + 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, + 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, + 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, + 0x0b, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x1a, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, 0x0a, 0x0a, 0x4c, 0x69, 0x73, 0x74, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x73, 0x12, 0x19, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x45, + 0x0a, 0x0a, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x2e, 0x64, + 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x1a, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1b, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x6f, 0x0a, 0x18, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, + 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x27, 0x2e, 0x64, 0x61, + 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x64, 0x61, 0x65, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, + 0x74, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x61, 0x70, 0x50, 0x65, 0x72, 0x73, 0x69, + 0x73, 0x74, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -2572,7 +2952,7 @@ func file_daemon_proto_rawDescGZIP() []byte { } var file_daemon_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 34) +var file_daemon_proto_msgTypes = make([]protoimpl.MessageInfo, 41) var file_daemon_proto_goTypes = []interface{}{ (LogLevel)(0), // 0: daemon.LogLevel (*LoginRequest)(nil), // 1: daemon.LoginRequest @@ -2606,18 +2986,25 @@ var file_daemon_proto_goTypes = []interface{}{ (*GetLogLevelResponse)(nil), // 29: daemon.GetLogLevelResponse (*SetLogLevelRequest)(nil), // 30: daemon.SetLogLevelRequest (*SetLogLevelResponse)(nil), // 31: daemon.SetLogLevelResponse - (*SetNetworkMapPersistenceRequest)(nil), // 32: daemon.SetNetworkMapPersistenceRequest - (*SetNetworkMapPersistenceResponse)(nil), // 33: daemon.SetNetworkMapPersistenceResponse - nil, // 34: daemon.Route.ResolvedIPsEntry - (*durationpb.Duration)(nil), // 35: google.protobuf.Duration - (*timestamppb.Timestamp)(nil), // 36: google.protobuf.Timestamp + (*State)(nil), // 32: daemon.State + (*ListStatesRequest)(nil), // 33: daemon.ListStatesRequest + (*ListStatesResponse)(nil), // 34: daemon.ListStatesResponse + (*CleanStateRequest)(nil), // 35: daemon.CleanStateRequest + (*CleanStateResponse)(nil), // 36: daemon.CleanStateResponse + (*DeleteStateRequest)(nil), // 37: daemon.DeleteStateRequest + (*DeleteStateResponse)(nil), // 38: daemon.DeleteStateResponse + (*SetNetworkMapPersistenceRequest)(nil), // 39: daemon.SetNetworkMapPersistenceRequest + (*SetNetworkMapPersistenceResponse)(nil), // 40: daemon.SetNetworkMapPersistenceResponse + nil, // 41: daemon.Route.ResolvedIPsEntry + (*durationpb.Duration)(nil), // 42: google.protobuf.Duration + (*timestamppb.Timestamp)(nil), // 43: google.protobuf.Timestamp } var file_daemon_proto_depIdxs = []int32{ - 35, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration + 42, // 0: daemon.LoginRequest.dnsRouteInterval:type_name -> google.protobuf.Duration 19, // 1: daemon.StatusResponse.fullStatus:type_name -> daemon.FullStatus - 36, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp - 36, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp - 35, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration + 43, // 2: daemon.PeerState.connStatusUpdate:type_name -> google.protobuf.Timestamp + 43, // 3: daemon.PeerState.lastWireguardHandshake:type_name -> google.protobuf.Timestamp + 42, // 4: daemon.PeerState.latency:type_name -> google.protobuf.Duration 16, // 5: daemon.FullStatus.managementState:type_name -> daemon.ManagementState 15, // 6: daemon.FullStatus.signalState:type_name -> daemon.SignalState 14, // 7: daemon.FullStatus.localPeerState:type_name -> daemon.LocalPeerState @@ -2625,41 +3012,48 @@ var file_daemon_proto_depIdxs = []int32{ 17, // 9: daemon.FullStatus.relays:type_name -> daemon.RelayState 18, // 10: daemon.FullStatus.dns_servers:type_name -> daemon.NSGroupState 25, // 11: daemon.ListRoutesResponse.routes:type_name -> daemon.Route - 34, // 12: daemon.Route.resolvedIPs:type_name -> daemon.Route.ResolvedIPsEntry + 41, // 12: daemon.Route.resolvedIPs:type_name -> daemon.Route.ResolvedIPsEntry 0, // 13: daemon.GetLogLevelResponse.level:type_name -> daemon.LogLevel 0, // 14: daemon.SetLogLevelRequest.level:type_name -> daemon.LogLevel - 24, // 15: daemon.Route.ResolvedIPsEntry.value:type_name -> daemon.IPList - 1, // 16: daemon.DaemonService.Login:input_type -> daemon.LoginRequest - 3, // 17: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest - 5, // 18: daemon.DaemonService.Up:input_type -> daemon.UpRequest - 7, // 19: daemon.DaemonService.Status:input_type -> daemon.StatusRequest - 9, // 20: daemon.DaemonService.Down:input_type -> daemon.DownRequest - 11, // 21: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest - 20, // 22: daemon.DaemonService.ListRoutes:input_type -> daemon.ListRoutesRequest - 22, // 23: daemon.DaemonService.SelectRoutes:input_type -> daemon.SelectRoutesRequest - 22, // 24: daemon.DaemonService.DeselectRoutes:input_type -> daemon.SelectRoutesRequest - 26, // 25: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest - 28, // 26: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest - 30, // 27: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest - 32, // 28: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest - 2, // 29: daemon.DaemonService.Login:output_type -> daemon.LoginResponse - 4, // 30: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse - 6, // 31: daemon.DaemonService.Up:output_type -> daemon.UpResponse - 8, // 32: daemon.DaemonService.Status:output_type -> daemon.StatusResponse - 10, // 33: daemon.DaemonService.Down:output_type -> daemon.DownResponse - 12, // 34: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse - 21, // 35: daemon.DaemonService.ListRoutes:output_type -> daemon.ListRoutesResponse - 23, // 36: daemon.DaemonService.SelectRoutes:output_type -> daemon.SelectRoutesResponse - 23, // 37: daemon.DaemonService.DeselectRoutes:output_type -> daemon.SelectRoutesResponse - 27, // 38: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse - 29, // 39: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse - 31, // 40: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse - 33, // 41: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse - 29, // [29:42] is the sub-list for method output_type - 16, // [16:29] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 32, // 15: daemon.ListStatesResponse.states:type_name -> daemon.State + 24, // 16: daemon.Route.ResolvedIPsEntry.value:type_name -> daemon.IPList + 1, // 17: daemon.DaemonService.Login:input_type -> daemon.LoginRequest + 3, // 18: daemon.DaemonService.WaitSSOLogin:input_type -> daemon.WaitSSOLoginRequest + 5, // 19: daemon.DaemonService.Up:input_type -> daemon.UpRequest + 7, // 20: daemon.DaemonService.Status:input_type -> daemon.StatusRequest + 9, // 21: daemon.DaemonService.Down:input_type -> daemon.DownRequest + 11, // 22: daemon.DaemonService.GetConfig:input_type -> daemon.GetConfigRequest + 20, // 23: daemon.DaemonService.ListRoutes:input_type -> daemon.ListRoutesRequest + 22, // 24: daemon.DaemonService.SelectRoutes:input_type -> daemon.SelectRoutesRequest + 22, // 25: daemon.DaemonService.DeselectRoutes:input_type -> daemon.SelectRoutesRequest + 26, // 26: daemon.DaemonService.DebugBundle:input_type -> daemon.DebugBundleRequest + 28, // 27: daemon.DaemonService.GetLogLevel:input_type -> daemon.GetLogLevelRequest + 30, // 28: daemon.DaemonService.SetLogLevel:input_type -> daemon.SetLogLevelRequest + 33, // 29: daemon.DaemonService.ListStates:input_type -> daemon.ListStatesRequest + 35, // 30: daemon.DaemonService.CleanState:input_type -> daemon.CleanStateRequest + 37, // 31: daemon.DaemonService.DeleteState:input_type -> daemon.DeleteStateRequest + 39, // 32: daemon.DaemonService.SetNetworkMapPersistence:input_type -> daemon.SetNetworkMapPersistenceRequest + 2, // 33: daemon.DaemonService.Login:output_type -> daemon.LoginResponse + 4, // 34: daemon.DaemonService.WaitSSOLogin:output_type -> daemon.WaitSSOLoginResponse + 6, // 35: daemon.DaemonService.Up:output_type -> daemon.UpResponse + 8, // 36: daemon.DaemonService.Status:output_type -> daemon.StatusResponse + 10, // 37: daemon.DaemonService.Down:output_type -> daemon.DownResponse + 12, // 38: daemon.DaemonService.GetConfig:output_type -> daemon.GetConfigResponse + 21, // 39: daemon.DaemonService.ListRoutes:output_type -> daemon.ListRoutesResponse + 23, // 40: daemon.DaemonService.SelectRoutes:output_type -> daemon.SelectRoutesResponse + 23, // 41: daemon.DaemonService.DeselectRoutes:output_type -> daemon.SelectRoutesResponse + 27, // 42: daemon.DaemonService.DebugBundle:output_type -> daemon.DebugBundleResponse + 29, // 43: daemon.DaemonService.GetLogLevel:output_type -> daemon.GetLogLevelResponse + 31, // 44: daemon.DaemonService.SetLogLevel:output_type -> daemon.SetLogLevelResponse + 34, // 45: daemon.DaemonService.ListStates:output_type -> daemon.ListStatesResponse + 36, // 46: daemon.DaemonService.CleanState:output_type -> daemon.CleanStateResponse + 38, // 47: daemon.DaemonService.DeleteState:output_type -> daemon.DeleteStateResponse + 40, // 48: daemon.DaemonService.SetNetworkMapPersistence:output_type -> daemon.SetNetworkMapPersistenceResponse + 33, // [33:49] is the sub-list for method output_type + 17, // [17:33] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_daemon_proto_init() } @@ -3041,7 +3435,7 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SetNetworkMapPersistenceRequest); i { + switch v := v.(*State); i { case 0: return &v.state case 1: @@ -3053,6 +3447,90 @@ func file_daemon_proto_init() { } } file_daemon_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListStatesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ListStatesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CleanStateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CleanStateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteStateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteStateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SetNetworkMapPersistenceRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_daemon_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SetNetworkMapPersistenceResponse); i { case 0: return &v.state @@ -3072,7 +3550,7 @@ func file_daemon_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_daemon_proto_rawDesc, NumEnums: 1, - NumMessages: 34, + NumMessages: 41, NumExtensions: 0, NumServices: 1, }, diff --git a/client/proto/daemon.proto b/client/proto/daemon.proto index 04aa6688262..96ade5b4e51 100644 --- a/client/proto/daemon.proto +++ b/client/proto/daemon.proto @@ -46,6 +46,15 @@ service DaemonService { // SetLogLevel sets the log level of the daemon rpc SetLogLevel(SetLogLevelRequest) returns (SetLogLevelResponse) {} + // List all states + rpc ListStates(ListStatesRequest) returns (ListStatesResponse) {} + + // Clean specific state or all states + rpc CleanState(CleanStateRequest) returns (CleanStateResponse) {} + + // Delete specific state or all states + rpc DeleteState(DeleteStateRequest) returns (DeleteStateResponse) {} + // SetNetworkMapPersistence enables or disables network map persistence rpc SetNetworkMapPersistence(SetNetworkMapPersistenceRequest) returns (SetNetworkMapPersistenceResponse) {} } @@ -299,6 +308,42 @@ message SetLogLevelRequest { message SetLogLevelResponse { } +// State represents a daemon state entry +message State { + string name = 1; +} + +// ListStatesRequest is empty as it requires no parameters +message ListStatesRequest {} + +// ListStatesResponse contains a list of states +message ListStatesResponse { + repeated State states = 1; +} + +// CleanStateRequest for cleaning states +message CleanStateRequest { + string state_name = 1; + bool all = 2; +} + +// CleanStateResponse contains the result of the clean operation +message CleanStateResponse { + int32 cleaned_states = 1; +} + +// DeleteStateRequest for deleting states +message DeleteStateRequest { + string state_name = 1; + bool all = 2; +} + +// DeleteStateResponse contains the result of the delete operation +message DeleteStateResponse { + int32 deleted_states = 1; +} + + message SetNetworkMapPersistenceRequest { bool enabled = 1; } diff --git a/client/proto/daemon_grpc.pb.go b/client/proto/daemon_grpc.pb.go index 31645763e0c..2e063604a4a 100644 --- a/client/proto/daemon_grpc.pb.go +++ b/client/proto/daemon_grpc.pb.go @@ -43,6 +43,12 @@ type DaemonServiceClient interface { GetLogLevel(ctx context.Context, in *GetLogLevelRequest, opts ...grpc.CallOption) (*GetLogLevelResponse, error) // SetLogLevel sets the log level of the daemon SetLogLevel(ctx context.Context, in *SetLogLevelRequest, opts ...grpc.CallOption) (*SetLogLevelResponse, error) + // List all states + ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) + // Clean specific state or all states + CleanState(ctx context.Context, in *CleanStateRequest, opts ...grpc.CallOption) (*CleanStateResponse, error) + // Delete specific state or all states + DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) } @@ -163,6 +169,33 @@ func (c *daemonServiceClient) SetLogLevel(ctx context.Context, in *SetLogLevelRe return out, nil } +func (c *daemonServiceClient) ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) { + out := new(ListStatesResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListStates", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) CleanState(ctx context.Context, in *CleanStateRequest, opts ...grpc.CallOption) (*CleanStateResponse, error) { + out := new(CleanStateResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/CleanState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *daemonServiceClient) DeleteState(ctx context.Context, in *DeleteStateRequest, opts ...grpc.CallOption) (*DeleteStateResponse, error) { + out := new(DeleteStateResponse) + err := c.cc.Invoke(ctx, "/daemon.DaemonService/DeleteState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *daemonServiceClient) SetNetworkMapPersistence(ctx context.Context, in *SetNetworkMapPersistenceRequest, opts ...grpc.CallOption) (*SetNetworkMapPersistenceResponse, error) { out := new(SetNetworkMapPersistenceResponse) err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetNetworkMapPersistence", in, out, opts...) @@ -201,6 +234,12 @@ type DaemonServiceServer interface { GetLogLevel(context.Context, *GetLogLevelRequest) (*GetLogLevelResponse, error) // SetLogLevel sets the log level of the daemon SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error) + // List all states + ListStates(context.Context, *ListStatesRequest) (*ListStatesResponse, error) + // Clean specific state or all states + CleanState(context.Context, *CleanStateRequest) (*CleanStateResponse, error) + // Delete specific state or all states + DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error) // SetNetworkMapPersistence enables or disables network map persistence SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) mustEmbedUnimplementedDaemonServiceServer() @@ -246,6 +285,15 @@ func (UnimplementedDaemonServiceServer) GetLogLevel(context.Context, *GetLogLeve func (UnimplementedDaemonServiceServer) SetLogLevel(context.Context, *SetLogLevelRequest) (*SetLogLevelResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetLogLevel not implemented") } +func (UnimplementedDaemonServiceServer) ListStates(context.Context, *ListStatesRequest) (*ListStatesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListStates not implemented") +} +func (UnimplementedDaemonServiceServer) CleanState(context.Context, *CleanStateRequest) (*CleanStateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CleanState not implemented") +} +func (UnimplementedDaemonServiceServer) DeleteState(context.Context, *DeleteStateRequest) (*DeleteStateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteState not implemented") +} func (UnimplementedDaemonServiceServer) SetNetworkMapPersistence(context.Context, *SetNetworkMapPersistenceRequest) (*SetNetworkMapPersistenceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method SetNetworkMapPersistence not implemented") } @@ -478,6 +526,60 @@ func _DaemonService_SetLogLevel_Handler(srv interface{}, ctx context.Context, de return interceptor(ctx, in, info, handler) } +func _DaemonService_ListStates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListStatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).ListStates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/ListStates", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).ListStates(ctx, req.(*ListStatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_CleanState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CleanStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).CleanState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/CleanState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).CleanState(ctx, req.(*CleanStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DaemonService_DeleteState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DaemonServiceServer).DeleteState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/daemon.DaemonService/DeleteState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DaemonServiceServer).DeleteState(ctx, req.(*DeleteStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _DaemonService_SetNetworkMapPersistence_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SetNetworkMapPersistenceRequest) if err := dec(in); err != nil { @@ -551,6 +653,18 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetLogLevel", Handler: _DaemonService_SetLogLevel_Handler, }, + { + MethodName: "ListStates", + Handler: _DaemonService_ListStates_Handler, + }, + { + MethodName: "CleanState", + Handler: _DaemonService_CleanState_Handler, + }, + { + MethodName: "DeleteState", + Handler: _DaemonService_DeleteState_Handler, + }, { MethodName: "SetNetworkMapPersistence", Handler: _DaemonService_SetNetworkMapPersistence_Handler, diff --git a/client/server/state.go b/client/server/state.go index 509782e86c7..222c7c7bd00 100644 --- a/client/server/state.go +++ b/client/server/state.go @@ -5,12 +5,112 @@ import ( "fmt" "github.com/hashicorp/go-multierror" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" nberrors "github.com/netbirdio/netbird/client/errors" + "github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/statemanager" + "github.com/netbirdio/netbird/client/proto" ) -// restoreResidualConfig checks if the client was not shut down in a clean way and restores residual state if required. +// ListStates returns a list of all saved states +func (s *Server) ListStates(_ context.Context, _ *proto.ListStatesRequest) (*proto.ListStatesResponse, error) { + mgr := statemanager.New(statemanager.GetDefaultStatePath()) + + stateNames, err := mgr.GetSavedStateNames() + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get saved state names: %v", err) + } + + states := make([]*proto.State, 0, len(stateNames)) + for _, name := range stateNames { + states = append(states, &proto.State{ + Name: name, + }) + } + + return &proto.ListStatesResponse{ + States: states, + }, nil +} + +// CleanState handles cleaning of states (performing cleanup operations) +func (s *Server) CleanState(ctx context.Context, req *proto.CleanStateRequest) (*proto.CleanStateResponse, error) { + if s.connectClient.Status() == internal.StatusConnected || s.connectClient.Status() == internal.StatusConnecting { + return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.") + } + + if req.All { + // Reuse existing cleanup logic for all states + if err := restoreResidualState(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "failed to clean all states: %v", err) + } + + // Get count of cleaned states + mgr := statemanager.New(statemanager.GetDefaultStatePath()) + stateNames, err := mgr.GetSavedStateNames() + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get state count: %v", err) + } + + return &proto.CleanStateResponse{ + CleanedStates: int32(len(stateNames)), + }, nil + } + + // Handle single state cleanup + mgr := statemanager.New(statemanager.GetDefaultStatePath()) + registerStates(mgr) + + if err := mgr.CleanupStateByName(req.StateName); err != nil { + return nil, status.Errorf(codes.Internal, "failed to clean state %s: %v", req.StateName, err) + } + + if err := mgr.PersistState(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "failed to persist state changes: %v", err) + } + + return &proto.CleanStateResponse{ + CleanedStates: 1, + }, nil +} + +// DeleteState handles deletion of states without cleanup +func (s *Server) DeleteState(ctx context.Context, req *proto.DeleteStateRequest) (*proto.DeleteStateResponse, error) { + if s.connectClient.Status() == internal.StatusConnected || s.connectClient.Status() == internal.StatusConnecting { + return nil, status.Errorf(codes.FailedPrecondition, "cannot clean state while connecting or connected, run 'netbird down' first.") + } + + mgr := statemanager.New(statemanager.GetDefaultStatePath()) + + var count int + var err error + + if req.All { + count, err = mgr.DeleteAllStates() + } else { + err = mgr.DeleteStateByName(req.StateName) + if err == nil { + count = 1 + } + } + + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete state: %v", err) + } + + // Persist the changes + if err := mgr.PersistState(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "failed to persist state changes: %v", err) + } + + return &proto.DeleteStateResponse{ + DeletedStates: int32(count), + }, nil +} + +// restoreResidualState checks if the client was not shut down in a clean way and restores residual if required. // Otherwise, we might not be able to connect to the management server to retrieve new config. func restoreResidualState(ctx context.Context) error { path := statemanager.GetDefaultStatePath() @@ -24,6 +124,7 @@ func restoreResidualState(ctx context.Context) error { registerStates(mgr) var merr *multierror.Error + if err := mgr.PerformCleanup(); err != nil { merr = multierror.Append(merr, fmt.Errorf("perform cleanup: %w", err)) } From d063fbb8b93f0053a0e734fa118816f0cdc5ad91 Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:41:19 +0100 Subject: [PATCH 17/17] [management] merge update account peers in sync call (#2978) --- management/server/peer.go | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/management/server/peer.go b/management/server/peer.go index a301544d5c3..474c2d6652e 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -678,10 +678,6 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac if err != nil { return nil, nil, nil, fmt.Errorf("failed to save peer: %w", err) } - - if sync.UpdateAccountPeers { - am.updateAccountPeers(ctx, account.Id) - } } peerNotValid, isStatusChanged, err := am.integratedPeerValidator.IsNotValidPeer(ctx, account.Id, peer, account.GetPeerGroupsList(peer.ID), account.Settings.Extra) @@ -689,17 +685,20 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac return nil, nil, nil, fmt.Errorf("failed to validate peer: %w", err) } - var postureChecks []*posture.Checks + postureChecks, err := am.getPeerPostureChecks(account, peer.ID) + if err != nil { + return nil, nil, nil, err + } + + if isStatusChanged || sync.UpdateAccountPeers || (updated && len(postureChecks) > 0) { + am.updateAccountPeers(ctx, account.Id) + } if peerNotValid { emptyMap := &NetworkMap{ Network: account.Network.Copy(), } - return peer, emptyMap, postureChecks, nil - } - - if isStatusChanged { - am.updateAccountPeers(ctx, account.Id) + return peer, emptyMap, []*posture.Checks{}, nil } validPeersMap, err := am.GetValidatedPeers(account) @@ -707,11 +706,6 @@ func (am *DefaultAccountManager) SyncPeer(ctx context.Context, sync PeerSync, ac return nil, nil, nil, fmt.Errorf("failed to get validated peers: %w", err) } - postureChecks, err = am.getPeerPostureChecks(account, peer.ID) - if err != nil { - return nil, nil, nil, err - } - customZone := account.GetPeersCustomZone(ctx, am.dnsDomain) return peer, account.GetPeerNetworkMap(ctx, peer.ID, customZone, validPeersMap, am.metrics.AccountManagerMetrics()), postureChecks, nil }