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
20 changed files with 132 additions and 410 deletions

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

@@ -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
} }

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

@@ -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

@@ -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

@@ -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

@@ -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)
}
})
}
}