Compare commits

..

11 Commits

Author SHA1 Message Date
crn4
4b0f2187fd Merge branch 'main' into vk/compare-nmaps 2025-11-17 16:33:59 +01:00
crn4
8848665172 Merge branch 'fix/rfwrules' into vk/compare-nmaps 2025-11-17 16:33:11 +01:00
Misha Bragin
0d79301141 Update client login success page (#4797) 2025-11-17 15:28:20 +01:00
crn4
5603d36165 added exception on not appending route firewall rules if we have all wildcard 2025-11-17 14:25:04 +01:00
crn4
acc23f469e Merge branch 'main' into vk/compare-nmaps 2025-11-14 15:05:34 +01:00
crn4
9405c014c3 more ids 2025-11-14 14:58:40 +01:00
crn4
4536dcb2b2 exception 2025-11-14 14:37:46 +01:00
crn4
590b414ab7 save all maps 2025-11-14 13:52:53 +01:00
crn4
a45ab85178 limit differences to 5 bytes - fixed 2025-11-13 17:48:40 +01:00
crn4
c0698c8153 limit differences to 5 bytes 2025-11-13 17:19:24 +01:00
crn4
b0042c5cd0 added both maps generation and save to file 2025-11-13 16:59:45 +01:00
12 changed files with 515 additions and 288 deletions

View File

@@ -217,7 +217,7 @@ jobs:
- arch: "386"
raceFlag: ""
- arch: "amd64"
raceFlag: "-race -v"
raceFlag: "-race"
runs-on: ubuntu-22.04
steps:
- name: Install Go
@@ -258,7 +258,6 @@ jobs:
run: |
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
go test ${{ matrix.raceFlag }} \
-tags devcert \
-exec 'sudo' \
-timeout 10m ./relay/... ./shared/relay/...

View File

@@ -192,17 +192,20 @@ func (p *PKCEAuthorizationFlow) handleRequest(req *http.Request) (*oauth2.Token,
if authError := query.Get(queryError); authError != "" {
authErrorDesc := query.Get(queryErrorDesc)
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
if authErrorDesc != "" {
return nil, fmt.Errorf("authentication failed: %s", authErrorDesc)
}
return nil, fmt.Errorf("authentication failed: %s", authError)
}
// Prevent timing attacks on the state
if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
return nil, fmt.Errorf("invalid state")
return nil, fmt.Errorf("authentication failed: Invalid state")
}
code := query.Get(queryCode)
if code == "" {
return nil, fmt.Errorf("missing code")
return nil, fmt.Errorf("authentication failed: missing code")
}
return p.oAuthConfig.Exchange(
@@ -231,7 +234,7 @@ func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo,
}
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), audience); err != nil {
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
return TokenInfo{}, fmt.Errorf("authentication failed: invalid access token - %w", err)
}
email, err := parseEmailFromIDToken(tokenInfo.IDToken)

View File

@@ -253,9 +253,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
return wrapErr(err)
}
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String(), &relayClient.ManagerOpts{
MTU: engineConfig.MTU,
})
relayManager := relayClient.NewManager(engineCtx, relayURLs, myPrivateKey.PublicKey().String(), engineConfig.MTU)
c.statusRecorder.SetRelayMgr(relayManager)
if len(relayURLs) > 0 {
if token != nil {

View File

@@ -14,6 +14,7 @@ import (
"github.com/golang/mock/gomock"
"github.com/google/uuid"
"github.com/netbirdio/netbird/client/internal/stdnet"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -24,10 +25,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/management-integrations/integrations"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
@@ -229,7 +227,7 @@ func TestEngine_SSH(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), &relayClient.ManagerOpts{MTU: iface.DefaultMTU})
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(
ctx, cancel,
&signal.MockClient{},
@@ -375,7 +373,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), &relayClient.ManagerOpts{MTU: iface.DefaultMTU})
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(
ctx, cancel,
&signal.MockClient{},
@@ -602,7 +600,7 @@ func TestEngine_Sync(t *testing.T) {
}
return nil
}
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), &relayClient.ManagerOpts{MTU: iface.DefaultMTU})
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, relayMgr, &EngineConfig{
WgIfaceName: "utun103",
WgAddr: "100.64.0.1/24",
@@ -767,7 +765,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), &relayClient.ManagerOpts{MTU: iface.DefaultMTU})
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, relayMgr, &EngineConfig{
WgIfaceName: wgIfaceName,
WgAddr: wgAddr,
@@ -969,7 +967,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), &relayClient.ManagerOpts{MTU: iface.DefaultMTU})
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, relayMgr, &EngineConfig{
WgIfaceName: wgIfaceName,
WgAddr: wgAddr,
@@ -1501,7 +1499,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
MTU: iface.DefaultMTU,
}
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), &relayClient.ManagerOpts{MTU: iface.DefaultMTU})
relayMgr := relayClient.NewManager(ctx, nil, key.PublicKey().String(), iface.DefaultMTU)
e, err := NewEngine(ctx, cancel, signalClient, mgmtClient, relayMgr, conf, MobileDependency{}, peer.NewRecorder("https://mgm"), nil), nil
e.ctx = ctx
return e, err

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,299 @@
package templates
import (
"html/template"
"os"
"path/filepath"
"testing"
)
func TestPKCEAuthMsgTemplate(t *testing.T) {
tests := []struct {
name string
data map[string]string
outputFile string
expectedTitle string
expectedInContent []string
notExpectedInContent []string
}{
{
name: "error_state",
data: map[string]string{
"Error": "authentication failed: invalid state",
},
outputFile: "pkce-auth-error.html",
expectedTitle: "Login Failed",
expectedInContent: []string{
"authentication failed: invalid state",
"Login Failed",
},
notExpectedInContent: []string{
"Login Successful",
"Your device is now registered and logged in to NetBird",
},
},
{
name: "success_state",
data: map[string]string{
// No error field means success
},
outputFile: "pkce-auth-success.html",
expectedTitle: "Login Successful",
expectedInContent: []string{
"Login Successful",
"Your device is now registered and logged in to NetBird. You can now close this window.",
},
notExpectedInContent: []string{
"Login Failed",
},
},
{
name: "error_state_timeout",
data: map[string]string{
"Error": "authentication timeout: request expired after 5 minutes",
},
outputFile: "pkce-auth-timeout.html",
expectedTitle: "Login Failed",
expectedInContent: []string{
"authentication timeout: request expired after 5 minutes",
"Login Failed",
},
notExpectedInContent: []string{
"Login Successful",
"Your device is now registered and logged in to NetBird",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Parse the template
tmpl, err := template.New("pkce-auth-msg").Parse(PKCEAuthMsgTmpl)
if err != nil {
t.Fatalf("Failed to parse template: %v", err)
}
// Create temp directory for this test
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, tt.outputFile)
// Create output file
file, err := os.Create(outputPath)
if err != nil {
t.Fatalf("Failed to create output file: %v", err)
}
// Execute the template
if err := tmpl.Execute(file, tt.data); err != nil {
file.Close()
t.Fatalf("Failed to execute template: %v", err)
}
file.Close()
t.Logf("Generated test output: %s", outputPath)
// Read the generated file
content, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("Failed to read output file: %v", err)
}
contentStr := string(content)
// Verify file has content
if len(contentStr) == 0 {
t.Error("Output file is empty")
}
// Verify basic HTML structure
basicElements := []string{
"<!DOCTYPE html>",
"<html",
"<head>",
"<body>",
"NetBird",
}
for _, elem := range basicElements {
if !contains(contentStr, elem) {
t.Errorf("Expected HTML to contain '%s', but it was not found", elem)
}
}
// Verify expected title
if !contains(contentStr, tt.expectedTitle) {
t.Errorf("Expected HTML to contain title '%s', but it was not found", tt.expectedTitle)
}
// Verify expected content is present
for _, expected := range tt.expectedInContent {
if !contains(contentStr, expected) {
t.Errorf("Expected HTML to contain '%s', but it was not found", expected)
}
}
// Verify unexpected content is not present
for _, notExpected := range tt.notExpectedInContent {
if contains(contentStr, notExpected) {
t.Errorf("Expected HTML to NOT contain '%s', but it was found", notExpected)
}
}
})
}
}
func TestPKCEAuthMsgTemplateValidation(t *testing.T) {
// Test that the template can be parsed without errors
tmpl, err := template.New("pkce-auth-msg").Parse(PKCEAuthMsgTmpl)
if err != nil {
t.Fatalf("Template parsing failed: %v", err)
}
// Test with empty data
t.Run("empty_data", func(t *testing.T) {
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, "empty-data.html")
file, err := os.Create(outputPath)
if err != nil {
t.Fatalf("Failed to create output file: %v", err)
}
defer file.Close()
if err := tmpl.Execute(file, nil); err != nil {
t.Errorf("Template execution with nil data failed: %v", err)
}
})
// Test with error data
t.Run("with_error", func(t *testing.T) {
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, "with-error.html")
file, err := os.Create(outputPath)
if err != nil {
t.Fatalf("Failed to create output file: %v", err)
}
defer file.Close()
data := map[string]string{
"Error": "test error message",
}
if err := tmpl.Execute(file, data); err != nil {
t.Errorf("Template execution with error data failed: %v", err)
}
})
}
func TestPKCEAuthMsgTemplateContent(t *testing.T) {
// Test that the template contains expected elements
tmpl, err := template.New("pkce-auth-msg").Parse(PKCEAuthMsgTmpl)
if err != nil {
t.Fatalf("Template parsing failed: %v", err)
}
t.Run("success_content", func(t *testing.T) {
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, "success.html")
file, err := os.Create(outputPath)
if err != nil {
t.Fatalf("Failed to create output file: %v", err)
}
defer file.Close()
data := map[string]string{}
if err := tmpl.Execute(file, data); err != nil {
t.Fatalf("Template execution failed: %v", err)
}
// Read the file and verify it contains expected content
content, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("Failed to read output file: %v", err)
}
// Check for success indicators
contentStr := string(content)
if len(contentStr) == 0 {
t.Error("Generated HTML is empty")
}
// Basic HTML structure checks
requiredElements := []string{
"<!DOCTYPE html>",
"<html",
"<head>",
"<body>",
"Login Successful",
"NetBird",
}
for _, elem := range requiredElements {
if !contains(contentStr, elem) {
t.Errorf("Expected HTML to contain '%s', but it was not found", elem)
}
}
})
t.Run("error_content", func(t *testing.T) {
tempDir := t.TempDir()
outputPath := filepath.Join(tempDir, "error.html")
file, err := os.Create(outputPath)
if err != nil {
t.Fatalf("Failed to create output file: %v", err)
}
defer file.Close()
errorMsg := "test error message"
data := map[string]string{
"Error": errorMsg,
}
if err := tmpl.Execute(file, data); err != nil {
t.Fatalf("Template execution failed: %v", err)
}
// Read the file and verify it contains expected content
content, err := os.ReadFile(outputPath)
if err != nil {
t.Fatalf("Failed to read output file: %v", err)
}
// Check for error indicators
contentStr := string(content)
if len(contentStr) == 0 {
t.Error("Generated HTML is empty")
}
// Basic HTML structure checks
requiredElements := []string{
"<!DOCTYPE html>",
"<html",
"<head>",
"<body>",
"Login Failed",
errorMsg,
}
for _, elem := range requiredElements {
if !contains(contentStr, elem) {
t.Errorf("Expected HTML to contain '%s', but it was not found", elem)
}
}
})
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && containsHelper(s, substr)))
}
func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}

View File

@@ -2,9 +2,11 @@ package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
@@ -23,6 +25,7 @@ import (
"github.com/netbirdio/netbird/management/server/account"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/settings"
@@ -191,7 +194,7 @@ func (c *Controller) sendUpdateAccountPeers(ctx context.Context, accountID strin
var remotePeerNetworkMap *types.NetworkMap
if c.experimentalNetworkMap(accountID) {
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, p.AccountID, p.ID, approvedPeersMap, customZone, c.accountManagerMetrics)
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, p.AccountID, p.ID, approvedPeersMap, customZone, c.accountManagerMetrics, resourcePolicies, routers)
} else {
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, p.ID, customZone, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics)
}
@@ -305,7 +308,7 @@ func (c *Controller) UpdateAccountPeer(ctx context.Context, accountId string, pe
var remotePeerNetworkMap *types.NetworkMap
if c.experimentalNetworkMap(accountId) {
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peer.ID, approvedPeersMap, customZone, c.accountManagerMetrics)
remotePeerNetworkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peer.ID, approvedPeersMap, customZone, c.accountManagerMetrics, resourcePolicies, routers)
} else {
remotePeerNetworkMap = account.GetPeerNetworkMap(ctx, peerId, customZone, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics)
}
@@ -434,6 +437,8 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
log.WithContext(ctx).Debugf("getPeerPostureChecks took %s", time.Since(startPosture))
customZone := account.GetPeersCustomZone(ctx, c.GetDNSDomain(account.Settings))
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
proxyNetworkMaps, err := c.proxyController.GetProxyNetworkMaps(ctx, account.Id, peer.ID, account.Peers)
if err != nil {
@@ -444,9 +449,9 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr
var networkMap *types.NetworkMap
if c.experimentalNetworkMap(accountID) {
networkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peer.ID, approvedPeersMap, customZone, c.accountManagerMetrics)
networkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peer.ID, approvedPeersMap, customZone, c.accountManagerMetrics, resourcePolicies, routers)
} else {
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, customZone, approvedPeersMap, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), c.accountManagerMetrics)
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, customZone, approvedPeersMap, resourcePolicies, routers, c.accountManagerMetrics)
}
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]
@@ -464,6 +469,17 @@ func (c *Controller) initNetworkMapBuilderIfNeeded(account *types.Account, valid
account.InitNetworkMapBuilderIfNeeded(validatedPeers)
}
var filter = map[string]struct{}{
"cvj7n0rl0ubs73epnv00": struct{}{},
"d08cjrrl0ubs73cg7mng": struct{}{},
"d2qte4jl0ubs738s24ug": struct{}{},
"cvlo8fjl0ubs73clbhtg": struct{}{},
"d3knp53l0ubs738a3n6g": struct{}{},
"d184fkbl0ubs73f279q0": struct{}{},
"d1h9jtrl0ubs73fpjg10": struct{}{},
"d182c4bl0ubs73f26se0": struct{}{},
}
func (c *Controller) getPeerNetworkMapExp(
ctx context.Context,
accountId string,
@@ -471,6 +487,8 @@ func (c *Controller) getPeerNetworkMapExp(
validatedPeers map[string]struct{},
customZone nbdns.CustomZone,
metrics *telemetry.AccountManagerMetrics,
resourcePolicies map[string][]*types.Policy,
routers map[string]map[string]*routerTypes.NetworkRouter,
) *types.NetworkMap {
account := c.getAccountFromHolderOrInit(accountId)
if account == nil {
@@ -479,7 +497,59 @@ func (c *Controller) getPeerNetworkMapExp(
Network: &types.Network{},
}
}
return account.GetPeerNetworkMapExp(ctx, peerId, customZone, validatedPeers, metrics)
if _, ok := filter[peerId]; !ok {
return account.GetPeerNetworkMap(ctx, peerId, customZone, validatedPeers, resourcePolicies, routers, nil)
}
expMap := account.GetPeerNetworkMapExp(ctx, peerId, customZone, validatedPeers, metrics)
go func() {
legacyMap := account.GetPeerNetworkMap(ctx, peerId, customZone, validatedPeers, resourcePolicies, routers, nil)
c.compareAndSaveNetworkMaps(ctx, accountId, peerId, expMap, legacyMap)
}()
return expMap
}
func (c *Controller) compareAndSaveNetworkMaps(ctx context.Context, accountId, peerId string, expMap, legacyMap *types.NetworkMap) {
expBytes, err := json.Marshal(expMap)
if err != nil {
log.WithContext(ctx).Warnf("failed to marshal experimental network map: %v", err)
return
}
legacyBytes, err := json.Marshal(legacyMap)
if err != nil {
log.WithContext(ctx).Warnf("failed to marshal legacy network map: %v", err)
return
}
// if len(expBytes) == len(legacyBytes) || math.Abs(float64(len(expBytes)-len(legacyBytes))) < 5 {
// log.WithContext(ctx).Debugf("network maps are equal for peer %s in account %s (size: %d bytes)", peerId, accountId, len(expBytes))
// return
// }
timestamp := time.Now().UnixMicro()
baseDir := filepath.Join("debug_networkmaps", accountId, peerId)
if err := os.MkdirAll(baseDir, 0o755); err != nil {
log.WithContext(ctx).Warnf("failed to create debug directory %s: %v", baseDir, err)
return
}
expFile := filepath.Join(baseDir, fmt.Sprintf("exp_networkmap_%d_%d.json", expMap.Network.Serial, timestamp))
if err := os.WriteFile(expFile, expBytes, 0o644); err != nil {
log.WithContext(ctx).Warnf("failed to write experimental network map to %s: %v", expFile, err)
return
}
legacyFile := filepath.Join(baseDir, fmt.Sprintf("legacy_networkmap_%d_%d.json", legacyMap.Network.Serial, timestamp))
if err := os.WriteFile(legacyFile, legacyBytes, 0o644); err != nil {
log.WithContext(ctx).Warnf("failed to write legacy network map to %s: %v", legacyFile, err)
return
}
// log.WithContext(ctx).Infof("network maps differ for peer %s in account %s - saved to %s (exp: %d bytes, legacy: %d bytes)", peerId, accountId, baseDir, len(expBytes), len(legacyBytes))
}
func (c *Controller) onPeerAddedUpdNetworkMapCache(account *types.Account, peerId string) error {
@@ -752,6 +822,8 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N
return nil, err
}
customZone := account.GetPeersCustomZone(ctx, c.GetDNSDomain(account.Settings))
resourcePolicies := account.GetResourcePoliciesMap()
routers := account.GetResourceRoutersMap()
proxyNetworkMaps, err := c.proxyController.GetProxyNetworkMaps(ctx, account.Id, peerID, account.Peers)
if err != nil {
@@ -762,9 +834,9 @@ func (c *Controller) GetNetworkMap(ctx context.Context, peerID string) (*types.N
var networkMap *types.NetworkMap
if c.experimentalNetworkMap(peer.AccountID) {
networkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peerID, validatedPeers, customZone, nil)
networkMap = c.getPeerNetworkMapExp(ctx, peer.AccountID, peerID, validatedPeers, customZone, nil, resourcePolicies, routers)
} else {
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, customZone, validatedPeers, account.GetResourcePoliciesMap(), account.GetResourceRoutersMap(), nil)
networkMap = account.GetPeerNetworkMap(ctx, peer.ID, customZone, validatedPeers, resourcePolicies, routers, nil)
}
proxyNetworkMap, ok := proxyNetworkMaps[peer.ID]

View File

@@ -22,10 +22,11 @@ import (
)
const (
allPeers = "0.0.0.0"
fw = "fw:"
rfw = "route-fw:"
nr = "network-resource-"
allPeers = "0.0.0.0"
allWildcard = "0.0.0.0/0"
v6AllWildcard = "::/0"
fw = "fw:"
rfw = "route-fw:"
)
type NetworkMapCache struct {
@@ -1640,6 +1641,10 @@ func (b *NetworkMapBuilder) updateRouteFirewallRules(routesView *PeerRoutesView,
}
if string(rule.RouteID) == update.RuleID {
if hasWildcard := slices.Contains(rule.SourceRanges, allWildcard) || slices.Contains(rule.SourceRanges, v6AllWildcard); hasWildcard {
break
}
sourceIP := update.AddSourceIP
if strings.Contains(sourceIP, ":") {

View File

@@ -132,7 +132,6 @@ func (r *Relay) Accept(conn net.Conn) {
storeTime := time.Now()
if isReconnection := r.store.AddPeer(peer); isReconnection {
r.metrics.RecordPeerReconnection()
r.notifier.PeerWentOffline(peer.ID())
}
r.notifier.PeerCameOnline(peer.ID())

View File

@@ -11,11 +11,11 @@ import (
"go.opentelemetry.io/otel"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/relay/server"
"github.com/netbirdio/netbird/shared/relay/auth/allow"
"github.com/netbirdio/netbird/shared/relay/auth/hmac"
"github.com/netbirdio/netbird/shared/relay/messages"
"github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/relay/server"
)
var (
@@ -312,7 +312,7 @@ func TestBindToUnavailabePeer(t *testing.T) {
clientAlice := NewClient(serverURL, hmacTokenStore, "alice", iface.DefaultMTU)
err = clientAlice.Connect(ctx)
if err != nil {
t.Fatalf("failed to connect to server: %s", err)
t.Errorf("failed to connect to server: %s", err)
}
_, err = clientAlice.OpenConn(ctx, "bob")
if err == nil {
@@ -364,7 +364,7 @@ func TestBindReconnect(t *testing.T) {
clientBob := NewClient(serverURL, hmacTokenStore, "bob", iface.DefaultMTU)
err = clientBob.Connect(ctx)
if err != nil {
t.Fatalf("failed to connect to server: %s", err)
t.Errorf("failed to connect to server: %s", err)
}
_, err = clientAlice.OpenConn(ctx, "bob")
@@ -374,7 +374,7 @@ func TestBindReconnect(t *testing.T) {
chBob, err := clientBob.OpenConn(ctx, "alice")
if err != nil {
t.Fatalf("failed to bind channel: %s", err)
t.Errorf("failed to bind channel: %s", err)
}
log.Infof("closing client Alice")
@@ -386,12 +386,12 @@ func TestBindReconnect(t *testing.T) {
clientAlice = NewClient(serverURL, hmacTokenStore, "alice", iface.DefaultMTU)
err = clientAlice.Connect(ctx)
if err != nil {
t.Fatalf("failed to connect to server: %s", err)
t.Errorf("failed to connect to server: %s", err)
}
chAlice, err := clientAlice.OpenConn(ctx, "bob")
if err != nil {
t.Fatalf("failed to bind channel: %s", err)
t.Errorf("failed to bind channel: %s", err)
}
testString := "hello alice, I am bob"
@@ -402,7 +402,7 @@ func TestBindReconnect(t *testing.T) {
chBob, err = clientBob.OpenConn(ctx, "alice")
if err != nil {
t.Fatalf("failed to bind channel: %s", err)
t.Errorf("failed to bind channel: %s", err)
}
_, err = chBob.Write([]byte(testString))
@@ -427,105 +427,6 @@ func TestBindReconnect(t *testing.T) {
}
}
func TestBindReconnectRace(t *testing.T) {
ctx := context.Background()
srvCfg := server.ListenerConfig{Address: serverListenAddr}
srv, err := server.NewServer(serverCfg)
if err != nil {
t.Fatalf("failed to create server: %s", err)
}
errChan := make(chan error, 1)
go func() {
err := srv.Listen(srvCfg)
if err != nil {
errChan <- err
}
}()
defer func() {
log.Infof("closing server")
err := srv.Shutdown(ctx)
if err != nil {
t.Errorf("failed to close server: %s", err)
}
}()
// wait for servers to start
if err := waitForServerToStart(errChan); err != nil {
t.Fatalf("failed to start server: %s", err)
}
clientBob := NewClient(serverURL, hmacTokenStore, "bob", iface.DefaultMTU)
err = clientBob.Connect(ctx)
if err != nil {
t.Fatalf("failed to connect to server: %s", err)
}
defer clientBob.Close()
// Run the reconnection scenario multiple times to expose the race
failures := 0
iterations := 1000
for i := 0; i < iterations; i++ {
log.Infof("Iteration %d/%d", i+1, iterations)
// Alice connects
clientAlice := NewClient(serverURL, hmacTokenStore, "alice", iface.DefaultMTU)
err = clientAlice.Connect(ctx)
if err != nil {
t.Fatalf("iteration %d: failed to connect alice: %s", i, err)
}
// Bob opens connection to Alice
_, err = clientBob.OpenConn(ctx, "alice")
if err != nil {
t.Fatalf("iteration %d: failed to open conn from bob: %s", i, err)
}
// Close Alice immediately
err = clientAlice.Close()
if err != nil {
t.Errorf("iteration %d: failed to close alice: %s", i, err)
}
// Reconnect Alice immediately (this is where the race occurs)
clientAlice = NewClient(serverURL, hmacTokenStore, "alice", iface.DefaultMTU)
err = clientAlice.Connect(ctx)
if err != nil {
t.Fatalf("iteration %d: failed to reconnect alice: %s", i, err)
}
// Bob tries to open a new connection to the reconnected Alice
// Without the fix, this will sometimes fail with "connection already exists"
// because Bob still has the old connection in its map
_, err = clientBob.OpenConn(ctx, "alice")
if err != nil {
log.Errorf("iteration %d: RACE DETECTED - failed to open new conn after reconnect: %s", i, err)
failures++
}
// Clean up
clientAlice.Close()
// Close Bob's connection to Alice to prepare for next iteration
clientBob.mu.Lock()
aliceID := messages.HashID("alice")
if container, ok := clientBob.conns[aliceID]; ok {
container.close()
delete(clientBob.conns, aliceID)
}
clientBob.mu.Unlock()
}
if failures > 0 {
t.Errorf("Race condition detected in %d out of %d iterations (%.1f%%)",
failures, iterations, float64(failures)/float64(iterations)*100)
} else {
log.Infof("No race detected in %d iterations (fix is working or race didn't trigger)", iterations)
}
}
func TestCloseConn(t *testing.T) {
ctx := context.Background()
@@ -558,18 +459,18 @@ func TestCloseConn(t *testing.T) {
bob := NewClient(serverURL, hmacTokenStore, "bob", iface.DefaultMTU)
err = bob.Connect(ctx)
if err != nil {
t.Fatalf("failed to connect to server: %s", err)
t.Errorf("failed to connect to server: %s", err)
}
clientAlice := NewClient(serverURL, hmacTokenStore, "alice", iface.DefaultMTU)
err = clientAlice.Connect(ctx)
if err != nil {
t.Fatalf("failed to connect to server: %s", err)
t.Errorf("failed to connect to server: %s", err)
}
conn, err := clientAlice.OpenConn(ctx, "bob")
if err != nil {
t.Fatalf("failed to bind channel: %s", err)
t.Errorf("failed to bind channel: %s", err)
}
log.Infof("closing connection")
@@ -631,7 +532,7 @@ func TestCloseRelayConn(t *testing.T) {
conn, err := clientAlice.OpenConn(ctx, "bob")
if err != nil {
t.Fatalf("failed to bind channel: %s", err)
t.Errorf("failed to bind channel: %s", err)
}
_ = clientAlice.relayConn.Close()

View File

@@ -14,15 +14,10 @@ import (
relayAuth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
)
const (
defaultRelayCleanupInterval = 60 * time.Second
defaultKeepUnusedServerTime = 5 * time.Second
defaultMTU = 1280
minMTU = 1280
maxMTU = 65535
)
var (
relayCleanupInterval = 60 * time.Second
keepUnusedServerTime = 5 * time.Second
ErrRelayClientNotConnected = fmt.Errorf("relay client not connected")
)
@@ -69,55 +64,14 @@ type Manager struct {
onReconnectedListenerFn func()
listenerLock sync.Mutex
cleanupInterval time.Duration
unusedServerTime time.Duration
mtu uint16
}
// ManagerOpts contains optional configuration for Manager
type ManagerOpts struct {
// CleanupInterval is the interval for cleaning up unused relay connections.
// If zero, defaults to defaultRelayCleanupInterval.
CleanupInterval time.Duration
// UnusedServerTime is the time to wait before closing unused relay connections.
// If zero, defaults to defaultKeepUnusedServerTime.
UnusedServerTime time.Duration
// MTU is the maximum transmission unit for relay connections.
// If zero, defaults to defaultMTU (1280).
// Must be between minMTU (1280) and maxMTU (65535).
MTU uint16
mtu uint16
}
// NewManager creates a new manager instance.
// The serverURL address can be empty. In this case, the manager will not serve.
// Optional parameters can be configured using ManagerOpts. Pass nil to use default values.
func NewManager(ctx context.Context, serverURLs []string, peerID string, opts *ManagerOpts) *Manager {
func NewManager(ctx context.Context, serverURLs []string, peerID string, mtu uint16) *Manager {
tokenStore := &relayAuth.TokenStore{}
cleanupInterval := defaultRelayCleanupInterval
unusedServerTime := defaultKeepUnusedServerTime
mtu := uint16(defaultMTU)
if opts != nil {
if opts.CleanupInterval > 0 {
cleanupInterval = opts.CleanupInterval
}
if opts.UnusedServerTime > 0 {
unusedServerTime = opts.UnusedServerTime
}
if opts.MTU > 0 {
if opts.MTU < minMTU {
log.Warnf("MTU %d is below minimum %d, using minimum", opts.MTU, minMTU)
mtu = minMTU
} else if opts.MTU > maxMTU {
log.Warnf("MTU %d exceeds maximum %d, using maximum", opts.MTU, maxMTU)
mtu = maxMTU
} else {
mtu = opts.MTU
}
}
}
m := &Manager{
ctx: ctx,
peerID: peerID,
@@ -131,8 +85,6 @@ func NewManager(ctx context.Context, serverURLs []string, peerID string, opts *M
},
relayClients: make(map[string]*RelayTrack),
onDisconnectedListeners: make(map[string]*list.List),
cleanupInterval: cleanupInterval,
unusedServerTime: unusedServerTime,
}
m.serverPicker.ServerURLs.Store(serverURLs)
m.reconnectGuard = NewGuard(m.serverPicker)
@@ -382,7 +334,7 @@ func (m *Manager) isForeignServer(address string) (bool, error) {
}
func (m *Manager) startCleanupLoop() {
ticker := time.NewTicker(m.cleanupInterval)
ticker := time.NewTicker(relayCleanupInterval)
defer ticker.Stop()
for {
select {
@@ -407,7 +359,7 @@ func (m *Manager) cleanUpUnusedRelays() {
continue
}
if time.Since(rt.created) <= m.unusedServerTime {
if time.Since(rt.created) <= keepUnusedServerTime {
rt.Unlock()
continue
}

View File

@@ -16,7 +16,7 @@ import (
func TestEmptyURL(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mgr := NewManager(ctx, nil, "alice", &ManagerOpts{MTU: iface.DefaultMTU})
mgr := NewManager(ctx, nil, "alice", iface.DefaultMTU)
err := mgr.Serve()
if err == nil {
t.Errorf("expected error, got nil")
@@ -91,12 +91,12 @@ func TestForeignConn(t *testing.T) {
mCtx, cancel := context.WithCancel(ctx)
defer cancel()
clientAlice := NewManager(mCtx, toURL(lstCfg1), "alice", &ManagerOpts{MTU: iface.DefaultMTU})
clientAlice := NewManager(mCtx, toURL(lstCfg1), "alice", iface.DefaultMTU)
if err := clientAlice.Serve(); err != nil {
t.Fatalf("failed to serve manager: %s", err)
}
clientBob := NewManager(mCtx, toURL(srvCfg2), "bob", &ManagerOpts{MTU: iface.DefaultMTU})
clientBob := NewManager(mCtx, toURL(srvCfg2), "bob", iface.DefaultMTU)
if err := clientBob.Serve(); err != nil {
t.Fatalf("failed to serve manager: %s", err)
}
@@ -198,12 +198,12 @@ func TestForeginConnClose(t *testing.T) {
mCtx, cancel := context.WithCancel(ctx)
defer cancel()
mgrBob := NewManager(mCtx, toURL(srvCfg2), "bob", &ManagerOpts{MTU: iface.DefaultMTU})
mgrBob := NewManager(mCtx, toURL(srvCfg2), "bob", iface.DefaultMTU)
if err := mgrBob.Serve(); err != nil {
t.Fatalf("failed to serve manager: %s", err)
}
mgr := NewManager(mCtx, toURL(srvCfg1), "alice", &ManagerOpts{MTU: iface.DefaultMTU})
mgr := NewManager(mCtx, toURL(srvCfg1), "alice", iface.DefaultMTU)
err = mgr.Serve()
if err != nil {
t.Fatalf("failed to serve manager: %s", err)
@@ -221,8 +221,8 @@ func TestForeginConnClose(t *testing.T) {
func TestForeignAutoClose(t *testing.T) {
ctx := context.Background()
testCleanupInterval := 1 * time.Second
testUnusedServerTime := 2 * time.Second
relayCleanupInterval = 1 * time.Second
keepUnusedServerTime = 2 * time.Second
srvCfg1 := server.ListenerConfig{
Address: "localhost:1234",
@@ -283,11 +283,7 @@ func TestForeignAutoClose(t *testing.T) {
t.Log("connect to server 1.")
mCtx, cancel := context.WithCancel(ctx)
defer cancel()
mgr := NewManager(mCtx, toURL(srvCfg1), idAlice, &ManagerOpts{
MTU: iface.DefaultMTU,
CleanupInterval: testCleanupInterval,
UnusedServerTime: testUnusedServerTime,
})
mgr := NewManager(mCtx, toURL(srvCfg1), idAlice, iface.DefaultMTU)
err = mgr.Serve()
if err != nil {
t.Fatalf("failed to serve manager: %s", err)
@@ -314,7 +310,7 @@ func TestForeignAutoClose(t *testing.T) {
}
// Wait for cleanup to happen
timeout := testCleanupInterval + testUnusedServerTime + 2*time.Second
timeout := relayCleanupInterval + keepUnusedServerTime + 2*time.Second
t.Logf("waiting for relay cleanup: %s", timeout)
select {
@@ -358,13 +354,13 @@ func TestAutoReconnect(t *testing.T) {
mCtx, cancel := context.WithCancel(ctx)
defer cancel()
clientBob := NewManager(mCtx, toURL(srvCfg), "bob", &ManagerOpts{MTU: iface.DefaultMTU})
clientBob := NewManager(mCtx, toURL(srvCfg), "bob", iface.DefaultMTU)
err = clientBob.Serve()
if err != nil {
t.Fatalf("failed to serve manager: %s", err)
}
clientAlice := NewManager(mCtx, toURL(srvCfg), "alice", &ManagerOpts{MTU: iface.DefaultMTU})
clientAlice := NewManager(mCtx, toURL(srvCfg), "alice", iface.DefaultMTU)
err = clientAlice.Serve()
if err != nil {
t.Fatalf("failed to serve manager: %s", err)
@@ -433,12 +429,12 @@ func TestNotifierDoubleAdd(t *testing.T) {
mCtx, cancel := context.WithCancel(ctx)
defer cancel()
clientBob := NewManager(mCtx, toURL(listenerCfg1), "bob", &ManagerOpts{MTU: iface.DefaultMTU})
clientBob := NewManager(mCtx, toURL(listenerCfg1), "bob", iface.DefaultMTU)
if err = clientBob.Serve(); err != nil {
t.Fatalf("failed to serve manager: %s", err)
}
clientAlice := NewManager(mCtx, toURL(listenerCfg1), "alice", &ManagerOpts{MTU: iface.DefaultMTU})
clientAlice := NewManager(mCtx, toURL(listenerCfg1), "alice", iface.DefaultMTU)
if err = clientAlice.Serve(); err != nil {
t.Fatalf("failed to serve manager: %s", err)
}