Compare commits

..

6 Commits

Author SHA1 Message Date
riccardom
a23722f459 Adds tests for IsDevelopmentVersion 2026-05-26 09:27:58 +02:00
riccardom
496c459870 Remove unexistent tests on wire format
The sha / dirty flag are added only when the CLI asks the version.
Account versions is unaffacted and can only strictly match "development"
2026-05-26 09:26:02 +02:00
riccardom
0546c55b1a Drop synthetic "dev"/"0.50.0-dev" firewall feature-gate fixtures
These test cases encoded the loose strings.Contains(v, "dev")
semantics inherited from peerSupportedFirewallFeatures, but
NetbirdVersion() never produces those values — only the literal
"development" (and now "development-<sha>[-dirty]") ever flows
through the wire. The agent owns the semantics of an ephemeral
development build, so the tests should exercise the strings we
actually emit.

Replaced with development, development-<sha> and
development-<sha>-dirty cases that match the HasPrefix("development")
predicate introduced upstream.
2026-05-26 09:21:27 +02:00
riccardom
c820a3a7f3 Adjust for "v0.31.1-dev" test case
which must be considered pre-release
2026-05-25 22:49:48 +02:00
riccardom
461f1cd96a Adds commit sha to development version for cobra command only
Leave dashboard unaffected
2026-05-25 22:44:53 +02:00
riccardom
67e4a13713 Refactor to use a common checker for development version 2026-05-25 22:37:44 +02:00
42 changed files with 249 additions and 688 deletions

View File

@@ -11,7 +11,7 @@ import (
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"google.golang.org/grpc" "google.golang.org/grpc"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator" "github.com/netbirdio/management-integrations/integrations"
nbcache "github.com/netbirdio/netbird/management/server/cache" nbcache "github.com/netbirdio/netbird/management/server/cache"
@@ -109,7 +109,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
t.Fatal(err) t.Fatal(err)
} }
iv, _ := validator.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore) iv, _ := integrations.NewIntegratedValidator(ctx, peersmanager, settingsManagerMock, eventStore, cacheStore)
metrics, err := telemetry.NewDefaultAppMetrics(ctx) metrics, err := telemetry.NewDefaultAppMetrics(ctx)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -12,7 +12,13 @@ var (
Short: "Print the NetBird's client application version", Short: "Print the NetBird's client application version",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
cmd.SetOut(cmd.OutOrStdout()) cmd.SetOut(cmd.OutOrStdout())
cmd.Println(version.NetbirdVersion()) out := version.NetbirdVersion()
if version.IsDevelopmentVersion(out) {
if commit := version.NetbirdCommit(); commit != "" {
out += "-" + commit
}
}
cmd.Println(out)
}, },
} }
) )

View File

@@ -12,7 +12,6 @@ import (
"sync" "sync"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
wgdevice "golang.zx2c4.com/wireguard/device"
wgnetstack "golang.zx2c4.com/wireguard/tun/netstack" wgnetstack "golang.zx2c4.com/wireguard/tun/netstack"
"github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface"
@@ -101,26 +100,6 @@ type Options struct {
MTU *uint16 MTU *uint16
// DNSLabels defines additional DNS labels configured in the peer. // DNSLabels defines additional DNS labels configured in the peer.
DNSLabels []string DNSLabels []string
// Performance configures the tunnel's buffer pool cap and batch size.
Performance Performance
}
// Performance configures the embedded client's tunnel memory/throughput knobs.
//
// These settings are process-global: any non-nil field also becomes the
// default for Clients constructed by later embed.New calls in the same
// process. Nil fields are ignored.
type Performance struct {
// PreallocatedBuffersPerPool caps the per-tunnel buffer pool. Zero
// leaves the pool unbounded. Lower values trade throughput for a
// tighter memory ceiling. May also be changed on a running Client via
// Client.SetPerformance, provided this field was nonzero at construction.
PreallocatedBuffersPerPool *uint32
// MaxBatchSize overrides the number of packets the tunnel reads or
// writes per syscall, which also bounds eager buffer allocation per
// worker. Zero uses the platform default. Applied at construction
// only; ignored by Client.SetPerformance.
MaxBatchSize *uint32
} }
// validateCredentials checks that exactly one credential type is provided // validateCredentials checks that exactly one credential type is provided
@@ -220,13 +199,6 @@ func New(opts Options) (*Client, error) {
config.PrivateKey = opts.PrivateKey config.PrivateKey = opts.PrivateKey
} }
if opts.Performance.PreallocatedBuffersPerPool != nil {
wgdevice.SetPreallocatedBuffersPerPool(*opts.Performance.PreallocatedBuffersPerPool)
}
if opts.Performance.MaxBatchSize != nil {
wgdevice.SetMaxBatchSizeOverride(*opts.Performance.MaxBatchSize)
}
return &Client{ return &Client{
deviceName: opts.DeviceName, deviceName: opts.DeviceName,
setupKey: opts.SetupKey, setupKey: opts.SetupKey,
@@ -523,25 +495,6 @@ func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
return sshcommon.VerifyHostKey(storedKey, key, peerAddress) return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
} }
// SetPerformance retunes a running Client. Only PreallocatedBuffersPerPool
// takes effect, and only when it was nonzero at construction;
// MaxBatchSize is construction-only and returns an error if set here.
//
// Returns ErrClientNotStarted / ErrEngineNotStarted if the Client is not
// running yet.
func (c *Client) SetPerformance(t Performance) error {
if t.MaxBatchSize != nil {
return errors.New("MaxBatchSize is construction-only and cannot be changed at runtime")
}
engine, err := c.getEngine()
if err != nil {
return err
}
return engine.SetPerformance(internal.Performance{
PreallocatedBuffersPerPool: t.PreallocatedBuffersPerPool,
})
}
// StartCapture begins capturing packets on this client's tunnel device. // StartCapture begins capturing packets on this client's tunnel device.
// Only one capture can be active at a time; starting a new one stops the previous. // Only one capture can be active at a time; starting a new one stops the previous.
// Call StopCapture (or CaptureSession.Stop) to end it. // Call StopCapture (or CaptureSession.Stop) to end it.

View File

@@ -1967,29 +1967,6 @@ func (e *Engine) GetClientMetrics() *metrics.ClientMetrics {
return e.clientMetrics return e.clientMetrics
} }
// Performance bundles runtime-adjustable tunnel pool knobs.
// See Engine.SetPerformance. Nil fields are ignored.
type Performance struct {
PreallocatedBuffersPerPool *uint32
}
// SetPerformance applies the given tuning to this engine's live Device.
func (e *Engine) SetPerformance(t Performance) error {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
if e.wgInterface == nil {
return fmt.Errorf("wg interface not initialized")
}
dev := e.wgInterface.GetWGDevice()
if dev == nil {
return fmt.Errorf("wg device not initialized")
}
if t.PreallocatedBuffersPerPool != nil {
dev.SetPreallocatedBuffersPerPool(*t.PreallocatedBuffersPerPool)
}
return nil
}
func findIPFromInterfaceName(ifaceName string) (net.IP, error) { func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
iface, err := net.InterfaceByName(ifaceName) iface, err := net.InterfaceByName(ifaceName)
if err != nil { if err != nil {

View File

@@ -27,7 +27,7 @@ import (
"github.com/netbirdio/netbird/client/internal/stdnet" "github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/management/server/job" "github.com/netbirdio/netbird/management/server/job"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator" "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/controller"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel" "github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
@@ -66,8 +66,8 @@ import (
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
mgmt "github.com/netbirdio/netbird/shared/management/client" mgmt "github.com/netbirdio/netbird/shared/management/client"
mgmtProto "github.com/netbirdio/netbird/shared/management/proto" mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/netiputil"
relayClient "github.com/netbirdio/netbird/shared/relay/client" relayClient "github.com/netbirdio/netbird/shared/relay/client"
"github.com/netbirdio/netbird/shared/netiputil"
signal "github.com/netbirdio/netbird/shared/signal/client" signal "github.com/netbirdio/netbird/shared/signal/client"
"github.com/netbirdio/netbird/shared/signal/proto" "github.com/netbirdio/netbird/shared/signal/proto"
signalServer "github.com/netbirdio/netbird/signal/server" signalServer "github.com/netbirdio/netbird/signal/server"
@@ -1641,7 +1641,7 @@ func startManagement(t *testing.T, dataDir, testFile string) (*grpc.Server, stri
return nil, "", err return nil, "", err
} }
ia, _ := validator.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore) ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, nil, eventStore, cacheStore)
metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
require.NoError(t, err) require.NoError(t, err)

View File

@@ -4,6 +4,8 @@ import (
"strings" "strings"
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
nbversion "github.com/netbirdio/netbird/version"
) )
var ( var (
@@ -11,7 +13,7 @@ var (
) )
func IsSupported(agentVersion string) bool { func IsSupported(agentVersion string) bool {
if agentVersion == "development" { if nbversion.IsDevelopmentVersion(agentVersion) {
return true return true
} }

View File

@@ -19,8 +19,6 @@ import (
const ( const (
latestVersion = "latest" latestVersion = "latest"
// this version will be ignored
developmentVersion = "development"
) )
var errNoUpdateState = errors.New("no update state found") var errNoUpdateState = errors.New("no update state found")
@@ -483,7 +481,7 @@ func (m *Manager) loadAndDeleteUpdateState(ctx context.Context) (*UpdateState, e
} }
func (m *Manager) shouldUpdate(updateVersion *v.Version, forceUpdate bool) bool { func (m *Manager) shouldUpdate(updateVersion *v.Version, forceUpdate bool) bool {
if m.currentVersion == developmentVersion { if version.IsDevelopmentVersion(m.currentVersion) {
log.Debugf("skipping auto-update, running development version") log.Debugf("skipping auto-update, running development version")
return false return false
} }

View File

@@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator" "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/controller"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel" "github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
@@ -315,7 +315,7 @@ func startManagement(t *testing.T, signalAddr string, counter *int) (*grpc.Serve
return nil, "", err return nil, "", err
} }
ia, _ := validator.NewIntegratedValidator(context.Background(), peersManager, settingsManagerMock, eventStore, cacheStore) ia, _ := integrations.NewIntegratedValidator(context.Background(), peersManager, settingsManagerMock, eventStore, cacheStore)
metrics, err := telemetry.NewDefaultAppMetrics(context.Background()) metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
require.NoError(t, err) require.NoError(t, err)

View File

@@ -6,7 +6,6 @@ import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"sync"
"syscall/js" "syscall/js"
"time" "time"
@@ -14,7 +13,7 @@ import (
) )
const ( const (
certValidationTimeout = 5 * time.Minute certValidationTimeout = 60 * time.Second
) )
func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, certChain [][]byte) (bool, error) { func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, certChain [][]byte) (bool, error) {
@@ -47,31 +46,17 @@ func (p *RDCleanPathProxy) validateCertificateWithJS(conn *proxyConnection, cert
promise := conn.wsHandlers.Call("onCertificateRequest", certInfo) promise := conn.wsHandlers.Call("onCertificateRequest", certInfo)
resultChan := make(chan bool, 1) resultChan := make(chan bool)
errorChan := make(chan error, 1) errorChan := make(chan error)
// Release from inside the callbacks so a post-timeout promise resolution promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
// does not invoke an already-released func. result := args[0].Bool()
var thenFn, catchFn js.Func resultChan <- result
var releaseOnce sync.Once
release := func() {
releaseOnce.Do(func() {
thenFn.Release()
catchFn.Release()
})
}
thenFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
defer release()
resultChan <- args[0].Bool()
return nil return nil
}) })).Call("catch", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
catchFn = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
defer release()
errorChan <- fmt.Errorf("certificate validation failed") errorChan <- fmt.Errorf("certificate validation failed")
return nil return nil
}) }))
promise.Call("then", thenFn).Call("catch", catchFn)
select { select {
case result := <-resultChan: case result := <-resultChan:

View File

@@ -11,7 +11,6 @@ import (
"io" "io"
"net" "net"
"sync" "sync"
"sync/atomic"
"syscall/js" "syscall/js"
"time" "time"
@@ -58,8 +57,6 @@ type RDCleanPathProxy struct {
} }
activeConnections map[string]*proxyConnection activeConnections map[string]*proxyConnection
destinations map[string]string destinations map[string]string
pendingHandlers map[string]js.Func
nextID atomic.Uint64
mu sync.Mutex mu sync.Mutex
} }
@@ -69,15 +66,8 @@ type proxyConnection struct {
rdpConn net.Conn rdpConn net.Conn
tlsConn *tls.Conn tlsConn *tls.Conn
wsHandlers js.Value wsHandlers js.Value
// Go-side callbacks exposed to JS. js.FuncOf pins the Go closure in a ctx context.Context
// global handle map and MUST be released, otherwise every connection cancel context.CancelFunc
// leaks the Go memory the closure captures.
wsHandlerFn js.Func
onMessageFn js.Func
onCloseFn js.Func
cleanupOnce sync.Once
ctx context.Context
cancel context.CancelFunc
} }
// NewRDCleanPathProxy creates a new RDCleanPath proxy // NewRDCleanPathProxy creates a new RDCleanPath proxy
@@ -90,11 +80,7 @@ func NewRDCleanPathProxy(client interface {
} }
} }
// CreateProxy creates a new proxy endpoint for the given destination. // CreateProxy creates a new proxy endpoint for the given destination
// The registered handler fn and its destinations/pendingHandlers entries are
// only released once a connection is established and cleanupConnection runs.
// If a caller invokes CreateProxy but never connects to the returned URL,
// those entries stay pinned for the lifetime of the page.
func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value { func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
destination := net.JoinHostPort(hostname, port) destination := net.JoinHostPort(hostname, port)
@@ -102,7 +88,7 @@ func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
resolve := args[0] resolve := args[0]
go func() { go func() {
proxyID := fmt.Sprintf("proxy_%d", p.nextID.Add(1)) proxyID := fmt.Sprintf("proxy_%d", len(p.activeConnections))
p.mu.Lock() p.mu.Lock()
if p.destinations == nil { if p.destinations == nil {
@@ -114,7 +100,7 @@ func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
proxyURL := fmt.Sprintf("%s://%s/%s", RDCleanPathProxyScheme, RDCleanPathProxyHost, proxyID) proxyURL := fmt.Sprintf("%s://%s/%s", RDCleanPathProxyScheme, RDCleanPathProxyHost, proxyID)
// Register the WebSocket handler for this specific proxy // Register the WebSocket handler for this specific proxy
handlerFn := js.FuncOf(func(_ js.Value, args []js.Value) any { js.Global().Set(fmt.Sprintf("handleRDCleanPathWebSocket_%s", proxyID), js.FuncOf(func(_ js.Value, args []js.Value) any {
if len(args) < 1 { if len(args) < 1 {
return js.ValueOf("error: requires WebSocket argument") return js.ValueOf("error: requires WebSocket argument")
} }
@@ -122,14 +108,7 @@ func (p *RDCleanPathProxy) CreateProxy(hostname, port string) js.Value {
ws := args[0] ws := args[0]
p.HandleWebSocketConnection(ws, proxyID) p.HandleWebSocketConnection(ws, proxyID)
return nil return nil
}) }))
p.mu.Lock()
if p.pendingHandlers == nil {
p.pendingHandlers = make(map[string]js.Func)
}
p.pendingHandlers[proxyID] = handlerFn
p.mu.Unlock()
js.Global().Set(fmt.Sprintf("handleRDCleanPathWebSocket_%s", proxyID), handlerFn)
log.Infof("Created RDCleanPath proxy endpoint: %s for destination: %s", proxyURL, destination) log.Infof("Created RDCleanPath proxy endpoint: %s for destination: %s", proxyURL, destination)
resolve.Invoke(proxyURL) resolve.Invoke(proxyURL)
@@ -163,10 +142,6 @@ func (p *RDCleanPathProxy) HandleWebSocketConnection(ws js.Value, proxyID string
p.mu.Lock() p.mu.Lock()
p.activeConnections[proxyID] = conn p.activeConnections[proxyID] = conn
if fn, ok := p.pendingHandlers[proxyID]; ok {
conn.wsHandlerFn = fn
delete(p.pendingHandlers, proxyID)
}
p.mu.Unlock() p.mu.Unlock()
p.setupWebSocketHandlers(ws, conn) p.setupWebSocketHandlers(ws, conn)
@@ -175,7 +150,7 @@ func (p *RDCleanPathProxy) HandleWebSocketConnection(ws js.Value, proxyID string
} }
func (p *RDCleanPathProxy) setupWebSocketHandlers(ws js.Value, conn *proxyConnection) { func (p *RDCleanPathProxy) setupWebSocketHandlers(ws js.Value, conn *proxyConnection) {
conn.onMessageFn = js.FuncOf(func(this js.Value, args []js.Value) any { ws.Set("onGoMessage", js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) < 1 { if len(args) < 1 {
return nil return nil
} }
@@ -183,15 +158,13 @@ func (p *RDCleanPathProxy) setupWebSocketHandlers(ws js.Value, conn *proxyConnec
data := args[0] data := args[0]
go p.handleWebSocketMessage(conn, data) go p.handleWebSocketMessage(conn, data)
return nil return nil
}) }))
ws.Set("onGoMessage", conn.onMessageFn)
conn.onCloseFn = js.FuncOf(func(_ js.Value, args []js.Value) any { ws.Set("onGoClose", js.FuncOf(func(_ js.Value, args []js.Value) any {
log.Debug("WebSocket closed by JavaScript") log.Debug("WebSocket closed by JavaScript")
conn.cancel() conn.cancel()
return nil return nil
}) }))
ws.Set("onGoClose", conn.onCloseFn)
} }
func (p *RDCleanPathProxy) handleWebSocketMessage(conn *proxyConnection, data js.Value) { func (p *RDCleanPathProxy) handleWebSocketMessage(conn *proxyConnection, data js.Value) {
@@ -288,49 +261,25 @@ func (p *RDCleanPathProxy) handleDirectRDP(conn *proxyConnection, firstPacket []
} }
func (p *RDCleanPathProxy) cleanupConnection(conn *proxyConnection) { func (p *RDCleanPathProxy) cleanupConnection(conn *proxyConnection) {
conn.cleanupOnce.Do(func() { log.Debugf("Cleaning up connection %s", conn.id)
log.Debugf("Cleaning up connection %s", conn.id) conn.cancel()
conn.cancel() if conn.tlsConn != nil {
if conn.tlsConn != nil { log.Debug("Closing TLS connection")
log.Debug("Closing TLS connection") if err := conn.tlsConn.Close(); err != nil {
if err := conn.tlsConn.Close(); err != nil { log.Debugf("Error closing TLS connection: %v", err)
log.Debugf("Error closing TLS connection: %v", err)
}
conn.tlsConn = nil
} }
if conn.rdpConn != nil { conn.tlsConn = nil
log.Debug("Closing TCP connection") }
if err := conn.rdpConn.Close(); err != nil { if conn.rdpConn != nil {
log.Debugf("Error closing TCP connection: %v", err) log.Debug("Closing TCP connection")
} if err := conn.rdpConn.Close(); err != nil {
conn.rdpConn = nil log.Debugf("Error closing TCP connection: %v", err)
} }
js.Global().Delete(fmt.Sprintf("handleRDCleanPathWebSocket_%s", conn.id)) conn.rdpConn = nil
}
// Detach before releasing so late JS calls surface as TypeError instead p.mu.Lock()
// of silent "call to released function". delete(p.activeConnections, conn.id)
if conn.wsHandlers.Truthy() { p.mu.Unlock()
conn.wsHandlers.Set("onGoMessage", js.Undefined())
conn.wsHandlers.Set("onGoClose", js.Undefined())
}
// wsHandlerFn may be zero-value if the pending handler lookup missed.
if conn.wsHandlerFn.Truthy() {
conn.wsHandlerFn.Release()
}
if conn.onMessageFn.Truthy() {
conn.onMessageFn.Release()
}
if conn.onCloseFn.Truthy() {
conn.onCloseFn.Release()
}
p.mu.Lock()
delete(p.activeConnections, conn.id)
delete(p.destinations, conn.id)
delete(p.pendingHandlers, conn.id)
p.mu.Unlock()
})
} }
func (p *RDCleanPathProxy) sendToWebSocket(conn *proxyConnection, data []byte) { func (p *RDCleanPathProxy) sendToWebSocket(conn *proxyConnection, data []byte) {

View File

@@ -13,7 +13,7 @@ import (
func CreateJSInterface(client *Client) js.Value { func CreateJSInterface(client *Client) js.Value {
jsInterface := js.Global().Get("Object").Call("create", js.Null()) jsInterface := js.Global().Get("Object").Call("create", js.Null())
writeFunc := js.FuncOf(func(this js.Value, args []js.Value) any { jsInterface.Set("write", js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) < 1 { if len(args) < 1 {
return js.ValueOf(false) return js.ValueOf(false)
} }
@@ -32,10 +32,9 @@ func CreateJSInterface(client *Client) js.Value {
_, err := client.Write(bytes) _, err := client.Write(bytes)
return js.ValueOf(err == nil) return js.ValueOf(err == nil)
}) }))
jsInterface.Set("write", writeFunc)
resizeFunc := js.FuncOf(func(this js.Value, args []js.Value) any { jsInterface.Set("resize", js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) < 2 { if len(args) < 2 {
return js.ValueOf(false) return js.ValueOf(false)
} }
@@ -43,26 +42,14 @@ func CreateJSInterface(client *Client) js.Value {
rows := args[1].Int() rows := args[1].Int()
err := client.Resize(cols, rows) err := client.Resize(cols, rows)
return js.ValueOf(err == nil) return js.ValueOf(err == nil)
}) }))
jsInterface.Set("resize", resizeFunc)
closeFunc := js.FuncOf(func(this js.Value, args []js.Value) any { jsInterface.Set("close", js.FuncOf(func(this js.Value, args []js.Value) any {
client.Close() client.Close()
return js.Undefined() return js.Undefined()
}) }))
jsInterface.Set("close", closeFunc)
go func() { go readLoop(client, jsInterface)
readLoop(client, jsInterface)
// Detach before releasing so late JS calls surface as TypeError instead
// of silent "call to released function".
jsInterface.Set("write", js.Undefined())
jsInterface.Set("resize", js.Undefined())
jsInterface.Set("close", js.Undefined())
writeFunc.Release()
resizeFunc.Release()
closeFunc.Release()
}()
return jsInterface return jsInterface
} }

View File

@@ -332,7 +332,7 @@ func setupServerHooks(servers *serverInstances, cfg *CombinedConfig) {
log.Infof("Signal server registered on port %s", cfg.Server.ListenAddress) log.Infof("Signal server registered on port %s", cfg.Server.ListenAddress)
} }
s.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), s.IDPHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg)) s.SetHandlerFunc(createCombinedHandler(grpcSrv, s.APIHandler(), servers.relaySrv, servers.metricsServer.Meter, cfg))
if servers.relaySrv != nil { if servers.relaySrv != nil {
log.Infof("Relay WebSocket handler added (path: /relay)") log.Infof("Relay WebSocket handler added (path: /relay)")
} }
@@ -521,7 +521,7 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*
} }
// createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic // createCombinedHandler creates an HTTP handler that multiplexes Management, Signal (via wsproxy), and Relay WebSocket traffic
func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, idpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler { func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {
wsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter)) wsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))
var relayAcceptFn func(conn listener.Conn) var relayAcceptFn func(conn listener.Conn)
@@ -556,10 +556,6 @@ func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, id
http.Error(w, "Relay service not enabled", http.StatusNotFound) http.Error(w, "Relay service not enabled", http.StatusNotFound)
} }
// Embedded IdP (Dex)
case idpHandler != nil && strings.HasPrefix(r.URL.Path, "/oauth2"):
idpHandler.ServeHTTP(w, r)
// Management HTTP API (default) // Management HTTP API (default)
default: default:
httpHandler.ServeHTTP(w, r) httpHandler.ServeHTTP(w, r)

2
go.mod
View File

@@ -335,7 +335,7 @@ replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-2024
replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949 replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949
replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0
replace github.com/cloudflare/circl => codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 replace github.com/cloudflare/circl => codeberg.org/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6

4
go.sum
View File

@@ -499,8 +499,8 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ=
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f h1:ff2D57RBjWtyQ2wVwJOxOgXAXOe/J2lJWtSX0Bz/BRk= github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0 h1:h/QnNzm7xzHPm+gajcblYUOclrW2FeNeDlUNj6tTWKQ=
github.com/netbirdio/wireguard-go v0.0.0-20260523085312-4b4a4e36017f/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= github.com/netbirdio/wireguard-go v0.0.0-20260107100953-33b7c9d03db0/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk= github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=

View File

@@ -32,6 +32,7 @@ import (
"github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/shared/management/status"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
"github.com/netbirdio/netbird/version"
) )
type Controller struct { type Controller struct {
@@ -510,7 +511,7 @@ func computeForwarderPort(peers []*nbpeer.Peer, requiredVersion string) int64 {
for _, peer := range peers { for _, peer := range peers {
// Development version is always supported // Development version is always supported
if peer.Meta.WtVersion == "development" { if version.IsDevelopmentVersion(peer.Meta.WtVersion) {
continue continue
} }
peerVersion := semver.Canonical("v" + peer.Meta.WtVersion) peerVersion := semver.Canonical("v" + peer.Meta.WtVersion)

View File

@@ -51,7 +51,7 @@ func (p *PeersUpdateManager) SendUpdate(ctx context.Context, peerID string, upda
found = true found = true
select { select {
case channel <- update: case channel <- update:
log.WithContext(ctx).Tracef("update was sent to channel for peer %s", peerID) log.WithContext(ctx).Debugf("update was sent to channel for peer %s", peerID)
default: default:
dropped = true dropped = true
log.WithContext(ctx).Warnf("channel for peer %s is %d full or closed", peerID, len(channel)) log.WithContext(ctx).Warnf("channel for peer %s is %d full or closed", peerID, len(channel))

View File

@@ -10,10 +10,8 @@ import (
"slices" "slices"
"time" "time"
"github.com/gorilla/mux"
grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware/v2" grpcMiddleware "github.com/grpc-ecosystem/go-grpc-middleware/v2"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip" "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/realip"
"github.com/rs/cors"
"github.com/rs/xid" "github.com/rs/xid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"google.golang.org/grpc" "google.golang.org/grpc"
@@ -21,6 +19,7 @@ import (
"google.golang.org/grpc/keepalive" "google.golang.org/grpc/keepalive"
cachestore "github.com/eko/gocache/lib/v4/store" cachestore "github.com/eko/gocache/lib/v4/store"
"github.com/netbirdio/management-integrations/integrations"
"github.com/netbirdio/netbird/encryption" "github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/formatter/hook" "github.com/netbirdio/netbird/formatter/hook"
@@ -28,20 +27,16 @@ import (
accesslogsmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager" accesslogsmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc" nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/activity" "github.com/netbirdio/netbird/management/server/activity"
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
nbcache "github.com/netbirdio/netbird/management/server/cache" nbcache "github.com/netbirdio/netbird/management/server/cache"
nbContext "github.com/netbirdio/netbird/management/server/context" nbContext "github.com/netbirdio/netbird/management/server/context"
nbhttp "github.com/netbirdio/netbird/management/server/http" nbhttp "github.com/netbirdio/netbird/management/server/http"
"github.com/netbirdio/netbird/management/server/http/middleware" "github.com/netbirdio/netbird/management/server/http/middleware"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/store" "github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/telemetry"
mgmtProto "github.com/netbirdio/netbird/shared/management/proto" mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/util/crypt" "github.com/netbirdio/netbird/util/crypt"
) )
const apiPrefix = "/api"
var ( var (
kaep = keepalive.EnforcementPolicy{ kaep = keepalive.EnforcementPolicy{
MinTime: 15 * time.Second, MinTime: 15 * time.Second,
@@ -99,17 +94,12 @@ func (s *BaseServer) Store() store.Store {
func (s *BaseServer) EventStore() activity.Store { func (s *BaseServer) EventStore() activity.Store {
return Create(s, func() activity.Store { return Create(s, func() activity.Store {
var err error integrationMetrics, err := integrations.InitIntegrationMetrics(context.Background(), s.Metrics())
key := s.Config.DataStoreEncryptionKey if err != nil {
if key == "" { log.Fatalf("failed to initialize integration metrics: %v", err)
log.Debugf("generate new activity store encryption key")
key, err = crypt.GenerateKey()
if err != nil {
log.Fatalf("failed to generate event store encryption key: %v", err)
}
} }
eventStore, err := activitystore.NewSqlStore(context.Background(), s.Config.Datadir, key) eventStore, _, err := integrations.InitEventStore(context.Background(), s.Config.Datadir, s.Config.DataStoreEncryptionKey, integrationMetrics)
if err != nil { if err != nil {
log.Fatalf("failed to initialize event store: %v", err) log.Fatalf("failed to initialize event store: %v", err)
} }
@@ -120,7 +110,7 @@ func (s *BaseServer) EventStore() activity.Store {
func (s *BaseServer) APIHandler() http.Handler { func (s *BaseServer) APIHandler() http.Handler {
return Create(s, func() http.Handler { return Create(s, func() http.Handler {
httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.Router(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.PermissionsManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies, s.RateLimiter(), s.IsValidChildAccount) httpAPIHandler, err := nbhttp.NewAPIHandler(context.Background(), s.AccountManager(), s.NetworksManager(), s.ResourcesManager(), s.RoutesManager(), s.GroupsManager(), s.GeoLocationManager(), s.AuthManager(), s.Metrics(), s.IntegratedValidator(), s.ProxyController(), s.PermissionsManager(), s.PeersManager(), s.SettingsManager(), s.ZonesManager(), s.RecordsManager(), s.NetworkMapController(), s.IdpManager(), s.ServiceManager(), s.ReverseProxyDomainManager(), s.AccessLogsManager(), s.ReverseProxyGRPCServer(), s.Config.ReverseProxy.TrustedHTTPProxies, s.RateLimiter())
if err != nil { if err != nil {
log.Fatalf("failed to create API handler: %v", err) log.Fatalf("failed to create API handler: %v", err)
} }
@@ -128,22 +118,6 @@ func (s *BaseServer) APIHandler() http.Handler {
}) })
} }
// IDPHandler returns the HTTP handler for the embedded IdP (Dex), or nil if
// the deployment isn't using the embedded variant.
func (s *BaseServer) IDPHandler() http.Handler {
embeddedIdP, ok := s.IdpManager().(*idp.EmbeddedIdPManager)
if !ok || embeddedIdP == nil {
return nil
}
return cors.AllowAll().Handler(embeddedIdP.Handler())
}
func (s *BaseServer) Router() *mux.Router {
return Create(s, func() *mux.Router {
return mux.NewRouter().PathPrefix(apiPrefix).Subrouter()
})
}
func (s *BaseServer) RateLimiter() *middleware.APIRateLimiter { func (s *BaseServer) RateLimiter() *middleware.APIRateLimiter {
return Create(s, func() *middleware.APIRateLimiter { return Create(s, func() *middleware.APIRateLimiter {
cfg, enabled := middleware.RateLimiterConfigFromEnv() cfg, enabled := middleware.RateLimiterConfigFromEnv()

View File

@@ -19,7 +19,6 @@ import (
"github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/auth" "github.com/netbirdio/netbird/management/server/auth"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator" "github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator"
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding" "github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
"github.com/netbirdio/netbird/management/server/job" "github.com/netbirdio/netbird/management/server/job"
nbjwt "github.com/netbirdio/netbird/shared/auth/jwt" nbjwt "github.com/netbirdio/netbird/shared/auth/jwt"
@@ -39,7 +38,7 @@ func (s *BaseServer) JobManager() *job.Manager {
func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator { func (s *BaseServer) IntegratedValidator() integrated_validator.IntegratedValidator {
return Create(s, func() integrated_validator.IntegratedValidator { return Create(s, func() integrated_validator.IntegratedValidator {
integratedPeerValidator, err := validator.NewIntegratedValidator( integratedPeerValidator, err := integrations.NewIntegratedValidator(
context.Background(), context.Background(),
s.PeersManager(), s.PeersManager(),
s.SettingsManager(), s.SettingsManager(),

View File

@@ -57,7 +57,13 @@ func (s *BaseServer) GeoLocationManager() geolocation.Geolocation {
func (s *BaseServer) PermissionsManager() permissions.Manager { func (s *BaseServer) PermissionsManager() permissions.Manager {
return Create(s, func() permissions.Manager { return Create(s, func() permissions.Manager {
return permissions.NewManager(s.Store()) manager := integrations.InitPermissionsManager(s.Store(), s.Metrics().GetMeter())
s.AfterInit(func(s *BaseServer) {
manager.SetAccountManager(s.AccountManager())
})
return manager
}) })
} }
@@ -147,6 +153,7 @@ func (s *BaseServer) IdpManager() idp.Manager {
return idpManager return idpManager
} }
return nil return nil
}) })
} }
@@ -228,7 +235,3 @@ func (s *BaseServer) ReverseProxyDomainManager() *manager.Manager {
return &m return &m
}) })
} }
func (s *BaseServer) IsValidChildAccount(_ context.Context, _, _, _ string) bool {
return false
}

View File

@@ -188,7 +188,7 @@ func (s *BaseServer) Start(ctx context.Context) error {
log.WithContext(srvCtx).Infof("running gRPC backward compatibility server: %s", compatListener.Addr().String()) log.WithContext(srvCtx).Infof("running gRPC backward compatibility server: %s", compatListener.Addr().String())
} }
rootHandler := s.handlerFunc(srvCtx, s.GRPCServer(), s.APIHandler(), s.IDPHandler(), s.Metrics().GetMeter()) rootHandler := s.handlerFunc(srvCtx, s.GRPCServer(), s.APIHandler(), s.Metrics().GetMeter())
switch { switch {
case s.certManager != nil: case s.certManager != nil:
// a call to certManager.Listener() always creates a new listener so we do it once // a call to certManager.Listener() always creates a new listener so we do it once
@@ -299,7 +299,7 @@ func (s *BaseServer) SetHandlerFunc(handler http.Handler) {
log.Tracef("custom handler set successfully") log.Tracef("custom handler set successfully")
} }
func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, idpHandler http.Handler, meter metric.Meter) http.Handler { func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, httpHandler http.Handler, meter metric.Meter) http.Handler {
// Check if a custom handler was set (for multiplexing additional services) // Check if a custom handler was set (for multiplexing additional services)
if customHandler, ok := s.GetContainer("customHandler"); ok { if customHandler, ok := s.GetContainer("customHandler"); ok {
if handler, ok := customHandler.(http.Handler); ok { if handler, ok := customHandler.(http.Handler); ok {
@@ -318,8 +318,6 @@ func (s *BaseServer) handlerFunc(_ context.Context, gRPCHandler *grpc.Server, ht
gRPCHandler.ServeHTTP(writer, request) gRPCHandler.ServeHTTP(writer, request)
case request.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent: case request.URL.Path == wsproxy.ProxyPath+wsproxy.ManagementComponent:
wsProxy.Handler().ServeHTTP(writer, request) wsProxy.Handler().ServeHTTP(writer, request)
case idpHandler != nil && strings.HasPrefix(request.URL.Path, "/oauth2"):
idpHandler.ServeHTTP(writer, request)
default: default:
httpHandler.ServeHTTP(writer, request) httpHandler.ServeHTTP(writer, request)
} }

View File

@@ -437,7 +437,7 @@ func (s *Server) handleUpdates(ctx context.Context, accountID string, peerKey wg
return nil return nil
} }
log.WithContext(ctx).Tracef("received an update for peer %s", peerKey.String()) log.WithContext(ctx).Debugf("received an update for peer %s", peerKey.String())
if debouncer.ProcessUpdate(update) { if debouncer.ProcessUpdate(update) {
// Send immediately (first update or after quiet period) // Send immediately (first update or after quiet period)
if err := s.sendUpdate(ctx, accountID, peerKey, peer, update, srv, streamStartTime); err != nil { if err := s.sendUpdate(ctx, accountID, peerKey, peer, update, srv, streamStartTime); err != nil {
@@ -492,7 +492,7 @@ func (s *Server) sendUpdate(ctx context.Context, accountID string, peerKey wgtyp
s.cancelPeerRoutines(ctx, accountID, peer, streamStartTime) s.cancelPeerRoutines(ctx, accountID, peer, streamStartTime)
return status.Errorf(codes.Internal, "failed sending update message") return status.Errorf(codes.Internal, "failed sending update message")
} }
log.WithContext(ctx).Tracef("sent an update to peer %s", peerKey.String()) log.WithContext(ctx).Debugf("sent an update to peer %s", peerKey.String())
return nil return nil
} }

View File

@@ -15,13 +15,15 @@ import (
"github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxytoken"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxytoken"
reverseproxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager" reverseproxymanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service/manager"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc" nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
idpmanager "github.com/netbirdio/netbird/management/server/idp" idpmanager "github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/management-integrations/integrations"
"github.com/netbirdio/netbird/management/internals/controllers/network_map" "github.com/netbirdio/netbird/management/internals/controllers/network_map"
"github.com/netbirdio/netbird/management/internals/modules/zones" "github.com/netbirdio/netbird/management/internals/modules/zones"
zonesManager "github.com/netbirdio/netbird/management/internals/modules/zones/manager" zonesManager "github.com/netbirdio/netbird/management/internals/modules/zones/manager"
@@ -30,10 +32,12 @@ import (
"github.com/netbirdio/netbird/management/server/account" "github.com/netbirdio/netbird/management/server/account"
"github.com/netbirdio/netbird/management/server/settings" "github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
"github.com/netbirdio/netbird/management/server/permissions" "github.com/netbirdio/netbird/management/server/permissions"
"github.com/netbirdio/netbird/management/server/http/handlers/proxy" "github.com/netbirdio/netbird/management/server/http/handlers/proxy"
nbpeers "github.com/netbirdio/netbird/management/internals/modules/peers"
"github.com/netbirdio/netbird/management/server/auth" "github.com/netbirdio/netbird/management/server/auth"
"github.com/netbirdio/netbird/management/server/geolocation" "github.com/netbirdio/netbird/management/server/geolocation"
nbgroups "github.com/netbirdio/netbird/management/server/groups" nbgroups "github.com/netbirdio/netbird/management/server/groups"
@@ -52,14 +56,17 @@ import (
"github.com/netbirdio/netbird/management/server/http/middleware" "github.com/netbirdio/netbird/management/server/http/middleware"
"github.com/netbirdio/netbird/management/server/http/middleware/bypass" "github.com/netbirdio/netbird/management/server/http/middleware/bypass"
nbinstance "github.com/netbirdio/netbird/management/server/instance" nbinstance "github.com/netbirdio/netbird/management/server/instance"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator"
nbnetworks "github.com/netbirdio/netbird/management/server/networks" nbnetworks "github.com/netbirdio/netbird/management/server/networks"
"github.com/netbirdio/netbird/management/server/networks/resources" "github.com/netbirdio/netbird/management/server/networks/resources"
"github.com/netbirdio/netbird/management/server/networks/routers" "github.com/netbirdio/netbird/management/server/networks/routers"
"github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/telemetry"
) )
const apiPrefix = "/api"
// NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints. // NewAPIHandler creates the Management service HTTP API handler registering all the available endpoints.
func NewAPIHandler(ctx context.Context, router *mux.Router, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, permissionsManager permissions.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix, rateLimiter *middleware.APIRateLimiter, isValidChildAccount middleware.IsValidChildAccountFunc) (http.Handler, error) { func NewAPIHandler(ctx context.Context, accountManager account.Manager, networksManager nbnetworks.Manager, resourceManager resources.Manager, routerManager routers.Manager, groupsManager nbgroups.Manager, LocationManager geolocation.Geolocation, authManager auth.Manager, appMetrics telemetry.AppMetrics, integratedValidator integrated_validator.IntegratedValidator, proxyController port_forwarding.Controller, permissionsManager permissions.Manager, peersManager nbpeers.Manager, settingsManager settings.Manager, zManager zones.Manager, rManager records.Manager, networkMapController network_map.Controller, idpManager idpmanager.Manager, serviceManager service.Manager, reverseProxyDomainManager *manager.Manager, reverseProxyAccessLogsManager accesslogs.Manager, proxyGRPCServer *nbgrpc.ProxyServiceServer, trustedHTTPProxies []netip.Prefix, rateLimiter *middleware.APIRateLimiter) (http.Handler, error) {
// Register bypass paths for unauthenticated endpoints // Register bypass paths for unauthenticated endpoints
if err := bypass.AddBypassPath("/api/instance"); err != nil { if err := bypass.AddBypassPath("/api/instance"); err != nil {
@@ -93,16 +100,25 @@ func NewAPIHandler(ctx context.Context, router *mux.Router, accountManager accou
accountManager.GetUserFromUserAuth, accountManager.GetUserFromUserAuth,
rateLimiter, rateLimiter,
appMetrics.GetMeter(), appMetrics.GetMeter(),
isValidChildAccount,
) )
corsMiddleware := cors.AllowAll() corsMiddleware := cors.AllowAll()
rootRouter := mux.NewRouter()
metricsMiddleware := appMetrics.HTTPMiddleware() metricsMiddleware := appMetrics.HTTPMiddleware()
prefix := apiPrefix
router := rootRouter.PathPrefix(prefix).Subrouter()
router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler) router.Use(metricsMiddleware.Handler, corsMiddleware.Handler, authMiddleware.Handler)
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), idpManager) if _, err := integrations.RegisterHandlers(ctx, prefix, router, accountManager, integratedValidator, appMetrics.GetMeter(), permissionsManager, peersManager, proxyController, settingsManager); err != nil {
return nil, fmt.Errorf("register integrations endpoints: %w", err)
}
// Check if embedded IdP is enabled for instance manager
embeddedIdP, embeddedIdpEnabled := idpManager.(*idpmanager.EmbeddedIdPManager)
instanceManager, err := nbinstance.NewManager(ctx, accountManager.GetStore(), embeddedIdP)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create instance manager: %w", err) return nil, fmt.Errorf("failed to create instance manager: %w", err)
} }
@@ -138,5 +154,10 @@ func NewAPIHandler(ctx context.Context, router *mux.Router, accountManager accou
oauthHandler.RegisterEndpoints(router) oauthHandler.RegisterEndpoints(router)
} }
return router, nil // Mount embedded IdP handler at /oauth2 path if configured
if embeddedIdpEnabled {
rootRouter.PathPrefix("/oauth2").Handler(corsMiddleware.Handler(embeddedIdP.Handler()))
}
return rootRouter, nil
} }

View File

@@ -11,6 +11,8 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
"github.com/netbirdio/management-integrations/integrations"
serverauth "github.com/netbirdio/netbird/management/server/auth" serverauth "github.com/netbirdio/netbird/management/server/auth"
nbcontext "github.com/netbirdio/netbird/management/server/context" nbcontext "github.com/netbirdio/netbird/management/server/context"
"github.com/netbirdio/netbird/management/server/http/middleware/bypass" "github.com/netbirdio/netbird/management/server/http/middleware/bypass"
@@ -25,8 +27,6 @@ type SyncUserJWTGroupsFunc func(ctx context.Context, userAuth auth.UserAuth) err
type GetUserFromUserAuthFunc func(ctx context.Context, userAuth auth.UserAuth) (*types.User, error) type GetUserFromUserAuthFunc func(ctx context.Context, userAuth auth.UserAuth) (*types.User, error)
type IsValidChildAccountFunc func(ctx context.Context, userID, accountID, childAccountID string) bool
// AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens // AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
type AuthMiddleware struct { type AuthMiddleware struct {
authManager serverauth.Manager authManager serverauth.Manager
@@ -35,7 +35,6 @@ type AuthMiddleware struct {
syncUserJWTGroups SyncUserJWTGroupsFunc syncUserJWTGroups SyncUserJWTGroupsFunc
rateLimiter *APIRateLimiter rateLimiter *APIRateLimiter
patUsageTracker *PATUsageTracker patUsageTracker *PATUsageTracker
isValidChildAccount IsValidChildAccountFunc
} }
// NewAuthMiddleware instance constructor // NewAuthMiddleware instance constructor
@@ -46,7 +45,6 @@ func NewAuthMiddleware(
getUserFromUserAuth GetUserFromUserAuthFunc, getUserFromUserAuth GetUserFromUserAuthFunc,
rateLimiter *APIRateLimiter, rateLimiter *APIRateLimiter,
meter metric.Meter, meter metric.Meter,
isValidChildAccount IsValidChildAccountFunc,
) *AuthMiddleware { ) *AuthMiddleware {
var patUsageTracker *PATUsageTracker var patUsageTracker *PATUsageTracker
if meter != nil { if meter != nil {
@@ -64,7 +62,6 @@ func NewAuthMiddleware(
getUserFromUserAuth: getUserFromUserAuth, getUserFromUserAuth: getUserFromUserAuth,
rateLimiter: rateLimiter, rateLimiter: rateLimiter,
patUsageTracker: patUsageTracker, patUsageTracker: patUsageTracker,
isValidChildAccount: isValidChildAccount,
} }
} }
@@ -127,7 +124,7 @@ func (m *AuthMiddleware) checkJWTFromRequest(r *http.Request, authHeaderParts []
} }
if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 { if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 {
if m.isValidChildAccount(ctx, userAuth.UserId, userAuth.AccountId, impersonate[0]) { if integrations.IsValidChildAccount(ctx, userAuth.UserId, userAuth.AccountId, impersonate[0]) {
userAuth.AccountId = impersonate[0] userAuth.AccountId = impersonate[0]
userAuth.IsChild = true userAuth.IsChild = true
} }
@@ -206,7 +203,7 @@ func (m *AuthMiddleware) checkPATFromRequest(r *http.Request, authHeaderParts []
} }
if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 { if impersonate, ok := r.URL.Query()["account"]; ok && len(impersonate) == 1 {
if m.isValidChildAccount(r.Context(), userAuth.UserId, userAuth.AccountId, impersonate[0]) { if integrations.IsValidChildAccount(r.Context(), userAuth.UserId, userAuth.AccountId, impersonate[0]) {
userAuth.AccountId = impersonate[0] userAuth.AccountId = impersonate[0]
userAuth.IsChild = true userAuth.IsChild = true
} }

View File

@@ -211,7 +211,6 @@ func TestAuthMiddleware_Handler(t *testing.T) {
}, },
disabledLimiter, disabledLimiter,
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handlerToTest := authMiddleware.Handler(nextHandler) handlerToTest := authMiddleware.Handler(nextHandler)
@@ -271,7 +270,6 @@ func TestAuthMiddleware_RateLimiting(t *testing.T) {
}, },
NewAPIRateLimiter(rateLimitConfig), NewAPIRateLimiter(rateLimitConfig),
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -324,7 +322,6 @@ func TestAuthMiddleware_RateLimiting(t *testing.T) {
}, },
NewAPIRateLimiter(rateLimitConfig), NewAPIRateLimiter(rateLimitConfig),
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -368,7 +365,6 @@ func TestAuthMiddleware_RateLimiting(t *testing.T) {
}, },
NewAPIRateLimiter(rateLimitConfig), NewAPIRateLimiter(rateLimitConfig),
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -413,7 +409,6 @@ func TestAuthMiddleware_RateLimiting(t *testing.T) {
}, },
NewAPIRateLimiter(rateLimitConfig), NewAPIRateLimiter(rateLimitConfig),
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -478,7 +473,6 @@ func TestAuthMiddleware_RateLimiting(t *testing.T) {
}, },
NewAPIRateLimiter(rateLimitConfig), NewAPIRateLimiter(rateLimitConfig),
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -538,7 +532,6 @@ func TestAuthMiddleware_RateLimiting(t *testing.T) {
}, },
NewAPIRateLimiter(rateLimitConfig), NewAPIRateLimiter(rateLimitConfig),
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -594,7 +587,6 @@ func TestAuthMiddleware_RateLimiting(t *testing.T) {
}, },
NewAPIRateLimiter(rateLimitConfig), NewAPIRateLimiter(rateLimitConfig),
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := authMiddleware.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -695,7 +687,6 @@ func TestAuthMiddleware_Handler_Child(t *testing.T) {
}, },
disabledLimiter, disabledLimiter,
nil, nil,
func(_ context.Context, _, _, _ string) bool { return false },
) )
for _, tc := range tt { for _, tc := range tt {

View File

@@ -7,7 +7,6 @@ import (
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/metric/noop" "go.opentelemetry.io/otel/metric/noop"
@@ -136,8 +135,7 @@ func BuildApiBlackBoxWithDBState(t testing_tools.TB, sqlFile string, expectedPee
customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "") customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "")
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager) zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter() apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil)
apiHandler, err := http2.NewAPIHandler(context.Background(), apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create API handler: %v", err) t.Fatalf("Failed to create API handler: %v", err)
} }
@@ -266,8 +264,7 @@ func BuildApiBlackBoxWithDBStateAndPeerChannel(t testing_tools.TB, sqlFile strin
customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "") customZonesManager := zonesManager.NewManager(store, am, permissionsManager, "")
zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager) zoneRecordsManager := recordsManager.NewManager(store, am, permissionsManager)
apiRouter := mux.NewRouter().PathPrefix("/api").Subrouter() apiHandler, err := http2.NewAPIHandler(context.Background(), am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, validatorMock, proxyController, permissionsManager, peersManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil)
apiHandler, err := http2.NewAPIHandler(context.Background(), apiRouter, am, networksManager, resourcesManager, routersManager, groupsManager, geoMock, authManagerMock, metrics, permissionsManager, settingsManager, customZonesManager, zoneRecordsManager, networkMapController, nil, serviceManager, nil, nil, nil, nil, nil, nil)
if err != nil { if err != nil {
t.Fatalf("Failed to create API handler: %v", err) t.Fatalf("Failed to create API handler: %v", err)
} }

View File

@@ -1,62 +0,0 @@
package validator
import (
"context"
cachestore "github.com/eko/gocache/lib/v4/store"
"github.com/netbirdio/netbird/management/internals/modules/peers"
"github.com/netbirdio/netbird/management/server/activity"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/settings"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/shared/management/proto"
)
type IntegratedValidatorImpl struct{}
func NewIntegratedValidator(_ context.Context, _ peers.Manager, _ settings.Manager, _ activity.Store, _ cachestore.StoreInterface) (*IntegratedValidatorImpl, error) {
return &IntegratedValidatorImpl{}, nil
}
func (v *IntegratedValidatorImpl) ValidateExtraSettings(context.Context, *types.ExtraSettings, *types.ExtraSettings, string, string) error {
return nil
}
func (v *IntegratedValidatorImpl) ValidatePeer(_ context.Context, update *nbpeer.Peer, _ *nbpeer.Peer, _ string, _ string, _ string, _ []string, _ *types.ExtraSettings) (*nbpeer.Peer, bool, error) {
return update, false, nil
}
func (v *IntegratedValidatorImpl) PreparePeer(_ context.Context, _ string, peer *nbpeer.Peer, _ []string, _ *types.ExtraSettings, _ bool) *nbpeer.Peer {
return peer.Copy()
}
func (v *IntegratedValidatorImpl) IsNotValidPeer(_ context.Context, _ string, _ *nbpeer.Peer, _ []string, _ *types.ExtraSettings) (bool, bool, error) {
return false, false, nil
}
func (v *IntegratedValidatorImpl) GetValidatedPeers(_ context.Context, _ string, _ []*types.Group, peers []*nbpeer.Peer, _ *types.ExtraSettings) (map[string]struct{}, error) {
validatedPeers := make(map[string]struct{})
for _, p := range peers {
validatedPeers[p.ID] = struct{}{}
}
return validatedPeers, nil
}
func (v *IntegratedValidatorImpl) GetInvalidPeers(_ context.Context, _ string, _ *types.ExtraSettings) (map[string]string, error) {
return make(map[string]string), nil
}
func (v *IntegratedValidatorImpl) PeerDeleted(_ context.Context, _, _ string, _ *types.ExtraSettings) error {
return nil
}
func (v *IntegratedValidatorImpl) SetPeerInvalidationListener(_ func(accountID string, peerIDs []string)) {
}
func (v *IntegratedValidatorImpl) Stop(_ context.Context) {
}
func (v *IntegratedValidatorImpl) ValidateFlowResponse(_ context.Context, _ string, flowResponse *proto.PKCEAuthorizationFlow) *proto.PKCEAuthorizationFlow {
return flowResponse
}

View File

@@ -30,6 +30,7 @@ import (
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/telemetry" "github.com/netbirdio/netbird/management/server/telemetry"
"github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/shared/management/status"
"github.com/netbirdio/netbird/version"
) )
const remoteJobsMinVer = "0.64.0" const remoteJobsMinVer = "0.64.0"
@@ -372,7 +373,7 @@ func (am *DefaultAccountManager) CreatePeerJob(ctx context.Context, accountID, p
} }
meetMinVer, err := posture.MeetsMinVersion(remoteJobsMinVer, p.Meta.WtVersion) meetMinVer, err := posture.MeetsMinVersion(remoteJobsMinVer, p.Meta.WtVersion)
if !strings.Contains(p.Meta.WtVersion, "dev") && (!meetMinVer || err != nil) { if !version.IsDevelopmentVersion(p.Meta.WtVersion) && (!meetMinVer || err != nil) {
return status.Errorf(status.PreconditionFailed, "peer version %s does not meet the minimum required version %s for remote jobs", p.Meta.WtVersion, remoteJobsMinVer) return status.Errorf(status.PreconditionFailed, "peer version %s does not meet the minimum required version %s for remote jobs", p.Meta.WtVersion, remoteJobsMinVer)
} }

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/hashicorp/go-version" "github.com/hashicorp/go-version"
log "github.com/sirupsen/logrus"
nbpeer "github.com/netbirdio/netbird/management/server/peer" nbpeer "github.com/netbirdio/netbird/management/server/peer"
) )
@@ -32,6 +33,9 @@ func (n *NBVersionCheck) Check(ctx context.Context, peer nbpeer.Peer) (bool, err
return true, nil return true, nil
} }
log.WithContext(ctx).Debugf("peer %s NB version %s is older than minimum allowed version %s",
peer.ID, peer.Meta.WtVersion, n.MinVersion)
return false, nil return false, nil
} }

View File

@@ -100,6 +100,8 @@ func checkMinVersion(ctx context.Context, peerGoOS, peerVersion string, check *M
return true, nil return true, nil
} }
log.WithContext(ctx).Debugf("peer %s OS version %s is older than minimum allowed version %s", peerGoOS, peerVersion, check.MinVersion)
return false, nil return false, nil
} }
@@ -123,5 +125,7 @@ func checkMinKernelVersion(ctx context.Context, peerGoOS, peerVersion string, ch
return true, nil return true, nil
} }
log.WithContext(ctx).Debugf("peer %s kernel version %s is older than minimum allowed version %s", peerGoOS, peerVersion, check.MinKernelVersion)
return false, nil return false, nil
} }

View File

@@ -29,6 +29,7 @@ import (
"github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/status" "github.com/netbirdio/netbird/shared/management/status"
"github.com/netbirdio/netbird/version"
) )
const ( const (
@@ -1804,7 +1805,7 @@ func shouldCheckRulesForNativeSSH(supportsNative bool, rule *PolicyRule, peer *n
// peerSupportedFirewallFeatures checks if the peer version supports port ranges. // peerSupportedFirewallFeatures checks if the peer version supports port ranges.
func peerSupportedFirewallFeatures(peerVer string) supportedFeatures { func peerSupportedFirewallFeatures(peerVer string) supportedFeatures {
if strings.Contains(peerVer, "dev") { if version.IsDevelopmentVersion(peerVer) {
return supportedFeatures{true, true} return supportedFeatures{true, true}
} }

View File

@@ -646,41 +646,7 @@ func Test_ExpandPortsAndRanges_SSHRuleExpansion(t *testing.T) {
expectedPorts: []string{"20-25", "10-100", "22022"}, expectedPorts: []string{"20-25", "10-100", "22022"},
}, },
{ {
name: "dev suffix version supports all features", name: "development version supports all features",
peer: &nbpeer.Peer{
ID: "peer1",
SSHEnabled: true,
Meta: nbpeer.PeerSystemMeta{
WtVersion: "0.50.0-dev",
Flags: nbpeer.Flags{ServerSSHAllowed: true},
},
},
rule: &PolicyRule{
Protocol: PolicyRuleProtocolTCP,
Ports: []string{"22"},
},
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
expectedPorts: []string{"22", "22022"},
},
{
name: "dev suffix version supports all features",
peer: &nbpeer.Peer{
ID: "peer1",
SSHEnabled: true,
Meta: nbpeer.PeerSystemMeta{
WtVersion: "dev",
Flags: nbpeer.Flags{ServerSSHAllowed: true},
},
},
rule: &PolicyRule{
Protocol: PolicyRuleProtocolTCP,
Ports: []string{"22"},
},
base: FirewallRule{PeerIP: "10.0.0.1", Direction: 0, Action: "accept", Protocol: "tcp"},
expectedPorts: []string{"22", "22022"},
},
{
name: "development suffix version supports all features",
peer: &nbpeer.Peer{ peer: &nbpeer.Peer{
ID: "peer1", ID: "peer1",
SSHEnabled: true, SSHEnabled: true,

View File

@@ -762,7 +762,7 @@ func (am *DefaultAccountManager) processUserUpdate(ctx context.Context, transact
} }
// Ensure the initiator still has admin privileges // Ensure the initiator still has admin privileges
if !freshInitiator.HasAdminPower() { if initiatorUser.HasAdminPower() && !freshInitiator.HasAdminPower() {
return false, nil, nil, nil, status.Errorf(status.PermissionDenied, "initiator role was changed during request processing") return false, nil, nil, nil, status.Errorf(status.PermissionDenied, "initiator role was changed during request processing")
} }
initiatorUser = freshInitiator initiatorUser = freshInitiator
@@ -906,23 +906,19 @@ func validateUserUpdate(groupsMap map[string]*types.Group, initiatorUser, oldUse
return nil return nil
} }
if !initiatorUser.HasAdminPower() {
return status.Errorf(status.PermissionDenied, "only admins and owners can update users")
}
if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && oldUser.Blocked != update.Blocked { if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && oldUser.Blocked != update.Blocked {
return status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves") return status.Errorf(status.PermissionDenied, "admins can't block or unblock themselves")
} }
if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && update.Role != initiatorUser.Role { if initiatorUser.HasAdminPower() && initiatorUser.Id == update.Id && update.Role != initiatorUser.Role {
return status.Errorf(status.PermissionDenied, "admins can't change their role") return status.Errorf(status.PermissionDenied, "admins can't change their role")
} }
if initiatorUser.Role != types.UserRoleOwner && oldUser.Role == types.UserRoleOwner && update.Role != oldUser.Role { if initiatorUser.Role == types.UserRoleAdmin && oldUser.Role == types.UserRoleOwner && update.Role != oldUser.Role {
return status.Errorf(status.PermissionDenied, "only owners can remove owner role from their user") return status.Errorf(status.PermissionDenied, "only owners can remove owner role from their user")
} }
if oldUser.Role == types.UserRoleOwner && update.IsBlocked() && !oldUser.IsBlocked() { if initiatorUser.Role == types.UserRoleAdmin && oldUser.Role == types.UserRoleOwner && update.IsBlocked() && !oldUser.IsBlocked() {
return status.Errorf(status.PermissionDenied, "unable to block owner user") return status.Errorf(status.PermissionDenied, "unable to block owner user")
} }
if initiatorUser.Role != types.UserRoleOwner && update.Role == types.UserRoleOwner && update.Role != oldUser.Role { if initiatorUser.Role == types.UserRoleAdmin && update.Role == types.UserRoleOwner && update.Role != oldUser.Role {
return status.Errorf(status.PermissionDenied, "only owners can add owner role to other users") return status.Errorf(status.PermissionDenied, "only owners can add owner role to other users")
} }
if oldUser.IsServiceUser && update.Role == types.UserRoleOwner { if oldUser.IsServiceUser && update.Role == types.UserRoleOwner {

View File

@@ -109,22 +109,6 @@ var debugStopCmd = &cobra.Command{
SilenceUsage: true, SilenceUsage: true,
} }
var debugPerfCmd = &cobra.Command{
Use: "perf <pool-cap>",
Short: "Live-retune the tunnel buffer pool cap on all running clients",
Args: cobra.ExactArgs(1),
RunE: runDebugPerfSet,
SilenceUsage: true,
}
var debugRuntimeCmd = &cobra.Command{
Use: "runtime",
Short: "Show runtime stats (heap, goroutines, RSS)",
Args: cobra.NoArgs,
RunE: runDebugRuntime,
SilenceUsage: true,
}
var debugCaptureCmd = &cobra.Command{ var debugCaptureCmd = &cobra.Command{
Use: "capture <account-id> [filter expression]", Use: "capture <account-id> [filter expression]",
Short: "Capture packets on a client's WireGuard interface", Short: "Capture packets on a client's WireGuard interface",
@@ -175,8 +159,6 @@ func init() {
debugCmd.AddCommand(debugLogCmd) debugCmd.AddCommand(debugLogCmd)
debugCmd.AddCommand(debugStartCmd) debugCmd.AddCommand(debugStartCmd)
debugCmd.AddCommand(debugStopCmd) debugCmd.AddCommand(debugStopCmd)
debugCmd.AddCommand(debugPerfCmd)
debugCmd.AddCommand(debugRuntimeCmd)
debugCmd.AddCommand(debugCaptureCmd) debugCmd.AddCommand(debugCaptureCmd)
rootCmd.AddCommand(debugCmd) rootCmd.AddCommand(debugCmd)
@@ -238,18 +220,6 @@ func runDebugStop(cmd *cobra.Command, args []string) error {
return getDebugClient(cmd).StopClient(cmd.Context(), args[0]) return getDebugClient(cmd).StopClient(cmd.Context(), args[0])
} }
func runDebugPerfSet(cmd *cobra.Command, args []string) error {
n, err := strconv.ParseUint(args[0], 10, 32)
if err != nil {
return fmt.Errorf("invalid value %q: %w", args[0], err)
}
return getDebugClient(cmd).PerfSet(cmd.Context(), uint32(n))
}
func runDebugRuntime(cmd *cobra.Command, _ []string) error {
return getDebugClient(cmd).Runtime(cmd.Context())
}
func runDebugCapture(cmd *cobra.Command, args []string) error { func runDebugCapture(cmd *cobra.Command, args []string) error {
duration, _ := cmd.Flags().GetDuration("duration") duration, _ := cmd.Flags().GetDuration("duration")
forcePcap, _ := cmd.Flags().GetBool("pcap") forcePcap, _ := cmd.Flags().GetBool("pcap")

View File

@@ -15,22 +15,11 @@ import (
"github.com/netbirdio/netbird/shared/management/domain" "github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/client/embed"
"github.com/netbirdio/netbird/proxy" "github.com/netbirdio/netbird/proxy"
nbacme "github.com/netbirdio/netbird/proxy/internal/acme" nbacme "github.com/netbirdio/netbird/proxy/internal/acme"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
) )
const (
// envPreallocatedBuffers caps the per-tunnel buffer pool. Zero (unset)
// keeps the upstream uncapped default.
envPreallocatedBuffers = "NB_PROXY_PREALLOCATED_BUFFERS"
// envMaxBatchSize overrides the per-tunnel batch size, which controls
// how many buffers each receive/TUN worker eagerly allocates. Zero
// (unset) keeps the platform default.
envMaxBatchSize = "NB_PROXY_MAX_BATCH_SIZE"
)
const DefaultManagementURL = "https://api.netbird.io:443" const DefaultManagementURL = "https://api.netbird.io:443"
// envProxyToken is the environment variable name for the proxy access token. // envProxyToken is the environment variable name for the proxy access token.
@@ -159,45 +148,6 @@ func runServer(cmd *cobra.Command, args []string) error {
logger.Infof("configured log level: %s", level) logger.Infof("configured log level: %s", level)
var wgPool, wgBatch uint64
var perf embed.Performance
if raw := os.Getenv(envPreallocatedBuffers); raw != "" {
n, err := strconv.ParseUint(raw, 10, 32)
if err != nil {
return fmt.Errorf("invalid %s %q: %w", envPreallocatedBuffers, raw, err)
}
wgPool = n
v := uint32(n)
perf.PreallocatedBuffersPerPool = &v
logger.Infof("tunnel preallocated buffers per pool: %d", n)
}
if raw := os.Getenv(envMaxBatchSize); raw != "" {
n, err := strconv.ParseUint(raw, 10, 32)
if err != nil {
return fmt.Errorf("invalid %s %q: %w", envMaxBatchSize, raw, err)
}
wgBatch = n
v := uint32(n)
perf.MaxBatchSize = &v
logger.Infof("tunnel max batch size override: %d", n)
}
if wgPool > 0 {
// Each bind recv goroutine (IPv4 + IPv6 + ICE relay) plus
// RoutineReadFromTUN eagerly reserves `batch` message buffers for
// the lifetime of the Device. A pool cap below that floor blocks
// the receive pipeline at startup.
batch := wgBatch
if batch == 0 {
batch = 128
}
const recvGoroutines = 4
floor := batch * recvGoroutines
if wgPool < floor {
logger.Warnf("%s=%d is below the eager-allocation floor (~%d for batch=%d); startup may deadlock",
envPreallocatedBuffers, wgPool, floor, batch)
}
}
switch forwardedProto { switch forwardedProto {
case "auto", "http", "https": case "auto", "http", "https":
default: default:
@@ -238,7 +188,6 @@ func runServer(cmd *cobra.Command, args []string) error {
CertLockMethod: nbacme.CertLockMethod(certLockMethod), CertLockMethod: nbacme.CertLockMethod(certLockMethod),
WildcardCertDir: wildcardCertDir, WildcardCertDir: wildcardCertDir,
WireguardPort: wgPort, WireguardPort: wgPort,
Performance: perf,
ProxyProtocol: proxyProtocol, ProxyProtocol: proxyProtocol,
PreSharedKey: preSharedKey, PreSharedKey: preSharedKey,
SupportsCustomPorts: supportsCustomPorts, SupportsCustomPorts: supportsCustomPorts,

View File

@@ -333,63 +333,6 @@ func (c *Client) printLogLevelResult(data map[string]any) {
} }
} }
// PerfSet live-retunes the tunnel buffer pool cap on all running embedded
// clients. Batch size is not live-tunable; configure it at proxy startup.
func (c *Client) PerfSet(ctx context.Context, value uint32) error {
path := fmt.Sprintf("/debug/perf?value=%d", value)
return c.fetchAndPrint(ctx, path, c.printPerfSet)
}
func (c *Client) printPerfSet(data map[string]any) {
if errMsg, ok := data["error"].(string); ok && errMsg != "" {
c.printError(data)
return
}
val, _ := data["value"].(float64)
applied, _ := data["applied"].(float64)
_, _ = fmt.Fprintf(c.out, "Pool cap set to: %d\n", uint32(val))
_, _ = fmt.Fprintf(c.out, "Applied to %d live clients\n", int(applied))
if failed, ok := data["failed"].(map[string]any); ok && len(failed) > 0 {
_, _ = fmt.Fprintln(c.out, "Failed:")
for k, v := range failed {
_, _ = fmt.Fprintf(c.out, " %s: %v\n", k, v)
}
}
}
// Runtime fetches runtime stats (heap, goroutines, RSS).
func (c *Client) Runtime(ctx context.Context) error {
return c.fetchAndPrint(ctx, "/debug/runtime", c.printRuntime)
}
func (c *Client) printRuntime(data map[string]any) {
i := func(k string) uint64 {
v, _ := data[k].(float64)
return uint64(v)
}
mb := func(n uint64) string { return fmt.Sprintf("%.1f MB", float64(n)/(1<<20)) }
_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "Go: %v on %d CPU (GOMAXPROCS=%d)\n", data["go_version"], uint32(i("num_cpu")), uint32(i("gomaxprocs")))
_, _ = fmt.Fprintf(c.out, "Goroutines: %d\n", i("goroutines"))
_, _ = fmt.Fprintf(c.out, "Live objects: %d\n", i("live_objects"))
_, _ = fmt.Fprintf(c.out, "GC: %d cycles, %v pause total\n", i("num_gc"), time.Duration(i("pause_total_ns")))
_, _ = fmt.Fprintln(c.out, "Heap:")
_, _ = fmt.Fprintf(c.out, " alloc: %s\n", mb(i("heap_alloc")))
_, _ = fmt.Fprintf(c.out, " in-use: %s\n", mb(i("heap_inuse")))
_, _ = fmt.Fprintf(c.out, " idle: %s\n", mb(i("heap_idle")))
_, _ = fmt.Fprintf(c.out, " released: %s\n", mb(i("heap_released")))
_, _ = fmt.Fprintf(c.out, " sys: %s\n", mb(i("heap_sys")))
_, _ = fmt.Fprintf(c.out, "Total sys: %s\n", mb(i("sys")))
if _, ok := data["vm_rss"]; ok {
_, _ = fmt.Fprintln(c.out, "Process:")
_, _ = fmt.Fprintf(c.out, " VmRSS: %s\n", mb(i("vm_rss")))
_, _ = fmt.Fprintf(c.out, " VmSize: %s\n", mb(i("vm_size")))
_, _ = fmt.Fprintf(c.out, " VmData: %s\n", mb(i("vm_data")))
}
_, _ = fmt.Fprintf(c.out, "Clients: %d (%d started)\n", i("clients"), i("started"))
}
// StartClient starts a specific client. // StartClient starts a specific client.
func (c *Client) StartClient(ctx context.Context, accountID string) error { func (c *Client) StartClient(ctx context.Context, accountID string) error {
path := "/debug/clients/" + url.PathEscape(accountID) + "/start" path := "/debug/clients/" + url.PathEscape(accountID) + "/start"

View File

@@ -11,8 +11,6 @@ import (
"maps" "maps"
"net" "net"
"net/http" "net/http"
"os"
"runtime"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@@ -61,7 +59,6 @@ func sortedAccountIDs(m map[types.AccountID]roundtrip.ClientDebugInfo) []types.A
type clientProvider interface { type clientProvider interface {
GetClient(accountID types.AccountID) (*nbembed.Client, bool) GetClient(accountID types.AccountID) (*nbembed.Client, bool)
ListClientsForDebug() map[types.AccountID]roundtrip.ClientDebugInfo ListClientsForDebug() map[types.AccountID]roundtrip.ClientDebugInfo
ListClientsForStartup() map[types.AccountID]*nbembed.Client
} }
// InboundListenerInfo describes a per-account inbound listener as // InboundListenerInfo describes a per-account inbound listener as
@@ -168,10 +165,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleListClients(w, r, wantJSON) h.handleListClients(w, r, wantJSON)
case "/debug/health": case "/debug/health":
h.handleHealth(w, r, wantJSON) h.handleHealth(w, r, wantJSON)
case "/debug/perf":
h.handlePerf(w, r)
case "/debug/runtime":
h.handleRuntime(w, r)
default: default:
if h.handleClientRoutes(w, r, path, wantJSON) { if h.handleClientRoutes(w, r, path, wantJSON) {
return return
@@ -265,10 +258,10 @@ func (h *Handler) handleIndex(w http.ResponseWriter, _ *http.Request, wantJSON b
} }
if wantJSON { if wantJSON {
clientsJSON := make([]map[string]any, 0, len(clients)) clientsJSON := make([]map[string]interface{}, 0, len(clients))
for _, id := range sortedIDs { for _, id := range sortedIDs {
info := clients[id] info := clients[id]
clientsJSON = append(clientsJSON, map[string]any{ clientsJSON = append(clientsJSON, map[string]interface{}{
"account_id": info.AccountID, "account_id": info.AccountID,
"service_count": info.ServiceCount, "service_count": info.ServiceCount,
"service_keys": info.ServiceKeys, "service_keys": info.ServiceKeys,
@@ -277,7 +270,7 @@ func (h *Handler) handleIndex(w http.ResponseWriter, _ *http.Request, wantJSON b
"age": time.Since(info.CreatedAt).Round(time.Second).String(), "age": time.Since(info.CreatedAt).Round(time.Second).String(),
}) })
} }
resp := map[string]any{ resp := map[string]interface{}{
"version": version.NetbirdVersion(), "version": version.NetbirdVersion(),
"uptime": time.Since(h.startTime).Round(time.Second).String(), "uptime": time.Since(h.startTime).Round(time.Second).String(),
"client_count": len(clients), "client_count": len(clients),
@@ -359,10 +352,10 @@ func (h *Handler) handleListClients(w http.ResponseWriter, _ *http.Request, want
if h.inbound != nil { if h.inbound != nil {
inboundAll = h.inbound.InboundListeners() inboundAll = h.inbound.InboundListeners()
} }
clientsJSON := make([]map[string]any, 0, len(clients)) clientsJSON := make([]map[string]interface{}, 0, len(clients))
for _, id := range sortedIDs { for _, id := range sortedIDs {
info := clients[id] info := clients[id]
row := map[string]any{ row := map[string]interface{}{
"account_id": info.AccountID, "account_id": info.AccountID,
"service_count": info.ServiceCount, "service_count": info.ServiceCount,
"service_keys": info.ServiceKeys, "service_keys": info.ServiceKeys,
@@ -375,7 +368,7 @@ func (h *Handler) handleListClients(w http.ResponseWriter, _ *http.Request, want
} }
clientsJSON = append(clientsJSON, row) clientsJSON = append(clientsJSON, row)
} }
resp := map[string]any{ resp := map[string]interface{}{
"uptime": time.Since(h.startTime).Round(time.Second).String(), "uptime": time.Since(h.startTime).Round(time.Second).String(),
"client_count": len(clients), "client_count": len(clients),
"clients": clientsJSON, "clients": clientsJSON,
@@ -465,7 +458,7 @@ func (h *Handler) handleClientStatus(w http.ResponseWriter, r *http.Request, acc
}) })
if wantJSON { if wantJSON {
resp := map[string]any{ resp := map[string]interface{}{
"account_id": accountID, "account_id": accountID,
"status": overview.FullDetailSummary(), "status": overview.FullDetailSummary(),
} }
@@ -564,20 +557,20 @@ func (h *Handler) handleClientTools(w http.ResponseWriter, _ *http.Request, acco
func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountID types.AccountID) { func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID) client, ok := h.provider.GetClient(accountID)
if !ok { if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"}) h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return return
} }
host := r.URL.Query().Get("host") host := r.URL.Query().Get("host")
portStr := r.URL.Query().Get("port") portStr := r.URL.Query().Get("port")
if host == "" || portStr == "" { if host == "" || portStr == "" {
h.writeJSON(w, map[string]any{"error": "host and port parameters required"}) h.writeJSON(w, map[string]interface{}{"error": "host and port parameters required"})
return return
} }
port, err := strconv.Atoi(portStr) port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 { if err != nil || port < 1 || port > 65535 {
h.writeJSON(w, map[string]any{"error": "invalid port"}) h.writeJSON(w, map[string]interface{}{"error": "invalid port"})
return return
} }
@@ -601,7 +594,7 @@ func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountI
conn, err := client.Dial(ctx, network, address) conn, err := client.Dial(ctx, network, address)
if err != nil { if err != nil {
h.writeJSON(w, map[string]any{ h.writeJSON(w, map[string]interface{}{
"success": false, "success": false,
"host": host, "host": host,
"port": port, "port": port,
@@ -616,38 +609,39 @@ func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountI
} }
latency := time.Since(start) latency := time.Since(start)
h.writeJSON(w, map[string]any{ resp := map[string]interface{}{
"success": true, "success": true,
"host": host, "host": host,
"port": port, "port": port,
"remote": remote, "remote": remote,
"latency_ms": latency.Milliseconds(), "latency_ms": latency.Milliseconds(),
"latency": formatDuration(latency), "latency": formatDuration(latency),
}) }
h.writeJSON(w, resp)
} }
func (h *Handler) handleLogLevel(w http.ResponseWriter, r *http.Request, accountID types.AccountID) { func (h *Handler) handleLogLevel(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID) client, ok := h.provider.GetClient(accountID)
if !ok { if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"}) h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return return
} }
level := r.URL.Query().Get("level") level := r.URL.Query().Get("level")
if level == "" { if level == "" {
h.writeJSON(w, map[string]any{"error": "level parameter required (trace, debug, info, warn, error)"}) h.writeJSON(w, map[string]interface{}{"error": "level parameter required (trace, debug, info, warn, error)"})
return return
} }
if err := client.SetLogLevel(level); err != nil { if err := client.SetLogLevel(level); err != nil {
h.writeJSON(w, map[string]any{ h.writeJSON(w, map[string]interface{}{
"success": false, "success": false,
"error": err.Error(), "error": err.Error(),
}) })
return return
} }
h.writeJSON(w, map[string]any{ h.writeJSON(w, map[string]interface{}{
"success": true, "success": true,
"level": level, "level": level,
}) })
@@ -658,7 +652,7 @@ const clientActionTimeout = 30 * time.Second
func (h *Handler) handleClientStart(w http.ResponseWriter, r *http.Request, accountID types.AccountID) { func (h *Handler) handleClientStart(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID) client, ok := h.provider.GetClient(accountID)
if !ok { if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"}) h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return return
} }
@@ -666,14 +660,14 @@ func (h *Handler) handleClientStart(w http.ResponseWriter, r *http.Request, acco
defer cancel() defer cancel()
if err := client.Start(ctx); err != nil { if err := client.Start(ctx); err != nil {
h.writeJSON(w, map[string]any{ h.writeJSON(w, map[string]interface{}{
"success": false, "success": false,
"error": err.Error(), "error": err.Error(),
}) })
return return
} }
h.writeJSON(w, map[string]any{ h.writeJSON(w, map[string]interface{}{
"success": true, "success": true,
"message": "client started", "message": "client started",
}) })
@@ -682,7 +676,7 @@ func (h *Handler) handleClientStart(w http.ResponseWriter, r *http.Request, acco
func (h *Handler) handleClientStop(w http.ResponseWriter, r *http.Request, accountID types.AccountID) { func (h *Handler) handleClientStop(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID) client, ok := h.provider.GetClient(accountID)
if !ok { if !ok {
h.writeJSON(w, map[string]any{"error": "client not found"}) h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return return
} }
@@ -690,125 +684,19 @@ func (h *Handler) handleClientStop(w http.ResponseWriter, r *http.Request, accou
defer cancel() defer cancel()
if err := client.Stop(ctx); err != nil { if err := client.Stop(ctx); err != nil {
h.writeJSON(w, map[string]any{ h.writeJSON(w, map[string]interface{}{
"success": false, "success": false,
"error": err.Error(), "error": err.Error(),
}) })
return return
} }
h.writeJSON(w, map[string]any{ h.writeJSON(w, map[string]interface{}{
"success": true, "success": true,
"message": "client stopped", "message": "client stopped",
}) })
} }
func (h *Handler) handlePerf(w http.ResponseWriter, r *http.Request) {
raw := r.URL.Query().Get("value")
if raw == "" {
http.Error(w, "value parameter is required", http.StatusBadRequest)
return
}
n, err := strconv.ParseUint(raw, 10, 32)
if err != nil {
http.Error(w, fmt.Sprintf("invalid value %q: %v", raw, err), http.StatusBadRequest)
return
}
capN := uint32(n)
applied := 0
failed := map[string]string{}
for accountID, client := range h.provider.ListClientsForStartup() {
if err := client.SetPerformance(nbembed.Performance{PreallocatedBuffersPerPool: &capN}); err != nil {
failed[string(accountID)] = err.Error()
continue
}
applied++
}
resp := map[string]any{
"success": true,
"value": capN,
"applied": applied,
}
if len(failed) > 0 {
resp["failed"] = failed
}
h.writeJSON(w, resp)
}
// handleRuntime returns cheap runtime and process stats. Safe to hit on a
// running proxy; does not read pprof profiles.
func (h *Handler) handleRuntime(w http.ResponseWriter, _ *http.Request) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
clients := h.provider.ListClientsForDebug()
started := 0
for _, c := range clients {
if c.HasClient {
started++
}
}
resp := map[string]any{
"uptime": time.Since(h.startTime).Round(time.Second).String(),
"goroutines": runtime.NumGoroutine(),
"num_cpu": runtime.NumCPU(),
"gomaxprocs": runtime.GOMAXPROCS(0),
"go_version": runtime.Version(),
"heap_alloc": m.HeapAlloc,
"heap_inuse": m.HeapInuse,
"heap_idle": m.HeapIdle,
"heap_released": m.HeapReleased,
"heap_sys": m.HeapSys,
"sys": m.Sys,
"live_objects": m.Mallocs - m.Frees,
"num_gc": m.NumGC,
"pause_total_ns": m.PauseTotalNs,
"clients": len(clients),
"started": started,
}
if proc := readProcStatus(); proc != nil {
resp["vm_rss"] = proc["VmRSS"]
resp["vm_size"] = proc["VmSize"]
resp["vm_data"] = proc["VmData"]
}
h.writeJSON(w, resp)
}
// readProcStatus parses /proc/self/status on Linux and returns size fields
// in bytes. Returns nil on non-Linux or read failure.
func readProcStatus() map[string]uint64 {
raw, err := os.ReadFile("/proc/self/status")
if err != nil {
return nil
}
out := map[string]uint64{}
for _, line := range strings.Split(string(raw), "\n") {
k, v, ok := strings.Cut(line, ":")
if !ok {
continue
}
if k != "VmRSS" && k != "VmSize" && k != "VmData" {
continue
}
fields := strings.Fields(v)
if len(fields) < 1 {
continue
}
n, err := strconv.ParseUint(fields[0], 10, 64)
if err != nil {
continue
}
// Values are reported in kB.
out[k] = n * 1024
}
return out
}
const maxCaptureDuration = 30 * time.Minute const maxCaptureDuration = 30 * time.Minute
// handleCapture streams a pcap or text packet capture for the given client. // handleCapture streams a pcap or text packet capture for the given client.
@@ -937,7 +825,7 @@ func (h *Handler) handleHealth(w http.ResponseWriter, r *http.Request, wantJSON
h.writeJSON(w, resp) h.writeJSON(w, resp)
} }
func (h *Handler) renderTemplate(w http.ResponseWriter, name string, data any) { func (h *Handler) renderTemplate(w http.ResponseWriter, name string, data interface{}) {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl := h.getTemplates() tmpl := h.getTemplates()
if tmpl == nil { if tmpl == nil {
@@ -950,7 +838,7 @@ func (h *Handler) renderTemplate(w http.ResponseWriter, name string, data any) {
} }
} }
func (h *Handler) writeJSON(w http.ResponseWriter, v any) { func (h *Handler) writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
enc.SetIndent("", " ") enc.SetIndent("", " ")

View File

@@ -131,7 +131,6 @@ type ClientConfig struct {
MgmtAddr string MgmtAddr string
WGPort uint16 WGPort uint16
PreSharedKey string PreSharedKey string
Performance embed.Performance
// BlockInbound mirrors embed.Options.BlockInbound. Set to true on the // BlockInbound mirrors embed.Options.BlockInbound. Set to true on the
// standalone proxy where the embedded client never accepts inbound; // standalone proxy where the embedded client never accepts inbound;
// set to false on the private/embedded proxy so the engine creates // set to false on the private/embedded proxy so the engine creates
@@ -307,7 +306,7 @@ func (n *NetBird) createClientEntry(ctx context.Context, accountID types.Account
ManagementURL: n.clientCfg.MgmtAddr, ManagementURL: n.clientCfg.MgmtAddr,
PrivateKey: privateKey.String(), PrivateKey: privateKey.String(),
LogLevel: log.WarnLevel.String(), LogLevel: log.WarnLevel.String(),
BlockInbound: n.clientCfg.BlockInbound, BlockInbound: n.clientCfg.BlockInbound,
// The embedded proxy peer must never be a stepping stone into // The embedded proxy peer must never be a stepping stone into
// the proxy host's LAN: it only exists to reach NetBird mesh // the proxy host's LAN: it only exists to reach NetBird mesh
// targets or, when direct_upstream is set, the host network // targets or, when direct_upstream is set, the host network
@@ -316,7 +315,6 @@ func (n *NetBird) createClientEntry(ctx context.Context, accountID types.Account
BlockLANAccess: true, BlockLANAccess: true,
WireguardPort: &wgPort, WireguardPort: &wgPort,
PreSharedKey: n.clientCfg.PreSharedKey, PreSharedKey: n.clientCfg.PreSharedKey,
Performance: n.clientCfg.Performance,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("create netbird client: %w", err) return nil, fmt.Errorf("create netbird client: %w", err)

View File

@@ -6,7 +6,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/embed"
"github.com/netbirdio/netbird/proxy/internal/acme" "github.com/netbirdio/netbird/proxy/internal/acme"
) )
@@ -90,10 +89,6 @@ type Config struct {
// PreSharedKey is the WireGuard pre-shared key used between the // PreSharedKey is the WireGuard pre-shared key used between the
// proxy's embedded clients and peers. // proxy's embedded clients and peers.
PreSharedKey string PreSharedKey string
// Performance configures the tunnel pool/batch sizes for every
// embedded client this proxy creates. Zero values fall back to
// upstream defaults.
Performance embed.Performance
// SupportsCustomPorts indicates whether the proxy can bind arbitrary // SupportsCustomPorts indicates whether the proxy can bind arbitrary
// ports for TCP/UDP/TLS services. // ports for TCP/UDP/TLS services.
@@ -153,7 +148,6 @@ func New(cfg Config) *Server {
WireguardPort: cfg.WireguardPort, WireguardPort: cfg.WireguardPort,
ProxyProtocol: cfg.ProxyProtocol, ProxyProtocol: cfg.ProxyProtocol,
PreSharedKey: cfg.PreSharedKey, PreSharedKey: cfg.PreSharedKey,
Performance: cfg.Performance,
SupportsCustomPorts: cfg.SupportsCustomPorts, SupportsCustomPorts: cfg.SupportsCustomPorts,
RequireSubdomain: cfg.RequireSubdomain, RequireSubdomain: cfg.RequireSubdomain,
Private: cfg.Private, Private: cfg.Private,

View File

@@ -41,7 +41,6 @@ import (
goproto "google.golang.org/protobuf/proto" goproto "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
"github.com/netbirdio/netbird/client/embed"
"github.com/netbirdio/netbird/proxy/internal/accesslog" "github.com/netbirdio/netbird/proxy/internal/accesslog"
"github.com/netbirdio/netbird/proxy/internal/acme" "github.com/netbirdio/netbird/proxy/internal/acme"
"github.com/netbirdio/netbird/proxy/internal/auth" "github.com/netbirdio/netbird/proxy/internal/auth"
@@ -186,9 +185,6 @@ type Server struct {
// single-account deployments; multiple accounts will fail to bind // single-account deployments; multiple accounts will fail to bind
// the same port. // the same port.
WireguardPort uint16 WireguardPort uint16
// Performance configures the tunnel pool/batch sizes for every
// embedded client this proxy spawns.
Performance embed.Performance
// ProxyProtocol enables PROXY protocol (v1/v2) on TCP listeners. // ProxyProtocol enables PROXY protocol (v1/v2) on TCP listeners.
// When enabled, the real client IP is extracted from the PROXY header // When enabled, the real client IP is extracted from the PROXY header
// sent by upstream L4 proxies that support PROXY protocol. // sent by upstream L4 proxies that support PROXY protocol.
@@ -337,8 +333,6 @@ func (s *Server) Start(ctx context.Context) error {
s.runCancel = runCancel s.runCancel = runCancel
s.initNetBirdClient() s.initNetBirdClient()
// Create health checker before the mapping worker so it can track
// management connectivity from the first stream connection.
s.healthChecker = health.NewChecker(s.Logger, s.netbird) s.healthChecker = health.NewChecker(s.Logger, s.netbird)
s.crowdsecRegistry = crowdsec.NewRegistry(s.CrowdSecAPIURL, s.CrowdSecAPIKey, log.NewEntry(s.Logger)) s.crowdsecRegistry = crowdsec.NewRegistry(s.CrowdSecAPIURL, s.CrowdSecAPIKey, log.NewEntry(s.Logger))
@@ -535,7 +529,6 @@ func (s *Server) initNetBirdClient() {
MgmtAddr: s.ManagementAddress, MgmtAddr: s.ManagementAddress,
WGPort: s.WireguardPort, WGPort: s.WireguardPort,
PreSharedKey: s.PreSharedKey, PreSharedKey: s.PreSharedKey,
Performance: s.Performance,
// On --private the embedded client serves per-account inbound // On --private the embedded client serves per-account inbound
// listeners and must apply management's ACL: keep BlockInbound off // listeners and must apply management's ACL: keep BlockInbound off
// so the engine creates the ACL manager. On the standalone proxy // so the engine creates the ACL manager. On the standalone proxy

View File

@@ -17,7 +17,7 @@ import (
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/management/server/integrations/integrated_validator/validator" "github.com/netbirdio/management-integrations/integrations"
ephemeral_manager "github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager" ephemeral_manager "github.com/netbirdio/netbird/management/internals/modules/peers/ephemeral/manager"
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller" "github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
@@ -103,7 +103,7 @@ func startManagement(t *testing.T) (*grpc.Server, net.Listener) {
t.Fatal(err) t.Fatal(err)
} }
ia, _ := validator.NewIntegratedValidator(ctx, peersManger, settingsManagerMock, eventStore, cacheStore) ia, _ := integrations.NewIntegratedValidator(ctx, peersManger, settingsManagerMock, eventStore, cacheStore)
metrics, err := telemetry.NewDefaultAppMetrics(ctx) metrics, err := telemetry.NewDefaultAppMetrics(ctx)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -2,19 +2,75 @@ package version
import ( import (
"regexp" "regexp"
"runtime/debug"
"strings"
v "github.com/hashicorp/go-version" v "github.com/hashicorp/go-version"
) )
// DevelopmentVersion is the value of NetbirdVersion() for non-release builds.
// Wire-format consumers (management server, dashboard) match against this
// string, so it must not change without coordinating those consumers.
const DevelopmentVersion = "development"
// will be replaced with the release version when using goreleaser // will be replaced with the release version when using goreleaser
var version = "development" var version = DevelopmentVersion
var ( var (
VersionRegexp = regexp.MustCompile("^" + v.VersionRegexpRaw + "$") VersionRegexp = regexp.MustCompile("^" + v.VersionRegexpRaw + "$")
SemverRegexp = regexp.MustCompile("^" + v.SemverRegexpRaw + "$") SemverRegexp = regexp.MustCompile("^" + v.SemverRegexpRaw + "$")
) )
// NetbirdVersion returns the Netbird version // NetbirdVersion returns the Netbird version. For non-release builds the
// value is the literal DevelopmentVersion constant; the VCS revision is
// exposed separately via NetbirdCommit so the wire format stays stable.
func NetbirdVersion() string { func NetbirdVersion() string {
return version return version
} }
// NetbirdCommit returns the VCS revision (truncated to 12 chars) of the
// build, with a "-dirty" suffix when the working tree was modified.
// Returns an empty string when no build info is embedded (e.g. release
// builds compiled by goreleaser without -buildvcs).
func NetbirdCommit() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return ""
}
var revision string
var modified bool
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
revision = s.Value
case "vcs.modified":
modified = s.Value == "true"
}
}
if revision == "" {
return ""
}
if len(revision) > 12 {
revision = revision[:12]
}
if modified {
revision += "-dirty"
}
return revision
}
// IsDevelopmentVersion reports whether the given version string identifies
// a non-release / development build. It is the single source of truth for
// "is this a dev build" checks across the codebase; use it instead of
// comparing against the "development" literal or ad-hoc substring checks.
//
// Matches the bare DevelopmentVersion constant as well as any future
// extension such as "development-<sha>" or "development-<sha>-dirty",
// while excluding tagged prereleases like "v0.31.1-dev".
func IsDevelopmentVersion(v string) bool {
return strings.HasPrefix(v, DevelopmentVersion)
}

26
version/version_test.go Normal file
View File

@@ -0,0 +1,26 @@
package version
import "testing"
func TestIsDevelopmentVersion(t *testing.T) {
tests := []struct {
version string
want bool
}{
{"development", true},
{"development-0823f3ff9ab1", true},
{"development-0823f3ff9ab1-dirty", true},
{"0.50.0", false},
{"v0.31.1-dev", false},
{"1.0.0-dev", false},
{"dev", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
if got := IsDevelopmentVersion(tt.version); got != tt.want {
t.Errorf("IsDevelopmentVersion(%q) = %v, want %v", tt.version, got, tt.want)
}
})
}
}