Compare commits

..

1 Commits

Author SHA1 Message Date
Viktor Liu
417fa6e833 Change default interface name from wt0 to nb0 2025-07-21 17:58:56 +02:00
152 changed files with 1752 additions and 7968 deletions

View File

@@ -1,3 +0,0 @@
*
!client/netbird-entrypoint.sh
!netbird

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-22.04
outputs:
management: ${{ steps.filter.outputs.management }}
steps:
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -24,8 +24,8 @@ jobs:
id: filter
with:
filters: |
management:
- 'management/**'
management:
- 'management/**'
- name: Install Go
uses: actions/setup-go@v5
@@ -148,7 +148,7 @@ jobs:
test_client_on_docker:
name: "Client (Docker) / Unit"
needs: [ build-cache ]
needs: [build-cache]
runs-on: ubuntu-22.04
steps:
- name: Install Go
@@ -181,7 +181,6 @@ jobs:
env:
HOST_GOCACHE: ${{ steps.go-env.outputs.cache_dir }}
HOST_GOMODCACHE: ${{ steps.go-env.outputs.modcache_dir }}
CONTAINER: "true"
run: |
CONTAINER_GOCACHE="/root/.cache/go-build"
CONTAINER_GOMODCACHE="/go/pkg/mod"
@@ -199,7 +198,6 @@ jobs:
-e GOARCH=${GOARCH_TARGET} \
-e GOCACHE=${CONTAINER_GOCACHE} \
-e GOMODCACHE=${CONTAINER_GOMODCACHE} \
-e CONTAINER=${CONTAINER} \
golang:1.23-alpine \
sh -c ' \
apk update; apk add --no-cache \
@@ -213,11 +211,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- arch: "386"
raceFlag: ""
- arch: "amd64"
raceFlag: ""
arch: [ '386','amd64' ]
runs-on: ubuntu-22.04
steps:
- name: Install Go
@@ -257,9 +251,9 @@ jobs:
- name: Test
run: |
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
go test ${{ matrix.raceFlag }} \
go test \
-exec 'sudo' \
-timeout 10m ./relay/...
-timeout 10m ./signal/...
test_signal:
name: "Signal / Unit"

1
.gitignore vendored
View File

@@ -30,4 +30,3 @@ infrastructure_files/setup-*.env
.vscode
.DS_Store
vendor/
/netbird

View File

@@ -155,15 +155,13 @@ dockers:
goarch: amd64
use: buildx
dockerfile: client/Dockerfile
extra_files:
- client/netbird-entrypoint.sh
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
- "--label=org.opencontainers.image.title={{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
- "--label=org.opencontainers.image.source=https://github.com/netbirdio/{{.ProjectName}}"
- "--label=org.opencontainers.image.version={{.Version}}"
- "--label=maintainer=dev@netbird.io"
- image_templates:
- netbirdio/netbird:{{ .Version }}-arm64v8
@@ -173,8 +171,6 @@ dockers:
goarch: arm64
use: buildx
dockerfile: client/Dockerfile
extra_files:
- client/netbird-entrypoint.sh
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
@@ -192,8 +188,6 @@ dockers:
goarm: 6
use: buildx
dockerfile: client/Dockerfile
extra_files:
- client/netbird-entrypoint.sh
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"
@@ -211,8 +205,6 @@ dockers:
goarch: amd64
use: buildx
dockerfile: client/Dockerfile-rootless
extra_files:
- client/netbird-entrypoint.sh
build_flag_templates:
- "--platform=linux/amd64"
- "--label=org.opencontainers.image.created={{.Date}}"
@@ -229,8 +221,6 @@ dockers:
goarch: arm64
use: buildx
dockerfile: client/Dockerfile-rootless
extra_files:
- client/netbird-entrypoint.sh
build_flag_templates:
- "--platform=linux/arm64"
- "--label=org.opencontainers.image.created={{.Date}}"
@@ -248,8 +238,6 @@ dockers:
goarm: 6
use: buildx
dockerfile: client/Dockerfile-rootless
extra_files:
- client/netbird-entrypoint.sh
build_flag_templates:
- "--platform=linux/arm"
- "--label=org.opencontainers.image.created={{.Date}}"

View File

@@ -1,27 +1,9 @@
# build & run locally with:
# cd "$(git rev-parse --show-toplevel)"
# CGO_ENABLED=0 go build -o netbird ./client
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
FROM alpine:3.22.0
FROM alpine:3.21.3
# iproute2: busybox doesn't display ip rules properly
RUN apk add --no-cache \
bash \
ca-certificates \
ip6tables \
iproute2 \
iptables
ENV \
NETBIRD_BIN="/usr/local/bin/netbird" \
NB_LOG_FILE="console,/var/log/netbird/client.log" \
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
NB_ENTRYPOINT_LOGIN_TIMEOUT="1"
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables
ARG NETBIRD_BINARY=netbird
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
COPY ${NETBIRD_BINARY} /usr/local/bin/netbird
ENV NB_FOREGROUND_MODE=true
ENTRYPOINT [ "/usr/local/bin/netbird","up"]

View File

@@ -1,33 +1,18 @@
# build & run locally with:
# cd "$(git rev-parse --show-toplevel)"
# CGO_ENABLED=0 go build -o netbird ./client
# podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-client .
# podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
FROM alpine:3.21.0
FROM alpine:3.22.0
ARG NETBIRD_BINARY=netbird
COPY ${NETBIRD_BINARY} /usr/local/bin/netbird
RUN apk add --no-cache \
bash \
ca-certificates \
RUN apk add --no-cache ca-certificates \
&& adduser -D -h /var/lib/netbird netbird
WORKDIR /var/lib/netbird
USER netbird:netbird
ENV \
NETBIRD_BIN="/usr/local/bin/netbird" \
NB_USE_NETSTACK_MODE="true" \
NB_ENABLE_NETSTACK_LOCAL_FORWARDING="true" \
NB_CONFIG="/var/lib/netbird/config.json" \
NB_STATE_DIR="/var/lib/netbird" \
NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \
NB_LOG_FILE="console,/var/lib/netbird/client.log" \
NB_DISABLE_DNS="true" \
NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
NB_ENTRYPOINT_LOGIN_TIMEOUT="1"
ENV NB_FOREGROUND_MODE=true
ENV NB_USE_NETSTACK_MODE=true
ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true
ENV NB_CONFIG=config.json
ENV NB_DAEMON_ADDR=unix://netbird.sock
ENV NB_DISABLE_DNS=true
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
ARG NETBIRD_BINARY=netbird
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
ENTRYPOINT [ "/usr/local/bin/netbird", "up" ]

View File

@@ -13,7 +13,6 @@ import (
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
@@ -83,7 +82,7 @@ func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersi
// Run start the internal client. It is a blocker function
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener) error {
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
})
if err != nil {
@@ -118,7 +117,7 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
// In this case make no sense handle registration steps.
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error {
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
})
if err != nil {

View File

@@ -13,7 +13,6 @@ import (
"github.com/netbirdio/netbird/client/cmd"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/system"
)
@@ -38,17 +37,17 @@ type URLOpener interface {
// Auth can register or login new client
type Auth struct {
ctx context.Context
config *profilemanager.Config
config *internal.Config
cfgPath string
}
// NewAuth instantiate Auth struct and validate the management URL
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
inputCfg := profilemanager.ConfigInput{
inputCfg := internal.ConfigInput{
ManagementURL: mgmURL,
}
cfg, err := profilemanager.CreateInMemoryConfig(inputCfg)
cfg, err := internal.CreateInMemoryConfig(inputCfg)
if err != nil {
return nil, err
}
@@ -61,7 +60,7 @@ func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
}
// NewAuthWithConfig instantiate Auth based on existing config
func NewAuthWithConfig(ctx context.Context, config *profilemanager.Config) *Auth {
func NewAuthWithConfig(ctx context.Context, config *internal.Config) *Auth {
return &Auth{
ctx: ctx,
config: config,
@@ -111,7 +110,7 @@ func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
return false, fmt.Errorf("backoff cycle failed: %v", err)
}
err = profilemanager.WriteOutConfig(a.cfgPath, a.config)
err = internal.WriteOutConfig(a.cfgPath, a.config)
return true, err
}
@@ -143,7 +142,7 @@ func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string
return fmt.Errorf("backoff cycle failed: %v", err)
}
return profilemanager.WriteOutConfig(a.cfgPath, a.config)
return internal.WriteOutConfig(a.cfgPath, a.config)
}
// Login try register the client on the server

View File

@@ -1,17 +1,17 @@
package android
import (
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal"
)
// Preferences exports a subset of the internal config for gomobile
type Preferences struct {
configInput profilemanager.ConfigInput
configInput internal.ConfigInput
}
// NewPreferences creates a new Preferences instance
func NewPreferences(configPath string) *Preferences {
ci := profilemanager.ConfigInput{
ci := internal.ConfigInput{
ConfigPath: configPath,
}
return &Preferences{ci}
@@ -23,7 +23,7 @@ func (p *Preferences) GetManagementURL() (string, error) {
return p.configInput.ManagementURL, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
@@ -41,7 +41,7 @@ func (p *Preferences) GetAdminURL() (string, error) {
return p.configInput.AdminURL, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
@@ -59,7 +59,7 @@ func (p *Preferences) GetPreSharedKey() (string, error) {
return *p.configInput.PreSharedKey, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
@@ -82,7 +82,7 @@ func (p *Preferences) GetRosenpassEnabled() (bool, error) {
return *p.configInput.RosenpassEnabled, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -100,7 +100,7 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) {
return *p.configInput.RosenpassPermissive, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -113,7 +113,7 @@ func (p *Preferences) GetDisableClientRoutes() (bool, error) {
return *p.configInput.DisableClientRoutes, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -131,7 +131,7 @@ func (p *Preferences) GetDisableServerRoutes() (bool, error) {
return *p.configInput.DisableServerRoutes, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -149,7 +149,7 @@ func (p *Preferences) GetDisableDNS() (bool, error) {
return *p.configInput.DisableDNS, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -167,7 +167,7 @@ func (p *Preferences) GetDisableFirewall() (bool, error) {
return *p.configInput.DisableFirewall, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -185,7 +185,7 @@ func (p *Preferences) GetServerSSHAllowed() (bool, error) {
return *p.configInput.ServerSSHAllowed, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -207,7 +207,7 @@ func (p *Preferences) GetBlockInbound() (bool, error) {
return *p.configInput.BlockInbound, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -221,6 +221,6 @@ func (p *Preferences) SetBlockInbound(block bool) {
// Commit writes out the changes to the config file
func (p *Preferences) Commit() error {
_, err := profilemanager.UpdateOrCreateConfig(p.configInput)
_, err := internal.UpdateOrCreateConfig(p.configInput)
return err
}

View File

@@ -4,7 +4,7 @@ import (
"path/filepath"
"testing"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal"
)
func TestPreferences_DefaultValues(t *testing.T) {
@@ -15,7 +15,7 @@ func TestPreferences_DefaultValues(t *testing.T) {
t.Fatalf("failed to read default value: %s", err)
}
if defaultVar != profilemanager.DefaultAdminURL {
if defaultVar != internal.DefaultAdminURL {
t.Errorf("invalid default admin url: %s", defaultVar)
}
@@ -24,7 +24,7 @@ func TestPreferences_DefaultValues(t *testing.T) {
t.Fatalf("failed to read default management URL: %s", err)
}
if defaultVar != profilemanager.DefaultManagementURL {
if defaultVar != internal.DefaultManagementURL {
t.Errorf("invalid default management url: %s", defaultVar)
}

View File

@@ -13,7 +13,6 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/debug"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/server"
nbstatus "github.com/netbirdio/netbird/client/status"
@@ -308,7 +307,7 @@ func getStatusOutput(cmd *cobra.Command, anon bool) string {
cmd.PrintErrf("Failed to get status: %v\n", err)
} else {
statusOutputString = nbstatus.ParseToFullDetailSummary(
nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, "", ""),
nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, ""),
)
}
return statusOutputString
@@ -356,7 +355,7 @@ func formatDuration(d time.Duration) string {
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
}
func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) {
func generateDebugBundle(config *internal.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) {
var networkMap *mgmProto.NetworkMap
var err error

View File

@@ -12,12 +12,11 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
)
func SetupDebugHandler(
ctx context.Context,
config *profilemanager.Config,
config *internal.Config,
recorder *peer.Status,
connectClient *internal.ConnectClient,
logFilePath string,

View File

@@ -12,7 +12,6 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
)
const (
@@ -29,7 +28,7 @@ const (
// $evt.Close()
func SetupDebugHandler(
ctx context.Context,
config *profilemanager.Config,
config *internal.Config,
recorder *peer.Status,
connectClient *internal.ConnectClient,
logFilePath string,
@@ -84,7 +83,7 @@ func SetupDebugHandler(
func waitForEvent(
ctx context.Context,
config *profilemanager.Config,
config *internal.Config,
recorder *peer.Status,
connectClient *internal.ConnectClient,
logFilePath string,

View File

@@ -20,7 +20,7 @@ var downCmd = &cobra.Command{
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, util.LogConsole)
err := util.InitLog(logLevel, "console")
if err != nil {
log.Errorf("failed initializing log %v", err)
return err

View File

@@ -4,12 +4,10 @@ import (
"context"
"fmt"
"os"
"os/user"
"runtime"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
@@ -17,7 +15,6 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/auth"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/util"
@@ -25,16 +22,19 @@ import (
func init() {
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
loginCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
loginCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
}
var loginCmd = &cobra.Command{
Use: "login",
Short: "login to the Netbird Management Service (first run)",
RunE: func(cmd *cobra.Command, args []string) error {
if err := setEnvAndFlags(cmd); err != nil {
return fmt.Errorf("set env and flags: %v", err)
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx := internal.CtxInitState(context.Background())
@@ -43,17 +43,6 @@ var loginCmd = &cobra.Command{
// nolint
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
}
username, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}
pm := profilemanager.NewProfileManager()
activeProf, err := getActiveProfile(cmd.Context(), pm, profileName, username.Username)
if err != nil {
return fmt.Errorf("get active profile: %v", err)
}
providedSetupKey, err := getSetupKey()
if err != nil {
@@ -61,15 +50,97 @@ var loginCmd = &cobra.Command{
}
// workaround to run without service
if util.FindFirstLogPath(logFiles) == "" {
if err := doForegroundLogin(ctx, cmd, providedSetupKey, activeProf); err != nil {
if logFile == "console" {
err = handleRebrand(cmd)
if err != nil {
return err
}
// update host's static platform and system information
system.UpdateStaticInfo()
ic := internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
}
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
ic.PreSharedKey = &preSharedKey
}
config, err := internal.UpdateOrCreateConfig(ic)
if err != nil {
return fmt.Errorf("get config file: %v", err)
}
config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
err = foregroundLogin(ctx, cmd, config, providedSetupKey)
if err != nil {
return fmt.Errorf("foreground login failed: %v", err)
}
cmd.Println("Logging successfully")
return nil
}
if err := doDaemonLogin(ctx, cmd, providedSetupKey, activeProf, username.Username, pm); err != nil {
return fmt.Errorf("daemon login failed: %v", err)
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
client := proto.NewDaemonServiceClient(conn)
var dnsLabelsReq []string
if dnsLabelsValidated != nil {
dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
}
loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey,
ManagementUrl: managementURL,
IsUnixDesktopClient: isUnixRunningDesktop(),
Hostname: hostName,
DnsLabels: dnsLabelsReq,
}
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
loginRequest.OptionalPreSharedKey = &preSharedKey
}
var loginErr error
var loginResp *proto.LoginResponse
err = WithBackOff(func() error {
var backOffErr error
loginResp, backOffErr = client.Login(ctx, &loginRequest)
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
s.Code() == codes.PermissionDenied ||
s.Code() == codes.NotFound ||
s.Code() == codes.Unimplemented) {
loginErr = backOffErr
return nil
}
return backOffErr
})
if err != nil {
return fmt.Errorf("login backoff cycle failed: %v", err)
}
if loginErr != nil {
return fmt.Errorf("login failed: %v", loginErr)
}
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
if err != nil {
return fmt.Errorf("waiting sso login failed with: %v", err)
}
}
cmd.Println("Logging successfully")
@@ -78,196 +149,7 @@ var loginCmd = &cobra.Command{
},
}
func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey string, activeProf *profilemanager.Profile, username string, pm *profilemanager.ProfileManager) error {
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
client := proto.NewDaemonServiceClient(conn)
var dnsLabelsReq []string
if dnsLabelsValidated != nil {
dnsLabelsReq = dnsLabelsValidated.ToSafeStringList()
}
loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey,
ManagementUrl: managementURL,
IsUnixDesktopClient: isUnixRunningDesktop(),
Hostname: hostName,
DnsLabels: dnsLabelsReq,
ProfileName: &activeProf.Name,
Username: &username,
}
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
loginRequest.OptionalPreSharedKey = &preSharedKey
}
var loginErr error
var loginResp *proto.LoginResponse
err = WithBackOff(func() error {
var backOffErr error
loginResp, backOffErr = client.Login(ctx, &loginRequest)
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
s.Code() == codes.PermissionDenied ||
s.Code() == codes.NotFound ||
s.Code() == codes.Unimplemented) {
loginErr = backOffErr
return nil
}
return backOffErr
})
if err != nil {
return fmt.Errorf("login backoff cycle failed: %v", err)
}
if loginErr != nil {
return fmt.Errorf("login failed: %v", loginErr)
}
if loginResp.NeedsSSOLogin {
if err := handleSSOLogin(ctx, cmd, loginResp, client, pm); err != nil {
return fmt.Errorf("sso login failed: %v", err)
}
}
return nil
}
func getActiveProfile(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) (*profilemanager.Profile, error) {
// switch profile if provided
if profileName != "" {
if err := switchProfileOnDaemon(ctx, pm, profileName, username); err != nil {
return nil, fmt.Errorf("switch profile: %v", err)
}
}
activeProf, err := pm.GetActiveProfile()
if err != nil {
return nil, fmt.Errorf("get active profile: %v", err)
}
if activeProf == nil {
return nil, fmt.Errorf("active profile not found, please run 'netbird profile create' first")
}
return activeProf, nil
}
func switchProfileOnDaemon(ctx context.Context, pm *profilemanager.ProfileManager, profileName string, username string) error {
err := switchProfile(context.Background(), profileName, username)
if err != nil {
return fmt.Errorf("switch profile on daemon: %v", err)
}
err = pm.SwitchProfile(profileName)
if err != nil {
return fmt.Errorf("switch profile: %v", err)
}
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
log.Errorf("failed to connect to service CLI interface %v", err)
return err
}
defer conn.Close()
client := proto.NewDaemonServiceClient(conn)
status, err := client.Status(ctx, &proto.StatusRequest{})
if err != nil {
return fmt.Errorf("unable to get daemon status: %v", err)
}
if status.Status == string(internal.StatusConnected) {
if _, err := client.Down(ctx, &proto.DownRequest{}); err != nil {
log.Errorf("call service down method: %v", err)
return err
}
}
return nil
}
func switchProfile(ctx context.Context, profileName string, username string) error {
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("failed to connect to daemon error: %v\n"+
"If the daemon is not running please run: "+
"\nnetbird service install \nnetbird service start\n", err)
}
defer conn.Close()
client := proto.NewDaemonServiceClient(conn)
_, err = client.SwitchProfile(ctx, &proto.SwitchProfileRequest{
ProfileName: &profileName,
Username: &username,
})
if err != nil {
return fmt.Errorf("switch profile failed: %v", err)
}
return nil
}
func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string, activeProf *profilemanager.Profile) error {
err := handleRebrand(cmd)
if err != nil {
return err
}
// update host's static platform and system information
system.UpdateStaticInfo()
configFilePath, err := activeProf.FilePath()
if err != nil {
return fmt.Errorf("get active profile file path: %v", err)
}
config, err := profilemanager.ReadConfig(configFilePath)
if err != nil {
return fmt.Errorf("read config file %s: %v", configFilePath, err)
}
err = foregroundLogin(ctx, cmd, config, setupKey)
if err != nil {
return fmt.Errorf("foreground login failed: %v", err)
}
cmd.Println("Logging successfully")
return nil
}
func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.LoginResponse, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager) error {
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
resp, err := client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
if err != nil {
return fmt.Errorf("waiting sso login failed with: %v", err)
}
if resp.Email != "" {
err = pm.SetActiveProfileState(&profilemanager.ProfileState{
Email: resp.Email,
})
if err != nil {
log.Warnf("failed to set active profile email: %v", err)
}
}
return nil
}
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey string) error {
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.Config, setupKey string) error {
needsLogin := false
err := WithBackOff(func() error {
@@ -313,7 +195,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profileman
return nil
}
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config) (*auth.TokenInfo, error) {
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop())
if err != nil {
return nil, err
@@ -369,16 +251,3 @@ func isUnixRunningDesktop() bool {
}
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
}
func setEnvAndFlags(cmd *cobra.Command) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
return nil
}

View File

@@ -2,11 +2,11 @@ package cmd
import (
"fmt"
"os/user"
"strings"
"testing"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/util"
)
@@ -14,41 +14,40 @@ func TestLogin(t *testing.T) {
mgmAddr := startTestingServices(t)
tempDir := t.TempDir()
currUser, err := user.Current()
if err != nil {
t.Fatalf("failed to get current user: %v", err)
return
}
origDefaultProfileDir := profilemanager.DefaultConfigPathDir
origActiveProfileStatePath := profilemanager.ActiveProfileStatePath
profilemanager.DefaultConfigPathDir = tempDir
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
sm := profilemanager.ServiceManager{}
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: "default",
Username: currUser.Username,
})
if err != nil {
t.Fatalf("failed to set active profile state: %v", err)
}
t.Cleanup(func() {
profilemanager.DefaultConfigPathDir = origDefaultProfileDir
profilemanager.ActiveProfileStatePath = origActiveProfileStatePath
})
confPath := tempDir + "/config.json"
mgmtURL := fmt.Sprintf("http://%s", mgmAddr)
rootCmd.SetArgs([]string{
"login",
"--config",
confPath,
"--log-file",
util.LogConsole,
"console",
"--setup-key",
strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"),
"--management-url",
mgmtURL,
})
// TODO(hakan): fix this test
_ = rootCmd.Execute()
err := rootCmd.Execute()
if err != nil {
t.Fatal(err)
}
// validate generated config
actualConf := &internal.Config{}
_, err = util.ReadJson(confPath, actualConf)
if err != nil {
t.Errorf("expected proper config file written, got broken %v", err)
}
if actualConf.ManagementURL.String() != mgmtURL {
t.Errorf("expected management URL %s got %s", mgmtURL, actualConf.ManagementURL.String())
}
if actualConf.WgIface != iface.WgInterfaceDefault {
t.Errorf("expected WgIfaceName %s got %s", iface.WgInterfaceDefault, actualConf.WgIface)
}
if len(actualConf.PrivateKey) == 0 {
t.Errorf("expected non empty Private key, got empty")
}
}

View File

@@ -1,236 +0,0 @@
package cmd
import (
"context"
"fmt"
"time"
"os/user"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/util"
)
var profileCmd = &cobra.Command{
Use: "profile",
Short: "manage Netbird profiles",
Long: `Manage Netbird profiles, allowing you to list, switch, and remove profiles.`,
}
var profileListCmd = &cobra.Command{
Use: "list",
Short: "list all profiles",
Long: `List all available profiles in the Netbird client.`,
RunE: listProfilesFunc,
}
var profileAddCmd = &cobra.Command{
Use: "add <profile_name>",
Short: "add a new profile",
Long: `Add a new profile to the Netbird client. The profile name must be unique.`,
Args: cobra.ExactArgs(1),
RunE: addProfileFunc,
}
var profileRemoveCmd = &cobra.Command{
Use: "remove <profile_name>",
Short: "remove a profile",
Long: `Remove a profile from the Netbird client. The profile must not be active.`,
Args: cobra.ExactArgs(1),
RunE: removeProfileFunc,
}
var profileSelectCmd = &cobra.Command{
Use: "select <profile_name>",
Short: "select a profile",
Long: `Select a profile to be the active profile in the Netbird client. The profile must exist.`,
Args: cobra.ExactArgs(1),
RunE: selectProfileFunc,
}
func setupCmd(cmd *cobra.Command) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(cmd)
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console")
if err != nil {
return err
}
return nil
}
func listProfilesFunc(cmd *cobra.Command, _ []string) error {
if err := setupCmd(cmd); err != nil {
return err
}
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
if err != nil {
return fmt.Errorf("connect to service CLI interface: %w", err)
}
defer conn.Close()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
daemonClient := proto.NewDaemonServiceClient(conn)
profiles, err := daemonClient.ListProfiles(cmd.Context(), &proto.ListProfilesRequest{
Username: currUser.Username,
})
if err != nil {
return err
}
// list profiles, add a tick if the profile is active
cmd.Println("Found", len(profiles.Profiles), "profiles:")
for _, profile := range profiles.Profiles {
// use a cross to indicate the passive profiles
activeMarker := "✗"
if profile.IsActive {
activeMarker = "✓"
}
cmd.Println(activeMarker, profile.Name)
}
return nil
}
func addProfileFunc(cmd *cobra.Command, args []string) error {
if err := setupCmd(cmd); err != nil {
return err
}
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
if err != nil {
return fmt.Errorf("connect to service CLI interface: %w", err)
}
defer conn.Close()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
daemonClient := proto.NewDaemonServiceClient(conn)
profileName := args[0]
_, err = daemonClient.AddProfile(cmd.Context(), &proto.AddProfileRequest{
ProfileName: profileName,
Username: currUser.Username,
})
if err != nil {
return err
}
cmd.Println("Profile added successfully:", profileName)
return nil
}
func removeProfileFunc(cmd *cobra.Command, args []string) error {
if err := setupCmd(cmd); err != nil {
return err
}
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
if err != nil {
return fmt.Errorf("connect to service CLI interface: %w", err)
}
defer conn.Close()
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
daemonClient := proto.NewDaemonServiceClient(conn)
profileName := args[0]
_, err = daemonClient.RemoveProfile(cmd.Context(), &proto.RemoveProfileRequest{
ProfileName: profileName,
Username: currUser.Username,
})
if err != nil {
return err
}
cmd.Println("Profile removed successfully:", profileName)
return nil
}
func selectProfileFunc(cmd *cobra.Command, args []string) error {
if err := setupCmd(cmd); err != nil {
return err
}
profileManager := profilemanager.NewProfileManager()
profileName := args[0]
currUser, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*7)
defer cancel()
conn, err := DialClientGRPCServer(ctx, daemonAddr)
if err != nil {
return fmt.Errorf("connect to service CLI interface: %w", err)
}
defer conn.Close()
daemonClient := proto.NewDaemonServiceClient(conn)
profiles, err := daemonClient.ListProfiles(ctx, &proto.ListProfilesRequest{
Username: currUser.Username,
})
if err != nil {
return fmt.Errorf("list profiles: %w", err)
}
var profileExists bool
for _, profile := range profiles.Profiles {
if profile.Name == profileName {
profileExists = true
break
}
}
if !profileExists {
return fmt.Errorf("profile %s does not exist", profileName)
}
if err := switchProfile(cmd.Context(), profileName, currUser.Username); err != nil {
return err
}
err = profileManager.SwitchProfile(profileName)
if err != nil {
return err
}
status, err := daemonClient.Status(ctx, &proto.StatusRequest{})
if err != nil {
return fmt.Errorf("get service status: %w", err)
}
if status.Status == string(internal.StatusConnected) {
if _, err := daemonClient.Down(ctx, &proto.DownRequest{}); err != nil {
return fmt.Errorf("call service down method: %w", err)
}
}
cmd.Println("Profile switched successfully to:", profileName)
return nil
}

View File

@@ -10,7 +10,6 @@ import (
"os/signal"
"path"
"runtime"
"slices"
"strings"
"syscall"
"time"
@@ -22,7 +21,7 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal"
)
const (
@@ -42,6 +41,7 @@ const (
)
var (
configPath string
defaultConfigPathDir string
defaultConfigPath string
oldDefaultConfigPathDir string
@@ -51,7 +51,7 @@ var (
defaultLogFile string
oldDefaultLogFileDir string
oldDefaultLogFile string
logFiles []string
logFile string
daemonAddr string
managementURL string
adminURL string
@@ -67,12 +67,12 @@ var (
interfaceName string
wireguardPort uint16
networkMonitor bool
serviceName string
autoConnectDisabled bool
extraIFaceBlackList []string
anonymizeFlag bool
dnsRouteInterval time.Duration
lazyConnEnabled bool
profilesDisabled bool
rootCmd = &cobra.Command{
Use: "netbird",
@@ -116,19 +116,26 @@ func init() {
defaultDaemonAddr = "tcp://127.0.0.1:41731"
}
defaultServiceName := "netbird"
if runtime.GOOS == "windows" {
defaultServiceName = "Netbird"
}
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultManagementURL))
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.DefaultAdminURL))
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
rootCmd.PersistentFlags().StringSliceVar(&logFiles, "log-file", []string{defaultLogFile}, "sets Netbird log paths written to simultaneously. If `console` is specified the log will be output to stdout. If `syslog` is specified the log will be sent to syslog daemon. You can pass the flag multiple times or separate entries by `,` character")
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout. If syslog is specified the log will be sent to syslog daemon.")
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
rootCmd.PersistentFlags().StringVar(&setupKeyPath, "setup-key-file", "", "The path to a setup key obtained from the Management Service Dashboard (used to register peer) This is ignored if the setup-key flag is provided.")
rootCmd.MarkFlagsMutuallyExclusive("setup-key", "setup-key-file")
rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output")
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "(DEPRECATED) Netbird config file location")
rootCmd.AddCommand(serviceCmd)
rootCmd.AddCommand(upCmd)
rootCmd.AddCommand(downCmd)
rootCmd.AddCommand(statusCmd)
@@ -138,7 +145,9 @@ func init() {
rootCmd.AddCommand(networksCMD)
rootCmd.AddCommand(forwardingRulesCmd)
rootCmd.AddCommand(debugCmd)
rootCmd.AddCommand(profileCmd)
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
networksCMD.AddCommand(routesListCmd)
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
@@ -151,12 +160,6 @@ func init() {
debugCmd.AddCommand(forCmd)
debugCmd.AddCommand(persistenceCmd)
// profile commands
profileCmd.AddCommand(profileListCmd)
profileCmd.AddCommand(profileAddCmd)
profileCmd.AddCommand(profileRemoveCmd)
profileCmd.AddCommand(profileSelectCmd)
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
`Sets external IPs maps between local addresses and interfaces.`+
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
@@ -183,13 +186,14 @@ func SetupCloseHandler(ctx context.Context, cancel context.CancelFunc) {
termCh := make(chan os.Signal, 1)
signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() {
defer cancel()
done := ctx.Done()
select {
case <-ctx.Done():
case <-done:
case <-termCh:
}
log.Info("shutdown signal received")
cancel()
}()
}
@@ -273,7 +277,7 @@ func getSetupKeyFromFile(setupKeyPath string) (string, error) {
func handleRebrand(cmd *cobra.Command) error {
var err error
if slices.Contains(logFiles, defaultLogFile) {
if logFile == defaultLogFile {
if migrateToNetbird(oldDefaultLogFile, defaultLogFile) {
cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir)
err = cpDir(oldDefaultLogFileDir, defaultLogFileDir)
@@ -282,14 +286,15 @@ func handleRebrand(cmd *cobra.Command) error {
}
}
}
if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) {
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir)
err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir)
if err != nil {
return err
if configPath == defaultConfigPath {
if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) {
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir)
err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,15 +1,12 @@
//go:build !ios && !android
package cmd
import (
"context"
"fmt"
"runtime"
"strings"
"sync"
"github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc"
@@ -17,16 +14,6 @@ import (
"github.com/netbirdio/netbird/client/server"
)
var serviceCmd = &cobra.Command{
Use: "service",
Short: "manages Netbird service",
}
var (
serviceName string
serviceEnvVars []string
)
type program struct {
ctx context.Context
cancel context.CancelFunc
@@ -35,32 +22,12 @@ type program struct {
serverInstanceMu sync.Mutex
}
func init() {
defaultServiceName := "netbird"
if runtime.GOOS == "windows" {
defaultServiceName = "Netbird"
}
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd)
serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile.")
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
serviceEnvDesc := `Sets extra environment variables for the service. ` +
`You can specify a comma-separated list of KEY=VALUE pairs. ` +
`E.g. --service-env LOG_LEVEL=debug,CUSTOM_VAR=value`
installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)
reconfigureCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)
rootCmd.AddCommand(serviceCmd)
}
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
ctx = internal.CtxInitState(ctx)
return &program{ctx: ctx, cancel: cancel}
}
func newSVCConfig() (*service.Config, error) {
func newSVCConfig() *service.Config {
config := &service.Config{
Name: serviceName,
DisplayName: "Netbird",
@@ -69,47 +36,23 @@ func newSVCConfig() (*service.Config, error) {
EnvVars: make(map[string]string),
}
if len(serviceEnvVars) > 0 {
extraEnvs, err := parseServiceEnvVars(serviceEnvVars)
if err != nil {
return nil, fmt.Errorf("parse service environment variables: %w", err)
}
config.EnvVars = extraEnvs
}
if runtime.GOOS == "linux" {
config.EnvVars["SYSTEMD_UNIT"] = serviceName
}
return config, nil
return config
}
func newSVC(prg *program, conf *service.Config) (service.Service, error) {
return service.New(prg, conf)
}
func parseServiceEnvVars(envVars []string) (map[string]string, error) {
envMap := make(map[string]string)
for _, env := range envVars {
if env == "" {
continue
}
parts := strings.SplitN(env, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid environment variable format: %s (expected KEY=VALUE)", env)
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
if key == "" {
return nil, fmt.Errorf("empty environment variable key in: %s", env)
}
envMap[key] = value
s, err := service.New(prg, conf)
if err != nil {
log.Fatal(err)
return nil, err
}
return envMap, nil
return s, nil
}
var serviceCmd = &cobra.Command{
Use: "service",
Short: "manages Netbird service",
}

View File

@@ -1,5 +1,3 @@
//go:build !ios && !android
package cmd
import (
@@ -49,19 +47,20 @@ func (p *program) Start(svc service.Service) error {
listen, err := net.Listen(split[0], split[1])
if err != nil {
return fmt.Errorf("listen daemon interface: %w", err)
return fmt.Errorf("failed to listen daemon interface: %w", err)
}
go func() {
defer listen.Close()
if split[0] == "unix" {
if err := os.Chmod(split[1], 0666); err != nil {
err = os.Chmod(split[1], 0666)
if err != nil {
log.Errorf("failed setting daemon permissions: %v", split[1])
return
}
}
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), profilesDisabled)
serverInstance := server.New(p.ctx, configPath, logFile)
if err := serverInstance.Start(); err != nil {
log.Fatalf("failed to start daemon: %v", err)
}
@@ -101,49 +100,37 @@ func (p *program) Stop(srv service.Service) error {
return nil
}
// Common setup for service control commands
func setupServiceControlCommand(cmd *cobra.Command, ctx context.Context, cancel context.CancelFunc) (service.Service, error) {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(serviceCmd)
cmd.SetOut(cmd.OutOrStdout())
if err := handleRebrand(cmd); err != nil {
return nil, err
}
if err := util.InitLog(logLevel, logFiles...); err != nil {
return nil, fmt.Errorf("init log: %w", err)
}
cfg, err := newSVCConfig()
if err != nil {
return nil, fmt.Errorf("create service config: %w", err)
}
s, err := newSVC(newProgram(ctx, cancel), cfg)
if err != nil {
return nil, err
}
return s, nil
}
var runCmd = &cobra.Command{
Use: "run",
Short: "runs Netbird as service",
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
SetFlagsFromEnvVars(rootCmd)
SetupCloseHandler(ctx, cancel)
SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
cmd.SetOut(cmd.OutOrStdout())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
err := handleRebrand(cmd)
if err != nil {
return err
}
return s.Run()
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
SetupCloseHandler(ctx, cancel)
SetupDebugHandler(ctx, nil, nil, nil, logFile)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Run()
if err != nil {
return err
}
return nil
},
}
@@ -151,14 +138,31 @@ var startCmd = &cobra.Command{
Use: "start",
Short: "starts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
if err := s.Start(); err != nil {
return fmt.Errorf("start service: %w", err)
err = util.InitLog(logLevel, logFile)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
cmd.PrintErrln(err)
return err
}
err = s.Start()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Netbird service has been started")
return nil
@@ -169,14 +173,29 @@ var stopCmd = &cobra.Command{
Use: "stop",
Short: "stops Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
if err := s.Stop(); err != nil {
return fmt.Errorf("stop service: %w", err)
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Stop()
if err != nil {
return err
}
cmd.Println("Netbird service has been stopped")
return nil
@@ -187,48 +206,31 @@ var restartCmd = &cobra.Command{
Use: "restart",
Short: "restarts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
if err := s.Restart(); err != nil {
return fmt.Errorf("restart service: %w", err)
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context())
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
err = s.Restart()
if err != nil {
return err
}
cmd.Println("Netbird service has been restarted")
return nil
},
}
var svcStatusCmd = &cobra.Command{
Use: "status",
Short: "shows Netbird service status",
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithCancel(cmd.Context())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
if err != nil {
return err
}
status, err := s.Status()
if err != nil {
return fmt.Errorf("get service status: %w", err)
}
var statusText string
switch status {
case service.StatusRunning:
statusText = "Running"
case service.StatusStopped:
statusText = "Stopped"
case service.StatusUnknown:
statusText = "Unknown"
default:
statusText = fmt.Sprintf("Unknown (%d)", status)
}
cmd.Printf("Netbird service status: %s\n", statusText)
return nil
},
}

View File

@@ -1,121 +1,87 @@
//go:build !ios && !android
package cmd
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/kardianos/service"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/util"
)
var ErrGetServiceStatus = fmt.Errorf("failed to get service status")
// Common service command setup
func setupServiceCommand(cmd *cobra.Command) error {
SetFlagsFromEnvVars(rootCmd)
SetFlagsFromEnvVars(serviceCmd)
cmd.SetOut(cmd.OutOrStdout())
return handleRebrand(cmd)
}
// Build service arguments for install/reconfigure
func buildServiceArguments() []string {
args := []string{
"service",
"run",
"--log-level",
logLevel,
"--daemon-addr",
daemonAddr,
}
if managementURL != "" {
args = append(args, "--management-url", managementURL)
}
for _, logFile := range logFiles {
args = append(args, "--log-file", logFile)
}
return args
}
// Configure platform-specific service settings
func configurePlatformSpecificSettings(svcConfig *service.Config) error {
if runtime.GOOS == "linux" {
// Respected only by systemd systems
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
if logFile := util.FindFirstLogPath(logFiles); logFile != "" {
setStdLogPath := true
dir := filepath.Dir(logFile)
if _, err := os.Stat(dir); err != nil {
if err = os.MkdirAll(dir, 0750); err != nil {
setStdLogPath = false
}
}
if setStdLogPath {
svcConfig.Option["LogOutput"] = true
svcConfig.Option["LogDirectory"] = dir
}
}
}
if runtime.GOOS == "windows" {
svcConfig.Option["OnFailure"] = "restart"
}
return nil
}
// Create fully configured service config for install/reconfigure
func createServiceConfigForInstall() (*service.Config, error) {
svcConfig, err := newSVCConfig()
if err != nil {
return nil, fmt.Errorf("create service config: %w", err)
}
svcConfig.Arguments = buildServiceArguments()
if err = configurePlatformSpecificSettings(svcConfig); err != nil {
return nil, fmt.Errorf("configure platform-specific settings: %w", err)
}
return svcConfig, nil
}
var installCmd = &cobra.Command{
Use: "install",
Short: "installs Netbird service",
RunE: func(cmd *cobra.Command, args []string) error {
if err := setupServiceCommand(cmd); err != nil {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
svcConfig, err := createServiceConfigForInstall()
if err != nil {
return err
svcConfig := newSVCConfig()
svcConfig.Arguments = []string{
"service",
"run",
"--config",
configPath,
"--log-level",
logLevel,
"--daemon-addr",
daemonAddr,
}
if managementURL != "" {
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
}
if logFile != "" {
svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
}
if runtime.GOOS == "linux" {
// Respected only by systemd systems
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
if logFile != "console" {
setStdLogPath := true
dir := filepath.Dir(logFile)
_, err := os.Stat(dir)
if err != nil {
err = os.MkdirAll(dir, 0750)
if err != nil {
setStdLogPath = false
}
}
if setStdLogPath {
svcConfig.Option["LogOutput"] = true
svcConfig.Option["LogDirectory"] = dir
}
}
}
if runtime.GOOS == "windows" {
svcConfig.Option["OnFailure"] = "restart"
}
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
if err != nil {
cmd.PrintErrln(err)
return err
}
if err := s.Install(); err != nil {
return fmt.Errorf("install service: %w", err)
err = s.Install()
if err != nil {
cmd.PrintErrln(err)
return err
}
cmd.Println("Netbird service has been installed")
@@ -127,109 +93,27 @@ var uninstallCmd = &cobra.Command{
Use: "uninstall",
Short: "uninstalls Netbird service from system",
RunE: func(cmd *cobra.Command, args []string) error {
if err := setupServiceCommand(cmd); err != nil {
return err
}
SetFlagsFromEnvVars(rootCmd)
cfg, err := newSVCConfig()
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return fmt.Errorf("create service config: %w", err)
return err
}
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
s, err := newSVC(newProgram(ctx, cancel), cfg)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil {
return err
}
if err := s.Uninstall(); err != nil {
return fmt.Errorf("uninstall service: %w", err)
err = s.Uninstall()
if err != nil {
return err
}
cmd.Println("Netbird service has been uninstalled")
return nil
},
}
var reconfigureCmd = &cobra.Command{
Use: "reconfigure",
Short: "reconfigures Netbird service with new settings",
Long: `Reconfigures the Netbird service with new settings without manual uninstall/install.
This command will temporarily stop the service, update its configuration, and restart it if it was running.`,
RunE: func(cmd *cobra.Command, args []string) error {
if err := setupServiceCommand(cmd); err != nil {
return err
}
wasRunning, err := isServiceRunning()
if err != nil && !errors.Is(err, ErrGetServiceStatus) {
return fmt.Errorf("check service status: %w", err)
}
svcConfig, err := createServiceConfigForInstall()
if err != nil {
return err
}
ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
if err != nil {
return fmt.Errorf("create service: %w", err)
}
if wasRunning {
cmd.Println("Stopping Netbird service...")
if err := s.Stop(); err != nil {
cmd.Printf("Warning: failed to stop service: %v\n", err)
}
}
cmd.Println("Removing existing service configuration...")
if err := s.Uninstall(); err != nil {
return fmt.Errorf("uninstall existing service: %w", err)
}
cmd.Println("Installing service with new configuration...")
if err := s.Install(); err != nil {
return fmt.Errorf("install service with new config: %w", err)
}
if wasRunning {
cmd.Println("Starting Netbird service...")
if err := s.Start(); err != nil {
return fmt.Errorf("start service after reconfigure: %w", err)
}
cmd.Println("Netbird service has been reconfigured and started")
} else {
cmd.Println("Netbird service has been reconfigured")
}
return nil
},
}
func isServiceRunning() (bool, error) {
cfg, err := newSVCConfig()
if err != nil {
return false, err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
s, err := newSVC(newProgram(ctx, cancel), cfg)
if err != nil {
return false, err
}
status, err := s.Status()
if err != nil {
return false, fmt.Errorf("%w: %w", ErrGetServiceStatus, err)
}
return status == service.StatusRunning, nil
}

View File

@@ -1,263 +0,0 @@
package cmd
import (
"context"
"fmt"
"os"
"runtime"
"testing"
"time"
"github.com/kardianos/service"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
serviceStartTimeout = 10 * time.Second
serviceStopTimeout = 5 * time.Second
statusPollInterval = 500 * time.Millisecond
)
// waitForServiceStatus waits for service to reach expected status with timeout
func waitForServiceStatus(expectedStatus service.Status, timeout time.Duration) (bool, error) {
cfg, err := newSVCConfig()
if err != nil {
return false, err
}
ctxSvc, cancel := context.WithCancel(context.Background())
defer cancel()
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
if err != nil {
return false, err
}
ctx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
defer timeoutCancel()
ticker := time.NewTicker(statusPollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return false, fmt.Errorf("timeout waiting for service status %v", expectedStatus)
case <-ticker.C:
status, err := s.Status()
if err != nil {
// Continue polling on transient errors
continue
}
if status == expectedStatus {
return true, nil
}
}
}
}
// TestServiceLifecycle tests the complete service lifecycle
func TestServiceLifecycle(t *testing.T) {
// TODO: Add support for Windows and macOS
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
t.Skipf("Skipping service lifecycle test on unsupported OS: %s", runtime.GOOS)
}
if os.Getenv("CONTAINER") == "true" {
t.Skip("Skipping service lifecycle test in container environment")
}
originalServiceName := serviceName
serviceName = "netbirdtest" + fmt.Sprintf("%d", time.Now().Unix())
defer func() {
serviceName = originalServiceName
}()
tempDir := t.TempDir()
configPath = fmt.Sprintf("%s/netbird-test-config.json", tempDir)
logLevel = "info"
daemonAddr = fmt.Sprintf("unix://%s/netbird-test.sock", tempDir)
ctx := context.Background()
t.Run("Install", func(t *testing.T) {
installCmd.SetContext(ctx)
err := installCmd.RunE(installCmd, []string{})
require.NoError(t, err)
cfg, err := newSVCConfig()
require.NoError(t, err)
ctxSvc, cancel := context.WithCancel(context.Background())
defer cancel()
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
require.NoError(t, err)
status, err := s.Status()
assert.NoError(t, err)
assert.NotEqual(t, service.StatusUnknown, status)
})
t.Run("Start", func(t *testing.T) {
startCmd.SetContext(ctx)
err := startCmd.RunE(startCmd, []string{})
require.NoError(t, err)
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
require.NoError(t, err)
assert.True(t, running)
})
t.Run("Restart", func(t *testing.T) {
restartCmd.SetContext(ctx)
err := restartCmd.RunE(restartCmd, []string{})
require.NoError(t, err)
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
require.NoError(t, err)
assert.True(t, running)
})
t.Run("Reconfigure", func(t *testing.T) {
originalLogLevel := logLevel
logLevel = "debug"
defer func() {
logLevel = originalLogLevel
}()
reconfigureCmd.SetContext(ctx)
err := reconfigureCmd.RunE(reconfigureCmd, []string{})
require.NoError(t, err)
running, err := waitForServiceStatus(service.StatusRunning, serviceStartTimeout)
require.NoError(t, err)
assert.True(t, running)
})
t.Run("Stop", func(t *testing.T) {
stopCmd.SetContext(ctx)
err := stopCmd.RunE(stopCmd, []string{})
require.NoError(t, err)
stopped, err := waitForServiceStatus(service.StatusStopped, serviceStopTimeout)
require.NoError(t, err)
assert.True(t, stopped)
})
t.Run("Uninstall", func(t *testing.T) {
uninstallCmd.SetContext(ctx)
err := uninstallCmd.RunE(uninstallCmd, []string{})
require.NoError(t, err)
cfg, err := newSVCConfig()
require.NoError(t, err)
ctxSvc, cancel := context.WithCancel(context.Background())
defer cancel()
s, err := newSVC(newProgram(ctxSvc, cancel), cfg)
require.NoError(t, err)
_, err = s.Status()
assert.Error(t, err)
})
}
// TestServiceEnvVars tests environment variable parsing
func TestServiceEnvVars(t *testing.T) {
tests := []struct {
name string
envVars []string
expected map[string]string
expectErr bool
}{
{
name: "Valid single env var",
envVars: []string{"LOG_LEVEL=debug"},
expected: map[string]string{
"LOG_LEVEL": "debug",
},
},
{
name: "Valid multiple env vars",
envVars: []string{"LOG_LEVEL=debug", "CUSTOM_VAR=value"},
expected: map[string]string{
"LOG_LEVEL": "debug",
"CUSTOM_VAR": "value",
},
},
{
name: "Env var with spaces",
envVars: []string{" KEY = value "},
expected: map[string]string{
"KEY": "value",
},
},
{
name: "Invalid format - no equals",
envVars: []string{"INVALID"},
expectErr: true,
},
{
name: "Invalid format - empty key",
envVars: []string{"=value"},
expectErr: true,
},
{
name: "Empty value is valid",
envVars: []string{"KEY="},
expected: map[string]string{
"KEY": "",
},
},
{
name: "Empty slice",
envVars: []string{},
expected: map[string]string{},
},
{
name: "Empty string in slice",
envVars: []string{"", "KEY=value", ""},
expected: map[string]string{"KEY": "value"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseServiceEnvVars(tt.envVars)
if tt.expectErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
}
})
}
}
// TestServiceConfigWithEnvVars tests service config creation with env vars
func TestServiceConfigWithEnvVars(t *testing.T) {
originalServiceName := serviceName
originalServiceEnvVars := serviceEnvVars
defer func() {
serviceName = originalServiceName
serviceEnvVars = originalServiceEnvVars
}()
serviceName = "test-service"
serviceEnvVars = []string{"TEST_VAR=test_value", "ANOTHER_VAR=another_value"}
cfg, err := newSVCConfig()
require.NoError(t, err)
assert.Equal(t, "test-service", cfg.Name)
assert.Equal(t, "test_value", cfg.EnvVars["TEST_VAR"])
assert.Equal(t, "another_value", cfg.EnvVars["ANOTHER_VAR"])
if runtime.GOOS == "linux" {
assert.Equal(t, "test-service", cfg.EnvVars["SYSTEMD_UNIT"])
}
}

View File

@@ -12,15 +12,14 @@ import (
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
nbssh "github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/util"
)
var (
port int
userName = "root"
host string
port int
user = "root"
host string
)
var sshCmd = &cobra.Command{
@@ -32,7 +31,7 @@ var sshCmd = &cobra.Command{
split := strings.Split(args[0], "@")
if len(split) == 2 {
userName = split[0]
user = split[0]
host = split[1]
} else {
host = args[0]
@@ -47,7 +46,7 @@ var sshCmd = &cobra.Command{
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, util.LogConsole)
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
@@ -59,19 +58,11 @@ var sshCmd = &cobra.Command{
ctx := internal.CtxInitState(cmd.Context())
pm := profilemanager.NewProfileManager()
activeProf, err := pm.GetActiveProfile()
config, err := internal.UpdateConfig(internal.ConfigInput{
ConfigPath: configPath,
})
if err != nil {
return fmt.Errorf("get active profile: %v", err)
}
profPath, err := activeProf.FilePath()
if err != nil {
return fmt.Errorf("get active profile path: %v", err)
}
config, err := profilemanager.ReadConfig(profPath)
if err != nil {
return fmt.Errorf("read profile config: %v", err)
return err
}
sig := make(chan os.Signal, 1)
@@ -98,7 +89,7 @@ var sshCmd = &cobra.Command{
}
func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command) error {
c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), userName, pemKey)
c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), user, pemKey)
if err != nil {
cmd.Printf("Error: %v\n", err)
cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" +

View File

@@ -11,7 +11,6 @@ import (
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto"
nbstatus "github.com/netbirdio/netbird/client/status"
"github.com/netbirdio/netbird/util"
@@ -60,7 +59,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
return err
}
err = util.InitLog(logLevel, util.LogConsole)
err = util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
@@ -92,13 +91,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
return nil
}
pm := profilemanager.NewProfileManager()
var profName string
if activeProf, err := pm.GetActiveProfile(); err == nil {
profName = activeProf.Name
}
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap, connectionTypeFilter, profName)
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap, connectionTypeFilter)
var statusOutputString string
switch {
case detailFlag:

View File

@@ -124,7 +124,7 @@ func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc
}
func startClientDaemon(
t *testing.T, ctx context.Context, _, _ string,
t *testing.T, ctx context.Context, _, configPath string,
) (*grpc.Server, net.Listener) {
t.Helper()
lis, err := net.Listen("tcp", "127.0.0.1:0")
@@ -134,7 +134,7 @@ func startClientDaemon(
s := grpc.NewServer()
server := client.New(ctx,
"", false)
configPath, "")
if err := server.Start(); err != nil {
t.Fatal(err)
}

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"net"
"net/netip"
"os/user"
"runtime"
"strings"
"time"
@@ -13,14 +12,12 @@ import (
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/management/domain"
@@ -38,9 +35,6 @@ const (
noBrowserFlag = "no-browser"
noBrowserDesc = "do not open the browser for SSO login"
profileNameFlag = "profile"
profileNameDesc = "profile name to use for the login. If not specified, the last used profile will be used."
)
var (
@@ -48,8 +42,6 @@ var (
dnsLabels []string
dnsLabelsValidated domain.List
noBrowser bool
profileName string
configPath string
upCmd = &cobra.Command{
Use: "up",
@@ -78,8 +70,6 @@ func init() {
)
upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
}
@@ -89,7 +79,7 @@ func upFunc(cmd *cobra.Command, args []string) error {
cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, util.LogConsole)
err := util.InitLog(logLevel, "console")
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
@@ -111,41 +101,13 @@ func upFunc(cmd *cobra.Command, args []string) error {
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
}
pm := profilemanager.NewProfileManager()
username, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}
var profileSwitched bool
// switch profile if provided
if profileName != "" {
err = switchProfile(cmd.Context(), profileName, username.Username)
if err != nil {
return fmt.Errorf("switch profile: %v", err)
}
err = pm.SwitchProfile(profileName)
if err != nil {
return fmt.Errorf("switch profile: %v", err)
}
profileSwitched = true
}
activeProf, err := pm.GetActiveProfile()
if err != nil {
return fmt.Errorf("get active profile: %v", err)
}
if foregroundMode {
return runInForegroundMode(ctx, cmd, activeProf)
return runInForegroundMode(ctx, cmd)
}
return runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched)
return runInDaemonMode(ctx, cmd)
}
func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error {
func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
err := handleRebrand(cmd)
if err != nil {
return err
@@ -156,12 +118,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
return err
}
configFilePath, err := activeProf.FilePath()
if err != nil {
return fmt.Errorf("get active profile file path: %v", err)
}
ic, err := setupConfig(customDNSAddressConverted, cmd, configFilePath)
ic, err := setupConfig(customDNSAddressConverted, cmd)
if err != nil {
return fmt.Errorf("setup config: %v", err)
}
@@ -171,12 +128,12 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
return err
}
config, err := profilemanager.UpdateOrCreateConfig(*ic)
config, err := internal.UpdateOrCreateConfig(*ic)
if err != nil {
return fmt.Errorf("get config file: %v", err)
}
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
err = foregroundLogin(ctx, cmd, config, providedSetupKey)
if err != nil {
@@ -196,10 +153,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
return connectClient.Run(nil)
}
func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, profileSwitched bool) error {
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
if err != nil {
return fmt.Errorf("parse custom DNS address: %v", err)
return err
}
conn, err := DialClientGRPCServer(ctx, daemonAddr)
@@ -224,41 +181,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
}
if status.Status == string(internal.StatusConnected) {
if !profileSwitched {
cmd.Println("Already connected")
return nil
}
if _, err := client.Down(ctx, &proto.DownRequest{}); err != nil {
log.Errorf("call service down method: %v", err)
return err
}
cmd.Println("Already connected")
return nil
}
username, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
}
// set the new config
req := setupSetConfigReq(customDNSAddressConverted, cmd, activeProf.Name, username.Username)
if _, err := client.SetConfig(ctx, req); err != nil {
if st, ok := gstatus.FromError(err); ok && st.Code() == codes.Unavailable {
log.Warnf("setConfig method is not available in the daemon")
} else {
return fmt.Errorf("call service setConfig method: %v", err)
}
}
if err := doDaemonUp(ctx, cmd, client, pm, activeProf, customDNSAddressConverted, username.Username); err != nil {
return fmt.Errorf("daemon up failed: %v", err)
}
cmd.Println("Connected")
return nil
}
func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, customDNSAddressConverted []byte, username string) error {
providedSetupKey, err := getSetupKey()
if err != nil {
return fmt.Errorf("get setup key: %v", err)
@@ -269,9 +195,6 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ
return fmt.Errorf("setup login request: %v", err)
}
loginRequest.ProfileName = &activeProf.Name
loginRequest.Username = &username
var loginErr error
var loginResp *proto.LoginResponse
@@ -296,105 +219,27 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ
}
if loginResp.NeedsSSOLogin {
if err := handleSSOLogin(ctx, cmd, loginResp, client, pm); err != nil {
return fmt.Errorf("sso login failed: %v", err)
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
if err != nil {
return fmt.Errorf("waiting sso login failed with: %v", err)
}
}
if _, err := client.Up(ctx, &proto.UpRequest{
ProfileName: &activeProf.Name,
Username: &username,
}); err != nil {
if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
return fmt.Errorf("call service up method: %v", err)
}
cmd.Println("Connected")
return nil
}
func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, profileName, username string) *proto.SetConfigRequest {
var req proto.SetConfigRequest
req.ProfileName = profileName
req.Username = username
req.ManagementUrl = managementURL
req.AdminURL = adminURL
req.NatExternalIPs = natExternalIPs
req.CustomDNSAddress = customDNSAddressConverted
req.ExtraIFaceBlacklist = extraIFaceBlackList
req.DnsLabels = dnsLabelsValidated.ToPunycodeList()
req.CleanDNSLabels = dnsLabels != nil && len(dnsLabels) == 0
req.CleanNATExternalIPs = natExternalIPs != nil && len(natExternalIPs) == 0
if cmd.Flag(enableRosenpassFlag).Changed {
req.RosenpassEnabled = &rosenpassEnabled
}
if cmd.Flag(rosenpassPermissiveFlag).Changed {
req.RosenpassPermissive = &rosenpassPermissive
}
if cmd.Flag(serverSSHAllowedFlag).Changed {
req.ServerSSHAllowed = &serverSSHAllowed
}
if cmd.Flag(interfaceNameFlag).Changed {
if err := parseInterfaceName(interfaceName); err != nil {
log.Errorf("parse interface name: %v", err)
return nil
}
req.InterfaceName = &interfaceName
}
if cmd.Flag(wireguardPortFlag).Changed {
p := int64(wireguardPort)
req.WireguardPort = &p
}
if cmd.Flag(networkMonitorFlag).Changed {
req.NetworkMonitor = &networkMonitor
}
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
req.OptionalPreSharedKey = &preSharedKey
}
if cmd.Flag(disableAutoConnectFlag).Changed {
req.DisableAutoConnect = &autoConnectDisabled
}
if cmd.Flag(dnsRouteIntervalFlag).Changed {
req.DnsRouteInterval = durationpb.New(dnsRouteInterval)
}
if cmd.Flag(disableClientRoutesFlag).Changed {
req.DisableClientRoutes = &disableClientRoutes
}
if cmd.Flag(disableServerRoutesFlag).Changed {
req.DisableServerRoutes = &disableServerRoutes
}
if cmd.Flag(disableDNSFlag).Changed {
req.DisableDns = &disableDNS
}
if cmd.Flag(disableFirewallFlag).Changed {
req.DisableFirewall = &disableFirewall
}
if cmd.Flag(blockLANAccessFlag).Changed {
req.BlockLanAccess = &blockLANAccess
}
if cmd.Flag(blockInboundFlag).Changed {
req.BlockInbound = &blockInbound
}
if cmd.Flag(enableLazyConnectionFlag).Changed {
req.LazyConnectionEnabled = &lazyConnEnabled
}
return &req
}
func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFilePath string) (*profilemanager.ConfigInput, error) {
ic := profilemanager.ConfigInput{
func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command) (*internal.ConfigInput, error) {
ic := internal.ConfigInput{
ManagementURL: managementURL,
ConfigPath: configFilePath,
AdminURL: adminURL,
ConfigPath: configPath,
NATExternalIPs: natExternalIPs,
CustomDNSAddress: customDNSAddressConverted,
ExtraIFaceBlackList: extraIFaceBlackList,
@@ -480,6 +325,7 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey,
ManagementUrl: managementURL,
AdminURL: adminURL,
NatExternalIPs: natExternalIPs,
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
CustomDNSAddress: customDNSAddressConverted,
@@ -638,7 +484,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) {
if !isValidAddrPort(customDNSAddress) {
return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress)
}
if customDNSAddress == "" && util.FindFirstLogPath(logFiles) != "" {
if customDNSAddress == "" && logFile != "console" {
parsed = []byte("empty")
} else {
parsed = []byte(customDNSAddress)

View File

@@ -3,55 +3,18 @@ package cmd
import (
"context"
"os"
"os/user"
"testing"
"time"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
)
var cliAddr string
func TestUpDaemon(t *testing.T) {
tempDir := t.TempDir()
origDefaultProfileDir := profilemanager.DefaultConfigPathDir
origActiveProfileStatePath := profilemanager.ActiveProfileStatePath
profilemanager.DefaultConfigPathDir = tempDir
profilemanager.ActiveProfileStatePath = tempDir + "/active_profile.json"
profilemanager.ConfigDirOverride = tempDir
currUser, err := user.Current()
if err != nil {
t.Fatalf("failed to get current user: %v", err)
return
}
sm := profilemanager.ServiceManager{}
err = sm.AddProfile("test1", currUser.Username)
if err != nil {
t.Fatalf("failed to add profile: %v", err)
return
}
err = sm.SetActiveProfileState(&profilemanager.ActiveProfileState{
Name: "test1",
Username: currUser.Username,
})
if err != nil {
t.Fatalf("failed to set active profile state: %v", err)
return
}
t.Cleanup(func() {
profilemanager.DefaultConfigPathDir = origDefaultProfileDir
profilemanager.ActiveProfileStatePath = origActiveProfileStatePath
profilemanager.ConfigDirOverride = ""
})
mgmAddr := startTestingServices(t)
tempDir := t.TempDir()
confPath := tempDir + "/config.json"
ctx := internal.CtxInitState(context.Background())

View File

@@ -17,7 +17,6 @@ import (
"github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/system"
)
@@ -27,7 +26,7 @@ var ErrClientNotStarted = errors.New("client not started")
// Client manages a netbird embedded client instance
type Client struct {
deviceName string
config *profilemanager.Config
config *internal.Config
mu sync.Mutex
cancel context.CancelFunc
setupKey string
@@ -89,9 +88,9 @@ func New(opts Options) (*Client, error) {
}
t := true
var config *profilemanager.Config
var config *internal.Config
var err error
input := profilemanager.ConfigInput{
input := internal.ConfigInput{
ConfigPath: opts.ConfigPath,
ManagementURL: opts.ManagementURL,
PreSharedKey: &opts.PreSharedKey,
@@ -99,9 +98,9 @@ func New(opts Options) (*Client, error) {
DisableClientRoutes: &opts.DisableClientRoutes,
}
if opts.ConfigPath != "" {
config, err = profilemanager.UpdateOrCreateConfig(input)
config, err = internal.UpdateOrCreateConfig(input)
} else {
config, err = profilemanager.CreateInMemoryConfig(input)
config, err = internal.CreateInMemoryConfig(input)
}
if err != nil {
return nil, fmt.Errorf("create config: %w", err)

View File

@@ -154,7 +154,7 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r
s.udpMux = NewUniversalUDPMuxDefault(
UniversalUDPMuxParams{
UDPConn: nbnet.WrapPacketConn(conn),
UDPConn: nbnet.WrapUDPConn(conn),
Net: s.transportNet,
FilterFn: s.filterFn,
WGAddress: s.address,

View File

@@ -7,16 +7,15 @@ import (
)
func (m *UDPMuxDefault) notifyAddressRemoval(addr string) {
// Kernel mode: direct nbnet.PacketConn (SharedSocket wrapped with nbnet)
if conn, ok := m.params.UDPConn.(*nbnet.PacketConn); ok {
conn.RemoveAddress(addr)
wrapped, ok := m.params.UDPConn.(*UDPConn)
if !ok {
return
}
// Userspace mode: UDPConn wrapper around nbnet.PacketConn
if wrapped, ok := m.params.UDPConn.(*UDPConn); ok {
if conn, ok := wrapped.GetPacketConn().(*nbnet.PacketConn); ok {
conn.RemoveAddress(addr)
}
nbnetConn, ok := wrapped.GetPacketConn().(*nbnet.UDPConn)
if !ok {
return
}
nbnetConn.RemoveAddress(addr)
}

View File

@@ -3,4 +3,4 @@
package configurer
// WgInterfaceDefault is a default interface name of Netbird
const WgInterfaceDefault = "wt0"
const WgInterfaceDefault = "nb0"

View File

@@ -530,7 +530,7 @@ func parseStatus(deviceName, ipcStr string) (*Stats, error) {
if currentPeer == nil {
continue
}
if val != "" && val != "0000000000000000000000000000000000000000000000000000000000000000" {
if val != "" {
currentPeer.PresharedKey = true
}
}

View File

@@ -16,7 +16,6 @@ import (
"github.com/netbirdio/netbird/client/iface/configurer"
"github.com/netbirdio/netbird/client/iface/wgaddr"
"github.com/netbirdio/netbird/sharedsock"
nbnet "github.com/netbirdio/netbird/util/net"
)
type TunKernelDevice struct {
@@ -100,14 +99,8 @@ func (t *TunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
if err != nil {
return nil, err
}
var udpConn net.PacketConn = rawSock
if !nbnet.AdvancedRouting() {
udpConn = nbnet.WrapPacketConn(rawSock)
}
bindParams := bind.UniversalUDPMuxParams{
UDPConn: udpConn,
UDPConn: rawSock,
Net: t.transportNet,
FilterFn: t.filterFn,
WGAddress: t.address,

View File

@@ -1,10 +1,7 @@
package listener
import "sync"
type CloseListener struct {
listener func()
mu sync.Mutex
}
func NewCloseListener() *CloseListener {
@@ -12,21 +9,11 @@ func NewCloseListener() *CloseListener {
}
func (c *CloseListener) SetCloseListener(listener func()) {
c.mu.Lock()
defer c.mu.Unlock()
c.listener = listener
}
func (c *CloseListener) Notify() {
c.mu.Lock()
if c.listener == nil {
c.mu.Unlock()
return
if c.listener != nil {
c.listener()
}
listener := c.listener
c.mu.Unlock()
listener()
}

View File

@@ -17,7 +17,7 @@ import (
)
func TestMain(m *testing.M) {
_ = util.InitLog("trace", util.LogConsole)
_ = util.InitLog("trace", "console")
code := m.Run()
os.Exit(code)
}

View File

@@ -183,11 +183,6 @@ func (p *WGUDPProxy) proxyToLocal(ctx context.Context) {
for {
n, err := p.remoteConnRead(ctx, buf)
if err != nil {
if ctx.Err() != nil {
return
}
p.closeListener.Notify()
return
}

View File

@@ -11,7 +11,6 @@ import (
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
)
// OAuthFlow represents an interface for authorization using different OAuth 2.0 flows
@@ -49,7 +48,6 @@ type TokenInfo struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
UseIDToken bool `json:"-"`
Email string `json:"-"`
}
// GetTokenToUse returns either the access or id token based on UseIDToken field
@@ -66,7 +64,7 @@ func (t TokenInfo) GetTokenToUse() string {
// and if that also fails, the authentication process is deemed unsuccessful
//
// On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow
func NewOAuthFlow(ctx context.Context, config *profilemanager.Config, isUnixDesktopClient bool) (OAuthFlow, error) {
func NewOAuthFlow(ctx context.Context, config *internal.Config, isUnixDesktopClient bool) (OAuthFlow, error) {
if (runtime.GOOS == "linux" || runtime.GOOS == "freebsd") && !isUnixDesktopClient {
return authenticateWithDeviceCodeFlow(ctx, config)
}
@@ -82,7 +80,7 @@ func NewOAuthFlow(ctx context.Context, config *profilemanager.Config, isUnixDesk
}
// authenticateWithPKCEFlow initializes the Proof Key for Code Exchange flow auth flow
func authenticateWithPKCEFlow(ctx context.Context, config *profilemanager.Config) (OAuthFlow, error) {
func authenticateWithPKCEFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL, config.ClientCertKeyPair)
if err != nil {
return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err)
@@ -91,7 +89,7 @@ func authenticateWithPKCEFlow(ctx context.Context, config *profilemanager.Config
}
// authenticateWithDeviceCodeFlow initializes the Device Code auth Flow
func authenticateWithDeviceCodeFlow(ctx context.Context, config *profilemanager.Config) (OAuthFlow, error) {
func authenticateWithDeviceCodeFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
if err != nil {
switch s, ok := gstatus.FromError(err); {

View File

@@ -6,7 +6,6 @@ import (
"crypto/subtle"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"html/template"
@@ -231,46 +230,9 @@ func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo,
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
}
email, err := parseEmailFromIDToken(tokenInfo.IDToken)
if err != nil {
log.Warnf("failed to parse email from ID token: %v", err)
} else {
tokenInfo.Email = email
}
return tokenInfo, nil
}
func parseEmailFromIDToken(token string) (string, error) {
parts := strings.Split(token, ".")
if len(parts) < 2 {
return "", fmt.Errorf("invalid token format")
}
data, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return "", fmt.Errorf("failed to decode payload: %w", err)
}
var claims map[string]interface{}
if err := json.Unmarshal(data, &claims); err != nil {
return "", fmt.Errorf("json unmarshal error: %w", err)
}
var email string
if emailValue, ok := claims["email"].(string); ok {
email = emailValue
} else {
val, ok := claims["name"].(string)
if ok {
email = val
} else {
return "", fmt.Errorf("email or name field not found in token payload")
}
}
return email, nil
}
func createCodeChallenge(codeVerifier string) string {
sha2 := sha256.Sum256([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(sha2[:])

View File

@@ -1,4 +1,4 @@
package profilemanager
package internal
import (
"context"
@@ -6,16 +6,16 @@ import (
"fmt"
"net/url"
"os"
"path/filepath"
"reflect"
"runtime"
"slices"
"strings"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
@@ -38,8 +38,8 @@ const (
DefaultAdminURL = "https://app.netbird.io:443"
)
var DefaultInterfaceBlacklist = []string{
iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
var defaultInterfaceBlacklist = []string{
iface.WgInterfaceDefault, "nb", "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
"Tailscale", "tailscale", "docker", "veth", "br-", "lo",
}
@@ -144,47 +144,78 @@ type Config struct {
LazyConnectionEnabled bool
}
var ConfigDirOverride string
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
func ReadConfig(configPath string) (*Config, error) {
if fileExists(configPath) {
err := util.EnforcePermission(configPath)
if err != nil {
log.Errorf("failed to enforce permission on config dir: %v", err)
}
func getConfigDir() (string, error) {
if ConfigDirOverride != "" {
return ConfigDirOverride, nil
config := &Config{}
if _, err := util.ReadJson(configPath, config); err != nil {
return nil, err
}
// initialize through apply() without changes
if changed, err := config.apply(ConfigInput{}); err != nil {
return nil, err
} else if changed {
if err = WriteOutConfig(configPath, config); err != nil {
return nil, err
}
}
return config, nil
}
configDir, err := os.UserConfigDir()
cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath})
if err != nil {
return "", err
return nil, err
}
configDir = filepath.Join(configDir, "netbird")
if _, err := os.Stat(configDir); os.IsNotExist(err) {
if err := os.MkdirAll(configDir, 0755); err != nil {
return "", err
}
}
return configDir, nil
err = WriteOutConfig(configPath, cfg)
return cfg, err
}
func getConfigDirForUser(username string) (string, error) {
if ConfigDirOverride != "" {
return ConfigDirOverride, nil
// UpdateConfig update existing configuration according to input configuration and return with the configuration
func UpdateConfig(input ConfigInput) (*Config, error) {
if !fileExists(input.ConfigPath) {
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
}
username = sanitizeProfileName(username)
configDir := filepath.Join(DefaultConfigPathDir, username)
if _, err := os.Stat(configDir); os.IsNotExist(err) {
if err := os.MkdirAll(configDir, 0600); err != nil {
return "", err
}
}
return configDir, nil
return update(input)
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
// UpdateOrCreateConfig reads existing config or generates a new one
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
if !fileExists(input.ConfigPath) {
log.Infof("generating new config %s", input.ConfigPath)
cfg, err := createNewConfig(input)
if err != nil {
return nil, err
}
err = util.WriteJsonWithRestrictedPermission(context.Background(), input.ConfigPath, cfg)
return cfg, err
}
if isPreSharedKeyHidden(input.PreSharedKey) {
input.PreSharedKey = nil
}
err := util.EnforcePermission(input.ConfigPath)
if err != nil {
log.Errorf("failed to enforce permission on config dir: %v", err)
}
return update(input)
}
// CreateInMemoryConfig generate a new config but do not write out it to the store
func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
return createNewConfig(input)
}
// WriteOutConfig write put the prepared config to the given path
func WriteOutConfig(path string, config *Config) error {
return util.WriteJson(context.Background(), path, config)
}
// createNewConfig creates a new config generating a new Wireguard key and saving to file
@@ -192,6 +223,8 @@ func createNewConfig(input ConfigInput) (*Config, error) {
config := &Config{
// defaults to false only for new (post 0.26) configurations
ServerSSHAllowed: util.False(),
// default to disabling server routes on Android for security
DisableServerRoutes: runtime.GOOS == "android",
}
if _, err := config.apply(input); err != nil {
@@ -201,6 +234,27 @@ func createNewConfig(input ConfigInput) (*Config, error) {
return config, nil
}
func update(input ConfigInput) (*Config, error) {
config := &Config{}
if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
return nil, err
}
updated, err := config.apply(input)
if err != nil {
return nil, err
}
if updated {
if err := util.WriteJson(context.Background(), input.ConfigPath, config); err != nil {
return nil, err
}
}
return config, nil
}
func (config *Config) apply(input ConfigInput) (updated bool, err error) {
if config.ManagementURL == nil {
log.Infof("using default Management URL %s", DefaultManagementURL)
@@ -328,8 +382,8 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
if len(config.IFaceBlackList) == 0 {
log.Infof("filling in interface blacklist with defaults: [ %s ]",
strings.Join(DefaultInterfaceBlacklist, " "))
config.IFaceBlackList = append(config.IFaceBlackList, DefaultInterfaceBlacklist...)
strings.Join(defaultInterfaceBlacklist, " "))
config.IFaceBlackList = append(config.IFaceBlackList, defaultInterfaceBlacklist...)
updated = true
}
@@ -542,69 +596,17 @@ func isPreSharedKeyHidden(preSharedKey *string) bool {
return false
}
// UpdateConfig update existing configuration according to input configuration and return with the configuration
func UpdateConfig(input ConfigInput) (*Config, error) {
if !fileExists(input.ConfigPath) {
return nil, fmt.Errorf("config file %s does not exist", input.ConfigPath)
}
return update(input)
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
// UpdateOrCreateConfig reads existing config or generates a new one
func UpdateOrCreateConfig(input ConfigInput) (*Config, error) {
if !fileExists(input.ConfigPath) {
log.Infof("generating new config %s", input.ConfigPath)
cfg, err := createNewConfig(input)
if err != nil {
return nil, err
}
err = util.WriteJsonWithRestrictedPermission(context.Background(), input.ConfigPath, cfg)
return cfg, err
}
if isPreSharedKeyHidden(input.PreSharedKey) {
input.PreSharedKey = nil
}
err := util.EnforcePermission(input.ConfigPath)
func createFile(path string) error {
file, err := os.Create(path)
if err != nil {
log.Errorf("failed to enforce permission on config dir: %v", err)
return err
}
return update(input)
}
func update(input ConfigInput) (*Config, error) {
config := &Config{}
if _, err := util.ReadJson(input.ConfigPath, config); err != nil {
return nil, err
}
updated, err := config.apply(input)
if err != nil {
return nil, err
}
if updated {
if err := util.WriteJson(context.Background(), input.ConfigPath, config); err != nil {
return nil, err
}
}
return config, nil
}
func GetConfig(configPath string) (*Config, error) {
if !fileExists(configPath) {
return nil, fmt.Errorf("config file %s does not exist", configPath)
}
config := &Config{}
if _, err := util.ReadJson(configPath, config); err != nil {
return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
}
return config, nil
return file.Close()
}
// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
@@ -688,46 +690,3 @@ func UpdateOldManagementURL(ctx context.Context, config *Config, configPath stri
return newConfig, nil
}
// CreateInMemoryConfig generate a new config but do not write out it to the store
func CreateInMemoryConfig(input ConfigInput) (*Config, error) {
return createNewConfig(input)
}
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
func ReadConfig(configPath string) (*Config, error) {
if fileExists(configPath) {
err := util.EnforcePermission(configPath)
if err != nil {
log.Errorf("failed to enforce permission on config dir: %v", err)
}
config := &Config{}
if _, err := util.ReadJson(configPath, config); err != nil {
return nil, err
}
// initialize through apply() without changes
if changed, err := config.apply(ConfigInput{}); err != nil {
return nil, err
} else if changed {
if err = WriteOutConfig(configPath, config); err != nil {
return nil, err
}
}
return config, nil
}
cfg, err := createNewConfig(ConfigInput{ConfigPath: configPath})
if err != nil {
return nil, err
}
err = WriteOutConfig(configPath, cfg)
return cfg, err
}
// WriteOutConfig write put the prepared config to the given path
func WriteOutConfig(path string, config *Config) error {
return util.WriteJson(context.Background(), path, config)
}

View File

@@ -1,4 +1,4 @@
package profilemanager
package internal
import (
"context"

View File

@@ -21,7 +21,6 @@ import (
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
cProto "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/ssh"
@@ -38,7 +37,7 @@ import (
type ConnectClient struct {
ctx context.Context
config *profilemanager.Config
config *Config
statusRecorder *peer.Status
engine *Engine
engineMutex sync.Mutex
@@ -48,7 +47,7 @@ type ConnectClient struct {
func NewConnectClient(
ctx context.Context,
config *profilemanager.Config,
config *Config,
statusRecorder *peer.Status,
) *ConnectClient {
@@ -414,7 +413,7 @@ func (c *ConnectClient) SetNetworkMapPersistence(enabled bool) {
}
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
nm := false
if config.NetworkMonitor != nil {
nm = *config.NetworkMonitor
@@ -484,7 +483,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourP
}
// loginToManagement creates Management ServiceDependencies client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc)
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *profilemanager.Config) (*mgmProto.LoginResponse, error) {
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) {
serverPublicKey, err := client.GetServerPublicKey()
if err != nil {

View File

@@ -16,7 +16,6 @@ import (
"path/filepath"
"runtime"
"runtime/pprof"
"slices"
"sort"
"strings"
"time"
@@ -25,10 +24,10 @@ import (
"google.golang.org/protobuf/encoding/protojson"
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/statemanager"
mgmProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/util"
)
const readmeContent = `Netbird debug bundle
@@ -39,12 +38,10 @@ status.txt: Anonymized status information of the NetBird client.
client.log: Most recent, anonymized client log file of the NetBird client.
netbird.err: Most recent, anonymized stderr log file of the NetBird client.
netbird.out: Most recent, anonymized stdout log file of the NetBird client.
routes.txt: Detailed system routing table in tabular format including destination, gateway, interface, metrics, and protocol information, if --system-info flag was provided.
routes.txt: Anonymized system routes, if --system-info flag was provided.
interfaces.txt: Anonymized network interface information, if --system-info flag was provided.
ip_rules.txt: Detailed IP routing rules in tabular format including priority, source, destination, interfaces, table, and action information (Linux only), if --system-info flag was provided.
iptables.txt: Anonymized iptables rules with packet counters, if --system-info flag was provided.
nftables.txt: Anonymized nftables rules with packet counters, if --system-info flag was provided.
resolved_domains.txt: Anonymized resolved domain IP addresses from the status recorder.
config.txt: Anonymized configuration information of the NetBird client.
network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
state.json: Anonymized client state dump containing netbird states.
@@ -108,29 +105,7 @@ go tool pprof -http=:8088 heap.prof
This will open a web browser tab with the profiling information.
Routes
The routes.txt file contains detailed routing table information in a tabular format:
- Destination: Network prefix (IP_ADDRESS/PREFIX_LENGTH)
- Gateway: Next hop IP address (or "-" if direct)
- Interface: Network interface name
- Metric: Route priority/metric (lower values preferred)
- Protocol: Routing protocol (kernel, static, dhcp, etc.)
- Scope: Route scope (global, link, host, etc.)
- Type: Route type (unicast, local, broadcast, etc.)
- Table: Routing table name (main, local, netbird, etc.)
The table format provides a comprehensive view of the system's routing configuration, including information from multiple routing tables on Linux systems. This is valuable for troubleshooting routing issues and understanding traffic flow.
For anonymized routes, IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. Interface names are anonymized using string anonymization.
Resolved Domains
The resolved_domains.txt file contains information about domain names that have been resolved to IP addresses by NetBird's DNS resolver. This includes:
- Original domain patterns that were configured for routing
- Resolved domain names that matched those patterns
- IP address prefixes that were resolved for each domain
- Parent domain associations showing which original pattern each resolved domain belongs to
All domain names and IP addresses in this file follow the same anonymization rules as described above. This information is valuable for troubleshooting DNS resolution and routing issues.
For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct.
Network Interfaces
The interfaces.txt file contains information about network interfaces, including:
@@ -168,22 +143,6 @@ nftables.txt:
- Shows packet and byte counters for each rule
- All IP addresses are anonymized
- Chain names, table names, and other non-sensitive information remain unchanged
IP Rules (Linux only)
The ip_rules.txt file contains detailed IP routing rule information:
- Priority: Rule priority number (lower values processed first)
- From: Source IP prefix or "all" if unspecified
- To: Destination IP prefix or "all" if unspecified
- IIF: Input interface name or "-" if unspecified
- OIF: Output interface name or "-" if unspecified
- Table: Target routing table name (main, local, netbird, etc.)
- Action: Rule action (lookup, goto, blackhole, etc.)
- Mark: Firewall mark value in hex format or "-" if unspecified
The table format provides comprehensive visibility into the IP routing decision process, including how traffic is directed to different routing tables based on various criteria. This is valuable for troubleshooting advanced routing configurations and policy-based routing.
For anonymized rules, IP addresses and prefixes are replaced as described above. Interface names are anonymized using string anonymization. Table names, actions, and other non-sensitive information remain unchanged.
`
const (
@@ -199,11 +158,12 @@ type BundleGenerator struct {
anonymizer *anonymize.Anonymizer
// deps
internalConfig *profilemanager.Config
internalConfig *internal.Config
statusRecorder *peer.Status
networkMap *mgmProto.NetworkMap
logFile string
// config
anonymize bool
clientStatus string
includeSystemInfo bool
@@ -220,7 +180,7 @@ type BundleConfig struct {
}
type GeneratorDependencies struct {
InternalConfig *profilemanager.Config
InternalConfig *internal.Config
StatusRecorder *peer.Status
NetworkMap *mgmProto.NetworkMap
LogFile string
@@ -296,11 +256,7 @@ func (g *BundleGenerator) createArchive() error {
}
if err := g.addConfig(); err != nil {
log.Errorf("failed to add config to debug bundle: %v", err)
}
if err := g.addResolvedDomains(); err != nil {
log.Errorf("failed to add resolved domains to debug bundle: %v", err)
log.Errorf("Failed to add config to debug bundle: %v", err)
}
if g.includeSystemInfo {
@@ -308,7 +264,7 @@ func (g *BundleGenerator) createArchive() error {
}
if err := g.addProf(); err != nil {
log.Errorf("failed to add profiles to debug bundle: %v", err)
log.Errorf("Failed to add profiles to debug bundle: %v", err)
}
if err := g.addNetworkMap(); err != nil {
@@ -316,26 +272,26 @@ func (g *BundleGenerator) createArchive() error {
}
if err := g.addStateFile(); err != nil {
log.Errorf("failed to add state file to debug bundle: %v", err)
log.Errorf("Failed to add state file to debug bundle: %v", err)
}
if err := g.addCorruptedStateFiles(); err != nil {
log.Errorf("failed to add corrupted state files to debug bundle: %v", err)
log.Errorf("Failed to add corrupted state files to debug bundle: %v", err)
}
if err := g.addWgShow(); err != nil {
log.Errorf("failed to add wg show output: %v", err)
log.Errorf("Failed to add wg show output: %v", err)
}
if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) {
if g.logFile != "console" && g.logFile != "" {
if err := g.addLogfile(); err != nil {
log.Errorf("failed to add log file to debug bundle: %v", err)
log.Errorf("Failed to add log file to debug bundle: %v", err)
if err := g.trySystemdLogFallback(); err != nil {
log.Errorf("failed to add systemd logs as fallback: %v", err)
log.Errorf("Failed to add systemd logs as fallback: %v", err)
}
}
} else if err := g.trySystemdLogFallback(); err != nil {
log.Errorf("failed to add systemd logs: %v", err)
log.Errorf("Failed to add systemd logs: %v", err)
}
return nil
@@ -343,19 +299,15 @@ func (g *BundleGenerator) createArchive() error {
func (g *BundleGenerator) addSystemInfo() {
if err := g.addRoutes(); err != nil {
log.Errorf("failed to add routes to debug bundle: %v", err)
log.Errorf("Failed to add routes to debug bundle: %v", err)
}
if err := g.addInterfaces(); err != nil {
log.Errorf("failed to add interfaces to debug bundle: %v", err)
}
if err := g.addIPRules(); err != nil {
log.Errorf("failed to add IP rules to debug bundle: %v", err)
log.Errorf("Failed to add interfaces to debug bundle: %v", err)
}
if err := g.addFirewallRules(); err != nil {
log.Errorf("failed to add firewall rules to debug bundle: %v", err)
log.Errorf("Failed to add firewall rules to debug bundle: %v", err)
}
}
@@ -410,6 +362,7 @@ func (g *BundleGenerator) addConfig() error {
}
}
// Add config content to zip file
configReader := strings.NewReader(configContent.String())
if err := g.addFileToZip(configReader, "config.txt"); err != nil {
return fmt.Errorf("add config file to zip: %w", err)
@@ -421,6 +374,7 @@ func (g *BundleGenerator) addConfig() error {
func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) {
configContent.WriteString("NetBird Client Configuration:\n\n")
// Add non-sensitive fields
configContent.WriteString(fmt.Sprintf("WgIface: %s\n", g.internalConfig.WgIface))
configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort))
if g.internalConfig.NetworkMonitor != nil {
@@ -505,27 +459,6 @@ func (g *BundleGenerator) addInterfaces() error {
return nil
}
func (g *BundleGenerator) addResolvedDomains() error {
if g.statusRecorder == nil {
log.Debugf("skipping resolved domains in debug bundle: no status recorder")
return nil
}
resolvedDomains := g.statusRecorder.GetResolvedDomainsStates()
if len(resolvedDomains) == 0 {
log.Debugf("skipping resolved domains in debug bundle: no resolved domains")
return nil
}
resolvedDomainsContent := formatResolvedDomains(resolvedDomains, g.anonymize, g.anonymizer)
resolvedDomainsReader := strings.NewReader(resolvedDomainsContent)
if err := g.addFileToZip(resolvedDomainsReader, "resolved_domains.txt"); err != nil {
return fmt.Errorf("add resolved domains file to zip: %w", err)
}
return nil
}
func (g *BundleGenerator) addNetworkMap() error {
if g.networkMap == nil {
log.Debugf("skipping empty network map in debug bundle")
@@ -558,8 +491,7 @@ func (g *BundleGenerator) addNetworkMap() error {
}
func (g *BundleGenerator) addStateFile() error {
sm := profilemanager.ServiceManager{}
path := sm.GetStatePath()
path := statemanager.GetDefaultStatePath()
if path == "" {
return nil
}
@@ -597,8 +529,7 @@ func (g *BundleGenerator) addStateFile() error {
}
func (g *BundleGenerator) addCorruptedStateFiles() error {
sm := profilemanager.ServiceManager{}
pattern := sm.GetStatePath()
pattern := statemanager.GetDefaultStatePath()
if pattern == "" {
return nil
}
@@ -639,6 +570,7 @@ func (g *BundleGenerator) addLogfile() error {
return fmt.Errorf("add client log file to zip: %w", err)
}
// add rotated log files based on logFileCount
g.addRotatedLogFiles(logDir)
stdErrLogPath := filepath.Join(logDir, errorLogFile)
@@ -667,7 +599,7 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
}
defer func() {
if err := logFile.Close(); err != nil {
log.Errorf("failed to close log file %s: %v", targetName, err)
log.Errorf("Failed to close log file %s: %v", targetName, err)
}
}()
@@ -691,21 +623,13 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error {
if err != nil {
return fmt.Errorf("open gz log file %s: %w", targetName, err)
}
defer func() {
if err := f.Close(); err != nil {
log.Errorf("failed to close gz file %s: %v", targetName, err)
}
}()
defer f.Close()
gzr, err := gzip.NewReader(f)
if err != nil {
return fmt.Errorf("create gzip reader: %w", err)
}
defer func() {
if err := gzr.Close(); err != nil {
log.Errorf("failed to close gzip reader %s: %v", targetName, err)
}
}()
defer gzr.Close()
var logReader io.Reader = gzr
if g.anonymize {
@@ -763,6 +687,7 @@ func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
return fi.ModTime().After(fj.ModTime())
})
// include up to logFileCount rotated files
maxFiles := int(g.logFileCount)
if maxFiles > len(files) {
maxFiles = len(files)
@@ -790,7 +715,7 @@ func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error
// If the reader is a file, we can get more accurate information
if f, ok := reader.(*os.File); ok {
if stat, err := f.Stat(); err != nil {
log.Tracef("failed to get file stat for %s: %v", filename, err)
log.Tracef("Failed to get file stat for %s: %v", filename, err)
} else {
header.Modified = stat.ModTime()
}
@@ -838,6 +763,89 @@ func seedFromStatus(a *anonymize.Anonymizer, status *peer.FullStatus) {
}
}
func formatRoutes(routes []netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string {
var ipv4Routes, ipv6Routes []netip.Prefix
// Separate IPv4 and IPv6 routes
for _, route := range routes {
if route.Addr().Is4() {
ipv4Routes = append(ipv4Routes, route)
} else {
ipv6Routes = append(ipv6Routes, route)
}
}
// Sort IPv4 and IPv6 routes separately
sort.Slice(ipv4Routes, func(i, j int) bool {
return ipv4Routes[i].Bits() > ipv4Routes[j].Bits()
})
sort.Slice(ipv6Routes, func(i, j int) bool {
return ipv6Routes[i].Bits() > ipv6Routes[j].Bits()
})
var builder strings.Builder
// Format IPv4 routes
builder.WriteString("IPv4 Routes:\n")
for _, route := range ipv4Routes {
formatRoute(&builder, route, anonymize, anonymizer)
}
// Format IPv6 routes
builder.WriteString("\nIPv6 Routes:\n")
for _, route := range ipv6Routes {
formatRoute(&builder, route, anonymize, anonymizer)
}
return builder.String()
}
func formatRoute(builder *strings.Builder, route netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) {
if anonymize {
anonymizedIP := anonymizer.AnonymizeIP(route.Addr())
builder.WriteString(fmt.Sprintf("%s/%d\n", anonymizedIP, route.Bits()))
} else {
builder.WriteString(fmt.Sprintf("%s\n", route))
}
}
func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string {
sort.Slice(interfaces, func(i, j int) bool {
return interfaces[i].Name < interfaces[j].Name
})
var builder strings.Builder
builder.WriteString("Network Interfaces:\n")
for _, iface := range interfaces {
builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name))
builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index))
builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU))
builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags))
addrs, err := iface.Addrs()
if err != nil {
builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err))
} else {
builder.WriteString(" Addresses:\n")
for _, addr := range addrs {
prefix, err := netip.ParsePrefix(addr.String())
if err != nil {
builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err))
continue
}
ip := prefix.Addr()
if anonymize {
ip = anonymizer.AnonymizeIP(ip)
}
builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits()))
}
}
}
return builder.String()
}
func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
defer func() {
// always nil
@@ -944,6 +952,7 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.
}
for i, ip := range peer.AllowedIps {
// Try to parse as prefix first (CIDR)
if prefix, err := netip.ParsePrefix(ip); err == nil {
anonIP := anonymizer.AnonymizeIP(prefix.Addr())
peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
@@ -1022,7 +1031,7 @@ func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.An
func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
switch record.Type {
case 1, 28:
case 1, 28: // A or AAAA record
if addr, err := netip.ParseAddr(record.RData); err == nil {
record.RData = anonymizer.AnonymizeIP(addr).String()
}

View File

@@ -17,27 +17,8 @@ import (
"github.com/google/nftables"
"github.com/google/nftables/expr"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
// addIPRules collects and adds IP rules to the archive
func (g *BundleGenerator) addIPRules() error {
log.Info("Collecting IP rules")
ipRules, err := systemops.GetIPRules()
if err != nil {
return fmt.Errorf("get IP rules: %w", err)
}
rulesContent := formatIPRulesTable(ipRules, g.anonymize, g.anonymizer)
rulesReader := strings.NewReader(rulesContent)
if err := g.addFileToZip(rulesReader, "ip_rules.txt"); err != nil {
return fmt.Errorf("add IP rules file to zip: %w", err)
}
return nil
}
const (
maxLogEntries = 100000
maxLogAge = 7 * 24 * time.Hour // Last 7 days
@@ -155,6 +136,7 @@ func (g *BundleGenerator) addFirewallRules() error {
func collectIPTablesRules() (string, error) {
var builder strings.Builder
// First try using iptables-save
saveOutput, err := collectIPTablesSave()
if err != nil {
log.Warnf("Failed to collect iptables rules using iptables-save: %v", err)
@@ -164,6 +146,7 @@ func collectIPTablesRules() (string, error) {
builder.WriteString("\n")
}
// Collect ipset information
ipsetOutput, err := collectIPSets()
if err != nil {
log.Warnf("Failed to collect ipset information: %v", err)
@@ -249,9 +232,11 @@ func getTableStatistics(table string) (string, error) {
// collectNFTablesRules attempts to collect nftables rules using either nft command or netlink
func collectNFTablesRules() (string, error) {
// First try using nft command
rules, err := collectNFTablesFromCommand()
if err != nil {
log.Debugf("Failed to collect nftables rules using nft command: %v, falling back to netlink", err)
// Fall back to netlink
rules, err = collectNFTablesFromNetlink()
if err != nil {
return "", fmt.Errorf("collect nftables rules using both nft and netlink failed: %w", err)
@@ -466,6 +451,7 @@ func formatRule(rule *nftables.Rule) string {
func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int {
curr := exprs[i]
// Handle Meta + Cmp sequence
if meta, ok := curr.(*expr.Meta); ok && i+1 < len(exprs) {
if cmp, ok := exprs[i+1].(*expr.Cmp); ok {
if formatted := formatMetaWithCmp(meta, cmp); formatted != "" {
@@ -475,6 +461,7 @@ func formatExprSequence(builder *strings.Builder, exprs []expr.Any, i int) int {
}
}
// Handle Payload + Cmp sequence
if payload, ok := curr.(*expr.Payload); ok && i+1 < len(exprs) {
if cmp, ok := exprs[i+1].(*expr.Cmp); ok {
builder.WriteString(formatPayloadWithCmp(payload, cmp))
@@ -506,13 +493,13 @@ func formatMetaWithCmp(meta *expr.Meta, cmp *expr.Cmp) string {
func formatPayloadWithCmp(p *expr.Payload, cmp *expr.Cmp) string {
if p.Base == expr.PayloadBaseNetworkHeader {
switch p.Offset {
case 12:
case 12: // Source IP
if p.Len == 4 {
return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
} else if p.Len == 2 {
return fmt.Sprintf("ip saddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
}
case 16:
case 16: // Destination IP
if p.Len == 4 {
return fmt.Sprintf("ip daddr %s %s", formatCmpOp(cmp.Op), formatIPBytes(cmp.Data))
} else if p.Len == 2 {
@@ -593,6 +580,7 @@ func formatExpr(exp expr.Any) string {
}
func formatImmediateData(data []byte) string {
// For IP addresses (4 bytes)
if len(data) == 4 {
return fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3])
}
@@ -600,21 +588,26 @@ func formatImmediateData(data []byte) string {
}
func formatMeta(e *expr.Meta) string {
// Handle source register case first (meta mark set)
if e.SourceRegister {
return fmt.Sprintf("meta %s set reg %d", formatMetaKey(e.Key), e.Register)
}
// For interface names, handle register load operation
switch e.Key {
case expr.MetaKeyIIFNAME,
expr.MetaKeyOIFNAME,
expr.MetaKeyBRIIIFNAME,
expr.MetaKeyBRIOIFNAME:
// Simply the key name with no register reference
return formatMetaKey(e.Key)
case expr.MetaKeyMARK:
// For mark operations, we want just "mark"
return "mark"
}
// For other meta keys, show as loading into register
return fmt.Sprintf("meta %s => reg %d", formatMetaKey(e.Key), e.Register)
}

View File

@@ -12,8 +12,3 @@ func (g *BundleGenerator) trySystemdLogFallback() error {
// TODO: Add BSD support
return nil
}
func (g *BundleGenerator) addIPRules() error {
// IP rules are only supported on Linux
return nil
}

View File

@@ -10,16 +10,16 @@ import (
)
func (g *BundleGenerator) addRoutes() error {
detailedRoutes, err := systemops.GetDetailedRoutesFromTable()
routes, err := systemops.GetRoutesFromTable()
if err != nil {
return fmt.Errorf("get detailed routes: %w", err)
return fmt.Errorf("get routes: %w", err)
}
routesContent := formatRoutesTable(detailedRoutes, g.anonymize, g.anonymizer)
// TODO: get routes including nexthop
routesContent := formatRoutes(routes, g.anonymize, g.anonymizer)
routesReader := strings.NewReader(routesContent)
if err := g.addFileToZip(routesReader, "routes.txt"); err != nil {
return fmt.Errorf("add routes file to zip: %w", err)
}
return nil
}

View File

@@ -1,206 +0,0 @@
package debug
import (
"fmt"
"net"
"net/netip"
"sort"
"strings"
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
"github.com/netbirdio/netbird/management/domain"
)
func formatInterfaces(interfaces []net.Interface, anonymize bool, anonymizer *anonymize.Anonymizer) string {
sort.Slice(interfaces, func(i, j int) bool {
return interfaces[i].Name < interfaces[j].Name
})
var builder strings.Builder
builder.WriteString("Network Interfaces:\n")
for _, iface := range interfaces {
builder.WriteString(fmt.Sprintf("\nInterface: %s\n", iface.Name))
builder.WriteString(fmt.Sprintf(" Index: %d\n", iface.Index))
builder.WriteString(fmt.Sprintf(" MTU: %d\n", iface.MTU))
builder.WriteString(fmt.Sprintf(" Flags: %v\n", iface.Flags))
addrs, err := iface.Addrs()
if err != nil {
builder.WriteString(fmt.Sprintf(" Addresses: Error retrieving addresses: %v\n", err))
} else {
builder.WriteString(" Addresses:\n")
for _, addr := range addrs {
prefix, err := netip.ParsePrefix(addr.String())
if err != nil {
builder.WriteString(fmt.Sprintf(" Error parsing address: %v\n", err))
continue
}
ip := prefix.Addr()
if anonymize {
ip = anonymizer.AnonymizeIP(ip)
}
builder.WriteString(fmt.Sprintf(" %s/%d\n", ip, prefix.Bits()))
}
}
}
return builder.String()
}
func formatResolvedDomains(resolvedDomains map[domain.Domain]peer.ResolvedDomainInfo, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if len(resolvedDomains) == 0 {
return "No resolved domains found.\n"
}
var builder strings.Builder
builder.WriteString("Resolved Domains:\n")
builder.WriteString("=================\n\n")
var sortedParents []domain.Domain
for parentDomain := range resolvedDomains {
sortedParents = append(sortedParents, parentDomain)
}
sort.Slice(sortedParents, func(i, j int) bool {
return sortedParents[i].SafeString() < sortedParents[j].SafeString()
})
for _, parentDomain := range sortedParents {
info := resolvedDomains[parentDomain]
parentKey := parentDomain.SafeString()
if anonymize {
parentKey = anonymizer.AnonymizeDomain(parentKey)
}
builder.WriteString(fmt.Sprintf("%s:\n", parentKey))
var sortedIPs []string
for _, prefix := range info.Prefixes {
ipStr := prefix.String()
if anonymize {
anonymizedIP := anonymizer.AnonymizeIP(prefix.Addr())
ipStr = fmt.Sprintf("%s/%d", anonymizedIP, prefix.Bits())
}
sortedIPs = append(sortedIPs, ipStr)
}
sort.Strings(sortedIPs)
for _, ipStr := range sortedIPs {
builder.WriteString(fmt.Sprintf(" %s\n", ipStr))
}
builder.WriteString("\n")
}
return builder.String()
}
func formatRoutesTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if len(detailedRoutes) == 0 {
return "No routes found.\n"
}
sort.Slice(detailedRoutes, func(i, j int) bool {
if detailedRoutes[i].Table != detailedRoutes[j].Table {
return detailedRoutes[i].Table < detailedRoutes[j].Table
}
return detailedRoutes[i].Route.Dst.String() < detailedRoutes[j].Route.Dst.String()
})
headers, rows := buildPlatformSpecificRouteTable(detailedRoutes, anonymize, anonymizer)
return formatTable("Routing Table:", headers, rows)
}
func formatRouteDestination(destination netip.Prefix, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if anonymize {
anonymizedDestIP := anonymizer.AnonymizeIP(destination.Addr())
return fmt.Sprintf("%s/%d", anonymizedDestIP, destination.Bits())
}
return destination.String()
}
func formatRouteGateway(gateway netip.Addr, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if gateway.IsValid() {
if anonymize {
return anonymizer.AnonymizeIP(gateway).String()
}
return gateway.String()
}
return "-"
}
func formatRouteInterface(iface *net.Interface) string {
if iface != nil {
return iface.Name
}
return "-"
}
func formatInterfaceIndex(index int) string {
if index <= 0 {
return "-"
}
return fmt.Sprintf("%d", index)
}
func formatRouteMetric(metric int) string {
if metric < 0 {
return "-"
}
return fmt.Sprintf("%d", metric)
}
func formatTable(title string, headers []string, rows [][]string) string {
widths := make([]int, len(headers))
for i, header := range headers {
widths[i] = len(header)
}
for _, row := range rows {
for i, cell := range row {
if len(cell) > widths[i] {
widths[i] = len(cell)
}
}
}
for i := range widths {
widths[i] += 2
}
var formatParts []string
for _, width := range widths {
formatParts = append(formatParts, fmt.Sprintf("%%-%ds", width))
}
formatStr := strings.Join(formatParts, "") + "\n"
var builder strings.Builder
builder.WriteString(title + "\n")
builder.WriteString(strings.Repeat("=", len(title)) + "\n\n")
headerArgs := make([]interface{}, len(headers))
for i, header := range headers {
headerArgs[i] = header
}
builder.WriteString(fmt.Sprintf(formatStr, headerArgs...))
separatorArgs := make([]interface{}, len(headers))
for i, width := range widths {
separatorArgs[i] = strings.Repeat("-", width-2)
}
builder.WriteString(fmt.Sprintf(formatStr, separatorArgs...))
for _, row := range rows {
rowArgs := make([]interface{}, len(row))
for i, cell := range row {
rowArgs[i] = cell
}
builder.WriteString(fmt.Sprintf(formatStr, rowArgs...))
}
return builder.String()
}

View File

@@ -1,185 +0,0 @@
//go:build linux && !android
package debug
import (
"fmt"
"net/netip"
"sort"
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
func formatIPRulesTable(ipRules []systemops.IPRule, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if len(ipRules) == 0 {
return "No IP rules found.\n"
}
sort.Slice(ipRules, func(i, j int) bool {
return ipRules[i].Priority < ipRules[j].Priority
})
columnConfig := detectIPRuleColumns(ipRules)
headers := buildIPRuleHeaders(columnConfig)
rows := buildIPRuleRows(ipRules, columnConfig, anonymize, anonymizer)
return formatTable("IP Rules:", headers, rows)
}
type ipRuleColumnConfig struct {
hasInvert, hasTo, hasMark, hasIIF, hasOIF, hasSuppressPlen bool
}
func detectIPRuleColumns(ipRules []systemops.IPRule) ipRuleColumnConfig {
var config ipRuleColumnConfig
for _, rule := range ipRules {
if rule.Invert {
config.hasInvert = true
}
if rule.To.IsValid() {
config.hasTo = true
}
if rule.Mark != 0 {
config.hasMark = true
}
if rule.IIF != "" {
config.hasIIF = true
}
if rule.OIF != "" {
config.hasOIF = true
}
if rule.SuppressPlen >= 0 {
config.hasSuppressPlen = true
}
}
return config
}
func buildIPRuleHeaders(config ipRuleColumnConfig) []string {
var headers []string
headers = append(headers, "Priority")
if config.hasInvert {
headers = append(headers, "Not")
}
headers = append(headers, "From")
if config.hasTo {
headers = append(headers, "To")
}
if config.hasMark {
headers = append(headers, "FWMark")
}
if config.hasIIF {
headers = append(headers, "IIF")
}
if config.hasOIF {
headers = append(headers, "OIF")
}
headers = append(headers, "Table")
headers = append(headers, "Action")
if config.hasSuppressPlen {
headers = append(headers, "SuppressPlen")
}
return headers
}
func buildIPRuleRows(ipRules []systemops.IPRule, config ipRuleColumnConfig, anonymize bool, anonymizer *anonymize.Anonymizer) [][]string {
var rows [][]string
for _, rule := range ipRules {
row := buildSingleIPRuleRow(rule, config, anonymize, anonymizer)
rows = append(rows, row)
}
return rows
}
func buildSingleIPRuleRow(rule systemops.IPRule, config ipRuleColumnConfig, anonymize bool, anonymizer *anonymize.Anonymizer) []string {
var row []string
row = append(row, fmt.Sprintf("%d", rule.Priority))
if config.hasInvert {
row = append(row, formatIPRuleInvert(rule.Invert))
}
row = append(row, formatIPRuleAddress(rule.From, "all", anonymize, anonymizer))
if config.hasTo {
row = append(row, formatIPRuleAddress(rule.To, "-", anonymize, anonymizer))
}
if config.hasMark {
row = append(row, formatIPRuleMark(rule.Mark, rule.Mask))
}
if config.hasIIF {
row = append(row, formatIPRuleInterface(rule.IIF))
}
if config.hasOIF {
row = append(row, formatIPRuleInterface(rule.OIF))
}
row = append(row, rule.Table)
row = append(row, formatIPRuleAction(rule.Action))
if config.hasSuppressPlen {
row = append(row, formatIPRuleSuppressPlen(rule.SuppressPlen))
}
return row
}
func formatIPRuleInvert(invert bool) string {
if invert {
return "not"
}
return "-"
}
func formatIPRuleAction(action string) string {
if action == "unspec" {
return "lookup"
}
return action
}
func formatIPRuleSuppressPlen(suppressPlen int) string {
if suppressPlen >= 0 {
return fmt.Sprintf("%d", suppressPlen)
}
return "-"
}
func formatIPRuleAddress(prefix netip.Prefix, defaultVal string, anonymize bool, anonymizer *anonymize.Anonymizer) string {
if !prefix.IsValid() {
return defaultVal
}
if anonymize {
anonymizedIP := anonymizer.AnonymizeIP(prefix.Addr())
return fmt.Sprintf("%s/%d", anonymizedIP, prefix.Bits())
}
return prefix.String()
}
func formatIPRuleMark(mark, mask uint32) string {
if mark == 0 {
return "-"
}
if mask != 0 {
return fmt.Sprintf("0x%x/0x%x", mark, mask)
}
return fmt.Sprintf("0x%x", mark)
}
func formatIPRuleInterface(iface string) string {
if iface == "" {
return "-"
}
return iface
}

View File

@@ -1,27 +0,0 @@
//go:build !windows
package debug
import (
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
// buildPlatformSpecificRouteTable builds headers and rows for non-Windows platforms
func buildPlatformSpecificRouteTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) ([]string, [][]string) {
headers := []string{"Destination", "Gateway", "Interface", "Idx", "Metric", "Protocol", "Scope", "Type", "Table", "Flags"}
var rows [][]string
for _, route := range detailedRoutes {
destStr := formatRouteDestination(route.Route.Dst, anonymize, anonymizer)
gatewayStr := formatRouteGateway(route.Route.Gw, anonymize, anonymizer)
interfaceStr := formatRouteInterface(route.Route.Interface)
indexStr := formatInterfaceIndex(route.InterfaceIndex)
metricStr := formatRouteMetric(route.Metric)
row := []string{destStr, gatewayStr, interfaceStr, indexStr, metricStr, route.Protocol, route.Scope, route.Type, route.Table, route.Flags}
rows = append(rows, row)
}
return headers, rows
}

View File

@@ -1,37 +0,0 @@
//go:build windows
package debug
import (
"fmt"
"github.com/netbirdio/netbird/client/anonymize"
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
)
// buildPlatformSpecificRouteTable builds headers and rows for Windows with interface metrics
func buildPlatformSpecificRouteTable(detailedRoutes []systemops.DetailedRoute, anonymize bool, anonymizer *anonymize.Anonymizer) ([]string, [][]string) {
headers := []string{"Destination", "Gateway", "Interface", "Idx", "Metric", "If Metric", "Protocol", "Age", "Origin"}
var rows [][]string
for _, route := range detailedRoutes {
destStr := formatRouteDestination(route.Route.Dst, anonymize, anonymizer)
gatewayStr := formatRouteGateway(route.Route.Gw, anonymize, anonymizer)
interfaceStr := formatRouteInterface(route.Route.Interface)
indexStr := formatInterfaceIndex(route.InterfaceIndex)
metricStr := formatRouteMetric(route.Metric)
ifMetricStr := formatInterfaceMetric(route.InterfaceMetric)
row := []string{destStr, gatewayStr, interfaceStr, indexStr, metricStr, ifMetricStr, route.Protocol, route.Scope, route.Type}
rows = append(rows, row)
}
return headers, rows
}
func formatInterfaceMetric(metric int) string {
if metric < 0 {
return "-"
}
return fmt.Sprintf("%d", metric)
}

View File

@@ -4,8 +4,8 @@ package dns
import (
"fmt"
"net/netip"
"os"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
@@ -15,6 +15,9 @@ const (
defaultResolvConfPath = "/etc/resolv.conf"
)
var timeoutRegex = regexp.MustCompile(`timeout:\d+`)
var attemptsRegex = regexp.MustCompile(`attempts:\d+`)
type resolvConf struct {
nameServers []string
searchDomains []string
@@ -105,9 +108,40 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
return rconf, nil
}
// prepareOptionsWithTimeout appends timeout to existing options if it doesn't exist,
// otherwise it adds a new option with timeout and attempts.
func prepareOptionsWithTimeout(input []string, timeout int, attempts int) []string {
configs := make([]string, len(input))
copy(configs, input)
for i, config := range configs {
if strings.HasPrefix(config, "options") {
config = strings.ReplaceAll(config, "rotate", "")
config = strings.Join(strings.Fields(config), " ")
if strings.Contains(config, "timeout:") {
config = timeoutRegex.ReplaceAllString(config, fmt.Sprintf("timeout:%d", timeout))
} else {
config = strings.Replace(config, "options ", fmt.Sprintf("options timeout:%d ", timeout), 1)
}
if strings.Contains(config, "attempts:") {
config = attemptsRegex.ReplaceAllString(config, fmt.Sprintf("attempts:%d", attempts))
} else {
config = strings.Replace(config, "options ", fmt.Sprintf("options attempts:%d ", attempts), 1)
}
configs[i] = config
return configs
}
}
return append(configs, fmt.Sprintf("options timeout:%d attempts:%d", timeout, attempts))
}
// removeFirstNbNameserver removes the given nameserver from the given file if it is in the first position
// and writes the file back to the original location
func removeFirstNbNameserver(filename string, nameserverIP netip.Addr) error {
func removeFirstNbNameserver(filename, nameserverIP string) error {
resolvConf, err := parseResolvConfFile(filename)
if err != nil {
return fmt.Errorf("parse backup resolv.conf: %w", err)
@@ -117,7 +151,7 @@ func removeFirstNbNameserver(filename string, nameserverIP netip.Addr) error {
return fmt.Errorf("read %s: %w", filename, err)
}
if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP.String() {
if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP {
newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1)
stat, err := os.Stat(filename)

View File

@@ -3,13 +3,11 @@
package dns
import (
"net/netip"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_parseResolvConf(t *testing.T) {
@@ -177,6 +175,52 @@ nameserver 192.168.0.1
}
}
func TestPrepareOptionsWithTimeout(t *testing.T) {
tests := []struct {
name string
others []string
timeout int
attempts int
expected []string
}{
{
name: "Append new options with timeout and attempts",
others: []string{"some config"},
timeout: 2,
attempts: 2,
expected: []string{"some config", "options timeout:2 attempts:2"},
},
{
name: "Modify existing options to exclude rotate and include timeout and attempts",
others: []string{"some config", "options rotate someother"},
timeout: 3,
attempts: 2,
expected: []string{"some config", "options attempts:2 timeout:3 someother"},
},
{
name: "Existing options with timeout and attempts are updated",
others: []string{"some config", "options timeout:4 attempts:3"},
timeout: 5,
attempts: 4,
expected: []string{"some config", "options timeout:5 attempts:4"},
},
{
name: "Modify existing options, add missing attempts before timeout",
others: []string{"some config", "options timeout:4"},
timeout: 4,
attempts: 3,
expected: []string{"some config", "options attempts:3 timeout:4"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := prepareOptionsWithTimeout(tc.others, tc.timeout, tc.attempts)
assert.Equal(t, tc.expected, result)
})
}
}
func TestRemoveFirstNbNameserver(t *testing.T) {
testCases := []struct {
name string
@@ -248,9 +292,7 @@ search localdomain`,
err := os.WriteFile(tempFile, []byte(tc.content), 0644)
assert.NoError(t, err)
ip, err := netip.ParseAddr(tc.ipToRemove)
require.NoError(t, err, "Failed to parse IP address")
err = removeFirstNbNameserver(tempFile, ip)
err = removeFirstNbNameserver(tempFile, tc.ipToRemove)
assert.NoError(t, err)
content, err := os.ReadFile(tempFile)

View File

@@ -3,7 +3,6 @@
package dns
import (
"net/netip"
"path"
"path/filepath"
"sync"
@@ -23,7 +22,7 @@ var (
}
)
type repairConfFn func([]string, netip.Addr, *resolvConf, *statemanager.Manager) error
type repairConfFn func([]string, string, *resolvConf, *statemanager.Manager) error
type repair struct {
operationFile string
@@ -43,7 +42,7 @@ func newRepair(operationFile string, updateFn repairConfFn) *repair {
}
}
func (f *repair) watchFileChanges(nbSearchDomains []string, nbNameserverIP netip.Addr, stateManager *statemanager.Manager) {
func (f *repair) watchFileChanges(nbSearchDomains []string, nbNameserverIP string, stateManager *statemanager.Manager) {
if f.inotify != nil {
return
}
@@ -137,7 +136,7 @@ func (f *repair) isEventRelevant(event fsnotify.Event) bool {
// nbParamsAreMissing checks if the resolv.conf file contains all the parameters that NetBird needs
// check the NetBird related nameserver IP at the first place
// check the NetBird related search domains in the search domains list
func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP netip.Addr, rConf *resolvConf) bool {
func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP string, rConf *resolvConf) bool {
if !isContains(nbSearchDomains, rConf.searchDomains) {
return true
}
@@ -146,7 +145,7 @@ func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP netip.Addr, rCon
return true
}
if rConf.nameServers[0] != nbNameserverIP.String() {
if rConf.nameServers[0] != nbNameserverIP {
return true
}

View File

@@ -4,7 +4,6 @@ package dns
import (
"context"
"net/netip"
"os"
"path/filepath"
"testing"
@@ -15,7 +14,7 @@ import (
)
func TestMain(m *testing.M) {
_ = util.InitLog("debug", util.LogConsole)
_ = util.InitLog("debug", "console")
code := m.Run()
os.Exit(code)
}
@@ -106,14 +105,14 @@ nameserver 8.8.8.8`,
var changed bool
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
updateFn := func([]string, netip.Addr, *resolvConf, *statemanager.Manager) error {
updateFn := func([]string, string, *resolvConf, *statemanager.Manager) error {
changed = true
cancel()
return nil
}
r := newRepair(operationFile, updateFn)
r.watchFileChanges([]string{"netbird.cloud"}, netip.MustParseAddr("10.0.0.1"), nil)
r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1", nil)
err = os.WriteFile(operationFile, []byte(tt.touchedConfContent), 0755)
if err != nil {
@@ -153,14 +152,14 @@ searchdomain netbird.cloud something`
var changed bool
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
updateFn := func([]string, netip.Addr, *resolvConf, *statemanager.Manager) error {
updateFn := func([]string, string, *resolvConf, *statemanager.Manager) error {
changed = true
cancel()
return nil
}
r := newRepair(tmpLink, updateFn)
r.watchFileChanges([]string{"netbird.cloud"}, netip.MustParseAddr("10.0.0.1"), nil)
r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1", nil)
err = os.WriteFile(tmpLink, []byte(modifyContent), 0755)
if err != nil {

View File

@@ -8,6 +8,7 @@ import (
"net/netip"
"os"
"strings"
"time"
log "github.com/sirupsen/logrus"
@@ -17,7 +18,7 @@ import (
const (
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
fileGeneratedResolvConfContentHeaderNextLine = fileGeneratedResolvConfContentHeader + `
# The original file can be restored from ` + fileDefaultResolvConfBackupLocation + "\n\n"
# If needed you can restore the original file by copying back ` + fileDefaultResolvConfBackupLocation + "\n\n"
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
@@ -25,11 +26,16 @@ const (
fileMaxNumberOfSearchDomains = 6
)
const (
dnsFailoverTimeout = 4 * time.Second
dnsFailoverAttempts = 1
)
type fileConfigurator struct {
repair *repair
originalPerms os.FileMode
nbNameserverIP netip.Addr
originalNameservers []string
repair *repair
originalPerms os.FileMode
nbNameserverIP string
}
func newFileConfigurator() (*fileConfigurator, error) {
@@ -43,9 +49,22 @@ func (f *fileConfigurator) supportCustomPort() bool {
}
func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
if !f.isBackupFileExist() {
if err := f.backup(); err != nil {
return fmt.Errorf("backup resolv.conf: %w", err)
backupFileExist := f.isBackupFileExist()
if !config.RouteAll {
if backupFileExist {
f.repair.stopWatchFileChanges()
err := f.restore()
if err != nil {
return fmt.Errorf("restoring the original resolv.conf file return err: %w", err)
}
}
return ErrRouteAllWithoutNameserverGroup
}
if !backupFileExist {
err := f.backup()
if err != nil {
return fmt.Errorf("unable to backup the resolv.conf file: %w", err)
}
}
@@ -57,8 +76,6 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st
log.Errorf("could not read original search domains from %s: %s", fileDefaultResolvConfBackupLocation, err)
}
f.originalNameservers = resolvConf.nameServers
f.repair.stopWatchFileChanges()
err = f.updateConfig(nbSearchDomains, f.nbNameserverIP, resolvConf, stateManager)
@@ -69,19 +86,15 @@ func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *st
return nil
}
// getOriginalNameservers returns the nameservers that were found in the original resolv.conf
func (f *fileConfigurator) getOriginalNameservers() []string {
return f.originalNameservers
}
func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP netip.Addr, cfg *resolvConf, stateManager *statemanager.Manager) error {
func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP string, cfg *resolvConf, stateManager *statemanager.Manager) error {
searchDomainList := mergeSearchDomains(nbSearchDomains, cfg.searchDomains)
nameServers := generateNsList(nbNameserverIP, cfg)
options := prepareOptionsWithTimeout(cfg.others, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts)
buf := prepareResolvConfContent(
searchDomainList,
[]string{nbNameserverIP.String()},
cfg.others,
)
nameServers,
options)
log.Debugf("creating managed file %s", defaultResolvConfPath)
err := os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms)
@@ -184,28 +197,38 @@ func restoreResolvConfFile() error {
return nil
}
// generateNsList generates a list of nameservers from the config and adds the primary nameserver to the beginning of the list
func generateNsList(nbNameserverIP string, cfg *resolvConf) []string {
ns := make([]string, 1, len(cfg.nameServers)+1)
ns[0] = nbNameserverIP
for _, cfgNs := range cfg.nameServers {
if nbNameserverIP != cfgNs {
ns = append(ns, cfgNs)
}
}
return ns
}
func prepareResolvConfContent(searchDomains, nameServers, others []string) bytes.Buffer {
var buf bytes.Buffer
buf.WriteString(fileGeneratedResolvConfContentHeaderNextLine)
for _, cfgLine := range others {
buf.WriteString(cfgLine)
buf.WriteByte('\n')
buf.WriteString("\n")
}
if len(searchDomains) > 0 {
buf.WriteString("search ")
buf.WriteString(strings.Join(searchDomains, " "))
buf.WriteByte('\n')
buf.WriteString("\n")
}
for _, ns := range nameServers {
buf.WriteString("nameserver ")
buf.WriteString(ns)
buf.WriteByte('\n')
buf.WriteString("\n")
}
return buf
}

View File

@@ -15,7 +15,6 @@ const (
PriorityDNSRoute = 75
PriorityUpstream = 50
PriorityDefault = 1
PriorityFallback = -100
)
type SubdomainMatcher interface {
@@ -192,7 +191,7 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
// No handler matched or all handlers passed
log.Tracef("no handler found for domain=%s", qname)
resp := &dns.Msg{}
resp.SetRcode(r, dns.RcodeRefused)
resp.SetRcode(r, dns.RcodeNameError)
if err := w.WriteMsg(resp); err != nil {
log.Errorf("failed to write DNS response: %v", err)
}

View File

@@ -11,6 +11,8 @@ import (
nbdns "github.com/netbirdio/netbird/dns"
)
var ErrRouteAllWithoutNameserverGroup = fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
const (
ipv4ReverseZone = ".in-addr.arpa."
ipv6ReverseZone = ".ip6.arpa."
@@ -25,14 +27,14 @@ type hostManager interface {
type SystemDNSSettings struct {
Domains []string
ServerIP netip.Addr
ServerIP string
ServerPort int
}
type HostDNSConfig struct {
Domains []DomainConfig `json:"domains"`
RouteAll bool `json:"routeAll"`
ServerIP netip.Addr `json:"serverIP"`
ServerIP string `json:"serverIP"`
ServerPort int `json:"serverPort"`
}
@@ -87,7 +89,7 @@ func newNoopHostMocker() hostManager {
}
}
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip netip.Addr, port int) HostDNSConfig {
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostDNSConfig {
config := HostDNSConfig{
RouteAll: false,
ServerIP: ip,

View File

@@ -7,7 +7,7 @@ import (
"bytes"
"fmt"
"io"
"net/netip"
"net"
"os/exec"
"strconv"
"strings"
@@ -165,13 +165,13 @@ func (s *systemConfigurator) removeKeyFromSystemConfig(key string) error {
}
func (s *systemConfigurator) addLocalDNS() error {
if !s.systemDNSSettings.ServerIP.IsValid() || len(s.systemDNSSettings.Domains) == 0 {
if s.systemDNSSettings.ServerIP == "" || len(s.systemDNSSettings.Domains) == 0 {
err := s.recordSystemDNSSettings(true)
log.Errorf("Unable to get system DNS configuration")
return err
}
localKey := getKeyWithInput(netbirdDNSStateKeyFormat, localSuffix)
if s.systemDNSSettings.ServerIP.IsValid() && len(s.systemDNSSettings.Domains) != 0 {
if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 {
err := s.addSearchDomains(localKey, strings.Join(s.systemDNSSettings.Domains, " "), s.systemDNSSettings.ServerIP, s.systemDNSSettings.ServerPort)
if err != nil {
return fmt.Errorf("couldn't add local network DNS conf: %w", err)
@@ -184,7 +184,7 @@ func (s *systemConfigurator) addLocalDNS() error {
}
func (s *systemConfigurator) recordSystemDNSSettings(force bool) error {
if s.systemDNSSettings.ServerIP.IsValid() && len(s.systemDNSSettings.Domains) != 0 && !force {
if s.systemDNSSettings.ServerIP != "" && len(s.systemDNSSettings.Domains) != 0 && !force {
return nil
}
@@ -238,8 +238,8 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
} else if inServerAddressesArray {
address := strings.Split(line, " : ")[1]
if ip, err := netip.ParseAddr(address); err == nil && ip.Is4() {
dnsSettings.ServerIP = ip
if ip := net.ParseIP(address); ip != nil && ip.To4() != nil {
dnsSettings.ServerIP = address
inServerAddressesArray = false // Stop reading after finding the first IPv4 address
}
}
@@ -250,12 +250,12 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
}
// default to 53 port
dnsSettings.ServerPort = defaultPort
dnsSettings.ServerPort = 53
return dnsSettings, nil
}
func (s *systemConfigurator) addSearchDomains(key, domains string, ip netip.Addr, port int) error {
func (s *systemConfigurator) addSearchDomains(key, domains string, ip string, port int) error {
err := s.addDNSState(key, domains, ip, port, true)
if err != nil {
return fmt.Errorf("add dns state: %w", err)
@@ -268,7 +268,7 @@ func (s *systemConfigurator) addSearchDomains(key, domains string, ip netip.Addr
return nil
}
func (s *systemConfigurator) addMatchDomains(key, domains string, dnsServer netip.Addr, port int) error {
func (s *systemConfigurator) addMatchDomains(key, domains, dnsServer string, port int) error {
err := s.addDNSState(key, domains, dnsServer, port, false)
if err != nil {
return fmt.Errorf("add dns state: %w", err)
@@ -281,14 +281,14 @@ func (s *systemConfigurator) addMatchDomains(key, domains string, dnsServer neti
return nil
}
func (s *systemConfigurator) addDNSState(state, domains string, dnsServer netip.Addr, port int, enableSearch bool) error {
func (s *systemConfigurator) addDNSState(state, domains, dnsServer string, port int, enableSearch bool) error {
noSearch := "1"
if enableSearch {
noSearch = "0"
}
lines := buildAddCommandLine(keySupplementalMatchDomains, arraySymbol+domains)
lines += buildAddCommandLine(keySupplementalMatchDomainsNoSearch, digitSymbol+noSearch)
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer.String())
lines += buildAddCommandLine(keyServerAddresses, arraySymbol+dnsServer)
lines += buildAddCommandLine(keyServerPort, digitSymbol+strconv.Itoa(port))
addDomainCommand := buildCreateStateWithOperation(state, lines)

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"io"
"net/netip"
"os/exec"
"strings"
"syscall"
@@ -211,8 +210,8 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
return nil
}
func (r *registryConfigurator) addDNSSetupForAll(ip netip.Addr) error {
if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip.String()); err != nil {
func (r *registryConfigurator) addDNSSetupForAll(ip string) error {
if err := r.setInterfaceRegistryKeyStringValue(interfaceConfigNameServerKey, ip); err != nil {
return fmt.Errorf("adding dns setup for all failed: %w", err)
}
r.routingAll = true
@@ -220,7 +219,7 @@ func (r *registryConfigurator) addDNSSetupForAll(ip netip.Addr) error {
return nil
}
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr) error {
func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip string) error {
// if the gpo key is present, we need to put our DNS settings there, otherwise our config might be ignored
// see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpnrpt/8cc31cb9-20cb-4140-9e85-3e08703b4745
if r.gpo {
@@ -242,7 +241,7 @@ func (r *registryConfigurator) addDNSMatchPolicy(domains []string, ip netip.Addr
}
// configureDNSPolicy handles the actual configuration of a DNS policy at the specified path
func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip netip.Addr) error {
func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []string, ip string) error {
if err := removeRegistryKeyFromDNSPolicyConfig(policyPath); err != nil {
return fmt.Errorf("remove existing dns policy: %w", err)
}
@@ -261,7 +260,7 @@ func (r *registryConfigurator) configureDNSPolicy(policyPath string, domains []s
return fmt.Errorf("set %s: %w", dnsPolicyConfigNameKey, err)
}
if err := regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip.String()); err != nil {
if err := regKey.SetStringValue(dnsPolicyConfigGenericDNSServersKey, ip); err != nil {
return fmt.Errorf("set %s: %w", dnsPolicyConfigGenericDNSServersKey, err)
}

View File

@@ -2,7 +2,6 @@ package dns
import (
"fmt"
"net/netip"
"github.com/miekg/dns"
@@ -46,8 +45,8 @@ func (m *MockServer) Stop() {
}
}
func (m *MockServer) DnsIP() netip.Addr {
return netip.MustParseAddr("100.10.254.255")
func (m *MockServer) DnsIP() string {
return ""
}
func (m *MockServer) OnUpdatedHostDNSServer(strings []string) {

View File

@@ -110,7 +110,11 @@ func (n *networkManagerDbusConfigurator) applyDNSConfig(config HostDNSConfig, st
connSettings.cleanDeprecatedSettings()
convDNSIP := binary.LittleEndian.Uint32(config.ServerIP.AsSlice())
dnsIP, err := netip.ParseAddr(config.ServerIP)
if err != nil {
return fmt.Errorf("unable to parse ip address, error: %w", err)
}
convDNSIP := binary.LittleEndian.Uint32(dnsIP.AsSlice())
connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP})
var (
searchDomains []string

View File

@@ -46,9 +46,9 @@ type resolvconf struct {
func detectResolvconfType() (resolvconfType, error) {
cmd := exec.Command(resolvconfCommand, "--version")
out, err := cmd.CombinedOutput()
out, err := cmd.Output()
if err != nil {
return typeOpenresolv, fmt.Errorf("determine resolvconf type: %w", err)
return typeOpenresolv, fmt.Errorf("failed to determine resolvconf type: %w", err)
}
if strings.Contains(string(out), "openresolv") {
@@ -66,7 +66,7 @@ func newResolvConfConfigurator(wgInterface string) (*resolvconf, error) {
implType, err := detectResolvconfType()
if err != nil {
log.Warnf("failed to detect resolvconf type, defaulting to openresolv: %v", err)
implType = typeResolvconf
implType = typeOpenresolv
} else {
log.Infof("detected resolvconf type: %v", implType)
}
@@ -85,14 +85,24 @@ func (r *resolvconf) supportCustomPort() bool {
}
func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
var err error
if !config.RouteAll {
err = r.restoreHostDNS()
if err != nil {
log.Errorf("restore host dns: %s", err)
}
return ErrRouteAllWithoutNameserverGroup
}
searchDomainList := searchDomains(config)
searchDomainList = mergeSearchDomains(searchDomainList, r.originalSearchDomains)
options := prepareOptionsWithTimeout(r.othersConfigs, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts)
buf := prepareResolvConfContent(
searchDomainList,
[]string{config.ServerIP.String()},
r.othersConfigs,
)
append([]string{config.ServerIP}, r.originalNameServers...),
options)
state := &ShutdownState{
ManagerType: resolvConfManager,
@@ -102,7 +112,8 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman
log.Errorf("failed to update shutdown state: %s", err)
}
if err := r.applyConfig(buf); err != nil {
err = r.applyConfig(buf)
if err != nil {
return fmt.Errorf("apply config: %w", err)
}
@@ -110,10 +121,6 @@ func (r *resolvconf) applyDNSConfig(config HostDNSConfig, stateManager *stateman
return nil
}
func (r *resolvconf) getOriginalNameservers() []string {
return r.originalNameServers
}
func (r *resolvconf) restoreHostDNS() error {
var cmd *exec.Cmd
@@ -150,7 +157,7 @@ func (r *resolvconf) applyConfig(content bytes.Buffer) error {
}
cmd.Stdin = &content
out, err := cmd.CombinedOutput()
out, err := cmd.Output()
log.Tracef("resolvconf output: %s", out)
if err != nil {
return fmt.Errorf("applying resolvconf configuration for %s interface: %w", r.ifaceName, err)

View File

@@ -20,6 +20,7 @@ import (
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager"
cProto "github.com/netbirdio/netbird/client/proto"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/domain"
)
@@ -40,7 +41,7 @@ type Server interface {
DeregisterHandler(domains domain.List, priority int)
Initialize() error
Stop()
DnsIP() netip.Addr
DnsIP() string
UpdateDNSServer(serial uint64, update nbdns.Config) error
OnUpdatedHostDNSServer(strings []string)
SearchDomains() []string
@@ -52,18 +53,10 @@ type nsGroupsByDomain struct {
groups []*nbdns.NameServerGroup
}
// hostManagerWithOriginalNS extends the basic hostManager interface
type hostManagerWithOriginalNS interface {
hostManager
getOriginalNameservers() []string
}
// DefaultServer dns server object
type DefaultServer struct {
ctx context.Context
ctxCancel context.CancelFunc
// disableSys disables system DNS management (e.g., /etc/resolv.conf updates) while keeping the DNS service running.
// This is different from ServiceEnable=false from management which completely disables the DNS service.
ctx context.Context
ctxCancel context.CancelFunc
disableSys bool
mux sync.Mutex
service service
@@ -190,7 +183,6 @@ func newDefaultServer(
statusRecorder: statusRecorder,
stateManager: stateManager,
hostsDNSHolder: newHostsDNSHolder(),
hostManager: &noopHostConfigurator{},
}
// register with root zone, handler chain takes care of the routing
@@ -223,7 +215,6 @@ func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, p
log.Warn("skipping empty domain")
continue
}
s.handlerChain.AddHandler(domain, handler, priority)
}
}
@@ -262,8 +253,7 @@ func (s *DefaultServer) Initialize() (err error) {
s.mux.Lock()
defer s.mux.Unlock()
if !s.isUsingNoopHostManager() {
// already initialized
if s.hostManager != nil {
return nil
}
@@ -276,19 +266,19 @@ func (s *DefaultServer) Initialize() (err error) {
s.stateManager.RegisterState(&ShutdownState{})
// Keep using noop host manager if dns off requested or running in netstack mode.
// use noop host manager if requested or running in netstack mode.
// Netstack mode currently doesn't have a way to receive DNS requests.
// TODO: Use listener on localhost in netstack mode when running as root.
if s.disableSys || netstack.IsEnabled() {
log.Info("system DNS is disabled, not setting up host manager")
s.hostManager = &noopHostConfigurator{}
return nil
}
hostManager, err := s.initialize()
s.hostManager, err = s.initialize()
if err != nil {
return fmt.Errorf("initialize: %w", err)
}
s.hostManager = hostManager
return nil
}
@@ -296,50 +286,32 @@ func (s *DefaultServer) Initialize() (err error) {
//
// When kernel space interface used it return real DNS server listener IP address
// For bind interface, fake DNS resolver address returned (second last IP address from Nebird network)
func (s *DefaultServer) DnsIP() netip.Addr {
func (s *DefaultServer) DnsIP() string {
return s.service.RuntimeIP()
}
// Stop stops the server
func (s *DefaultServer) Stop() {
s.ctxCancel()
s.mux.Lock()
defer s.mux.Unlock()
s.ctxCancel()
if err := s.disableDNS(); err != nil {
log.Errorf("failed to disable DNS: %v", err)
if s.hostManager != nil {
if err := s.hostManager.restoreHostDNS(); err != nil {
log.Error("failed to restore host DNS settings: ", err)
} else if err := s.stateManager.DeleteState(&ShutdownState{}); err != nil {
log.Errorf("failed to delete shutdown dns state: %v", err)
}
}
s.service.Stop()
maps.Clear(s.extraDomains)
}
func (s *DefaultServer) disableDNS() error {
defer s.service.Stop()
if s.isUsingNoopHostManager() {
return nil
}
// Deregister original nameservers if they were registered as fallback
if srvs, ok := s.hostManager.(hostManagerWithOriginalNS); ok && len(srvs.getOriginalNameservers()) > 0 {
log.Debugf("deregistering original nameservers as fallback handlers")
s.deregisterHandler([]string{nbdns.RootZone}, PriorityFallback)
}
if err := s.hostManager.restoreHostDNS(); err != nil {
log.Errorf("failed to restore host DNS settings: %v", err)
} else if err := s.stateManager.DeleteState(&ShutdownState{}); err != nil {
log.Errorf("failed to delete shutdown dns state: %v", err)
}
s.hostManager = &noopHostConfigurator{}
return nil
}
// OnUpdatedHostDNSServer update the DNS servers addresses for root zones
// It will be applied if the mgm server do not enforce DNS settings for root zone
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) {
s.hostsDNSHolder.set(hostsDnsList)
@@ -376,6 +348,10 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro
s.mux.Lock()
defer s.mux.Unlock()
if s.hostManager == nil {
return fmt.Errorf("dns service is not initialized yet")
}
hash, err := hashstructure.Hash(update, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
@@ -433,14 +409,13 @@ func (s *DefaultServer) ProbeAvailability() {
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
// is the service should be Disabled, we stop the listener or fake resolver
// and proceed with a regular update to clean up the handlers and records
if update.ServiceEnable {
if err := s.enableDNS(); err != nil {
log.Errorf("failed to enable DNS: %v", err)
if err := s.service.Listen(); err != nil {
log.Errorf("failed to start DNS service: %v", err)
}
} else if !s.permanent {
if err := s.disableDNS(); err != nil {
log.Errorf("failed to disable DNS: %v", err)
}
s.service.Stop()
}
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
@@ -485,40 +460,11 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
return nil
}
func (s *DefaultServer) isUsingNoopHostManager() bool {
_, isNoop := s.hostManager.(*noopHostConfigurator)
return isNoop
}
func (s *DefaultServer) enableDNS() error {
if err := s.service.Listen(); err != nil {
return fmt.Errorf("start DNS service: %w", err)
}
if !s.isUsingNoopHostManager() {
return nil
}
if s.disableSys || netstack.IsEnabled() {
return nil
}
log.Info("DNS service re-enabled, initializing host manager")
if !s.service.RuntimeIP().IsValid() {
return errors.New("DNS service runtime IP is invalid")
}
hostManager, err := s.initialize()
if err != nil {
return fmt.Errorf("initialize host manager: %w", err)
}
s.hostManager = hostManager
return nil
}
func (s *DefaultServer) applyHostConfig() {
if s.hostManager == nil {
return
}
// prevent reapplying config if we're shutting down
if s.ctx.Err() != nil {
return
@@ -547,56 +493,25 @@ func (s *DefaultServer) applyHostConfig() {
if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil {
log.Errorf("failed to apply DNS host manager update: %v", err)
s.handleErrNoGroupaAll(err)
}
s.registerFallback(config)
}
// registerFallback registers original nameservers as low-priority fallback handlers
func (s *DefaultServer) registerFallback(config HostDNSConfig) {
hostMgrWithNS, ok := s.hostManager.(hostManagerWithOriginalNS)
if !ok {
func (s *DefaultServer) handleErrNoGroupaAll(err error) {
if !errors.Is(ErrRouteAllWithoutNameserverGroup, err) {
return
}
originalNameservers := hostMgrWithNS.getOriginalNameservers()
if len(originalNameservers) == 0 {
if s.statusRecorder == nil {
return
}
log.Infof("registering original nameservers %v as upstream handlers with priority %d", originalNameservers, PriorityFallback)
handler, err := newUpstreamResolver(
s.ctx,
s.wgInterface.Name(),
s.wgInterface.Address().IP,
s.wgInterface.Address().Network,
s.statusRecorder,
s.hostsDNSHolder,
nbdns.RootZone,
s.statusRecorder.PublishEvent(
cProto.SystemEvent_WARNING, cProto.SystemEvent_DNS,
"The host dns manager does not support match domains",
"The host dns manager does not support match domains without a catch-all nameserver group.",
map[string]string{"manager": s.hostManager.string()},
)
if err != nil {
log.Errorf("failed to create upstream resolver for original nameservers: %v", err)
return
}
for _, ns := range originalNameservers {
if ns == config.ServerIP.String() {
log.Debugf("skipping original nameserver %s as it is the same as the server IP %s", ns, config.ServerIP)
continue
}
ns = fmt.Sprintf("%s:%d", ns, defaultPort)
if ip, err := netip.ParseAddr(ns); err == nil && ip.Is6() {
ns = fmt.Sprintf("[%s]:%d", ns, defaultPort)
}
handler.upstreamServers = append(handler.upstreamServers, ns)
}
handler.deactivate = func(error) { /* always active */ }
handler.reactivate = func() { /* always active */ }
s.registerHandler([]string{nbdns.RootZone}, handler, PriorityFallback)
}
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, []nbdns.SimpleRecord, error) {
@@ -673,8 +588,14 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
// Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts
priority := basePriority - i
// Check if we're about to overlap with the next priority tier
if s.leaksPriority(domainGroup, basePriority, priority) {
// Check if we're about to overlap with the next priority tier.
// This boundary check ensures that the priority of upstream handlers does not conflict
// with the default priority tier. By decrementing the priority for each handler, we avoid
// overlaps, but if the calculated priority falls into the default tier, we skip the remaining
// handlers to maintain the integrity of the priority system.
if basePriority == PriorityUpstream && priority <= PriorityDefault {
log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers",
domainGroup.domain, PriorityUpstream-PriorityDefault)
break
}
@@ -727,21 +648,6 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
return muxUpdates, nil
}
func (s *DefaultServer) leaksPriority(domainGroup nsGroupsByDomain, basePriority int, priority int) bool {
if basePriority == PriorityUpstream && priority <= PriorityDefault {
log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers",
domainGroup.domain, PriorityUpstream-PriorityDefault)
return true
}
if basePriority == PriorityDefault && priority <= PriorityFallback {
log.Warnf("too many handlers for domain=%s, would overlap with fallback priority tier (diff=%d). Skipping remaining handlers",
domainGroup.domain, PriorityDefault-PriorityFallback)
return true
}
return false
}
func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
// this will introduce a short period of time when the server is not able to handle DNS requests
for _, existing := range s.dnsMuxMap {
@@ -854,12 +760,6 @@ func (s *DefaultServer) upstreamCallbacks(
}
func (s *DefaultServer) addHostRootZone() {
hostDNSServers := s.hostsDNSHolder.get()
if len(hostDNSServers) == 0 {
log.Debug("no host DNS servers available, skipping root zone handler creation")
return
}
handler, err := newUpstreamResolver(
s.ctx,
s.wgInterface.Name(),
@@ -875,7 +775,7 @@ func (s *DefaultServer) addHostRootZone() {
}
handler.upstreamServers = make([]string, 0)
for k := range hostDNSServers {
for k := range s.hostsDNSHolder.get() {
handler.upstreamServers = append(handler.upstreamServers, k)
}
handler.deactivate = func(error) {}

View File

@@ -938,7 +938,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
return wgIface, nil
}
func newDnsResolver(ip netip.Addr, port int) *net.Resolver {
func newDnsResolver(ip string, port int) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
@@ -1047,7 +1047,7 @@ type mockService struct{}
func (m *mockService) Listen() error { return nil }
func (m *mockService) Stop() {}
func (m *mockService) RuntimeIP() netip.Addr { return netip.MustParseAddr("127.0.0.1") }
func (m *mockService) RuntimeIP() string { return "127.0.0.1" }
func (m *mockService) RuntimePort() int { return 53 }
func (m *mockService) RegisterMux(string, dns.Handler) {}
func (m *mockService) DeregisterMux(string) {}

View File

@@ -1,8 +1,6 @@
package dns
import (
"net/netip"
"github.com/miekg/dns"
)
@@ -16,5 +14,5 @@ type service interface {
RegisterMux(domain string, handler dns.Handler)
DeregisterMux(key string)
RuntimePort() int
RuntimeIP() netip.Addr
RuntimeIP() string
}

View File

@@ -18,11 +18,8 @@ import (
const (
customPort = 5053
)
var (
defaultIP = netip.MustParseAddr("127.0.0.1")
customIP = netip.MustParseAddr("127.0.0.153")
defaultIP = "127.0.0.1"
customIP = "127.0.0.153"
)
type serviceViaListener struct {
@@ -30,7 +27,7 @@ type serviceViaListener struct {
dnsMux *dns.ServeMux
customAddr *netip.AddrPort
server *dns.Server
listenIP netip.Addr
listenIP string
listenPort uint16
listenerIsRunning bool
listenerFlagLock sync.Mutex
@@ -68,7 +65,6 @@ func (s *serviceViaListener) Listen() error {
log.Errorf("failed to eval runtime address: %s", err)
return fmt.Errorf("eval listen address: %w", err)
}
s.listenIP = s.listenIP.Unmap()
s.server.Addr = fmt.Sprintf("%s:%d", s.listenIP, s.listenPort)
log.Debugf("starting dns on %s", s.server.Addr)
go func() {
@@ -128,7 +124,7 @@ func (s *serviceViaListener) RuntimePort() int {
}
}
func (s *serviceViaListener) RuntimeIP() netip.Addr {
func (s *serviceViaListener) RuntimeIP() string {
return s.listenIP
}
@@ -143,9 +139,9 @@ func (s *serviceViaListener) setListenerStatus(running bool) {
// first check the 53 port availability on WG interface or lo, if not success
// pick a random port on WG interface for eBPF, if not success
// check the 5053 port availability on WG interface or lo without eBPF usage,
func (s *serviceViaListener) evalListenAddress() (netip.Addr, uint16, error) {
func (s *serviceViaListener) evalListenAddress() (string, uint16, error) {
if s.customAddr != nil {
return s.customAddr.Addr(), s.customAddr.Port(), nil
return s.customAddr.Addr().String(), s.customAddr.Port(), nil
}
ip, ok := s.testFreePort(defaultPort)
@@ -156,7 +152,7 @@ func (s *serviceViaListener) evalListenAddress() (netip.Addr, uint16, error) {
ebpfSrv, port, ok := s.tryToUseeBPF()
if ok {
s.ebpfService = ebpfSrv
return s.wgInterface.Address().IP, port, nil
return s.wgInterface.Address().IP.String(), port, nil
}
ip, ok = s.testFreePort(customPort)
@@ -164,15 +160,15 @@ func (s *serviceViaListener) evalListenAddress() (netip.Addr, uint16, error) {
return ip, customPort, nil
}
return netip.Addr{}, 0, fmt.Errorf("failed to find a free port for DNS server")
return "", 0, fmt.Errorf("failed to find a free port for DNS server")
}
func (s *serviceViaListener) testFreePort(port int) (netip.Addr, bool) {
var ips []netip.Addr
func (s *serviceViaListener) testFreePort(port int) (string, bool) {
var ips []string
if runtime.GOOS != "darwin" {
ips = []netip.Addr{s.wgInterface.Address().IP, defaultIP, customIP}
ips = []string{s.wgInterface.Address().IP.String(), defaultIP, customIP}
} else {
ips = []netip.Addr{defaultIP, customIP}
ips = []string{defaultIP, customIP}
}
for _, ip := range ips {
@@ -182,10 +178,10 @@ func (s *serviceViaListener) testFreePort(port int) (netip.Addr, bool) {
return ip, true
}
return netip.Addr{}, false
return "", false
}
func (s *serviceViaListener) tryToBind(ip netip.Addr, port int) bool {
func (s *serviceViaListener) tryToBind(ip string, port int) bool {
addrString := fmt.Sprintf("%s:%d", ip, port)
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString))
probeListener, err := net.ListenUDP("udp", udpAddr)
@@ -228,7 +224,7 @@ func (s *serviceViaListener) tryToUseeBPF() (ebpfMgr.Manager, uint16, bool) {
}
func (s *serviceViaListener) generateFreePort() (uint16, error) {
ok := s.tryToBind(s.wgInterface.Address().IP, customPort)
ok := s.tryToBind(s.wgInterface.Address().IP.String(), customPort)
if ok {
return customPort, nil
}

View File

@@ -16,7 +16,7 @@ import (
type ServiceViaMemory struct {
wgInterface WGIface
dnsMux *dns.ServeMux
runtimeIP netip.Addr
runtimeIP string
runtimePort int
udpFilterHookID string
listenerIsRunning bool
@@ -32,7 +32,7 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
wgInterface: wgIface,
dnsMux: dns.NewServeMux(),
runtimeIP: lastIP,
runtimeIP: lastIP.String(),
runtimePort: defaultPort,
}
return s
@@ -84,7 +84,7 @@ func (s *ServiceViaMemory) RuntimePort() int {
return s.runtimePort
}
func (s *ServiceViaMemory) RuntimeIP() netip.Addr {
func (s *ServiceViaMemory) RuntimeIP() string {
return s.runtimeIP
}
@@ -121,5 +121,10 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
return true
}
return filter.AddUDPPacketHook(false, s.runtimeIP, uint16(s.runtimePort), hook), nil
ip, err := netip.ParseAddr(s.runtimeIP)
if err != nil {
return "", fmt.Errorf("parse runtime ip: %w", err)
}
return filter.AddUDPPacketHook(false, ip, uint16(s.runtimePort), hook), nil
}

View File

@@ -89,16 +89,21 @@ func (s *systemdDbusConfigurator) supportCustomPort() bool {
}
func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
parsedIP, err := netip.ParseAddr(config.ServerIP)
if err != nil {
return fmt.Errorf("unable to parse ip address, error: %w", err)
}
ipAs4 := parsedIP.As4()
defaultLinkInput := systemdDbusDNSInput{
Family: unix.AF_INET,
Address: config.ServerIP.AsSlice(),
Address: ipAs4[:],
}
if err := s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}); err != nil {
if err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}); err != nil {
return fmt.Errorf("set interface DNS server %s:%d: %w", config.ServerIP, config.ServerPort, err)
}
// We don't support dnssec. On some machines this is default on so we explicitly set it to off
if err := s.callLinkMethod(systemdDbusSetDNSSECMethodSuffix, dnsSecDisabled); err != nil {
if err = s.callLinkMethod(systemdDbusSetDNSSECMethodSuffix, dnsSecDisabled); err != nil {
log.Warnf("failed to set DNSSEC to 'no': %v", err)
}
@@ -124,7 +129,8 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
}
if config.RouteAll {
if err := s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true); err != nil {
err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true)
if err != nil {
return fmt.Errorf("set link as default dns router: %w", err)
}
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
@@ -133,7 +139,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
})
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.ServerIP, config.ServerPort)
} else {
if err := s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, false); err != nil {
if err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, false); err != nil {
return fmt.Errorf("remove link as default dns router: %w", err)
}
}
@@ -147,8 +153,9 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
}
log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
if err := s.setDomainsForInterface(domainsInput); err != nil {
log.Error("failed to set domains for interface: ", err)
err = s.setDomainsForInterface(domainsInput)
if err != nil {
log.Error(err)
}
if err := s.flushDNSCache(); err != nil {

View File

@@ -35,7 +35,12 @@ func (s *ShutdownState) Cleanup() error {
}
// TODO: move file contents to state manager
func createUncleanShutdownIndicator(sourcePath string, dnsAddress netip.Addr, stateManager *statemanager.Manager) error {
func createUncleanShutdownIndicator(sourcePath string, dnsAddressStr string, stateManager *statemanager.Manager) error {
dnsAddress, err := netip.ParseAddr(dnsAddressStr)
if err != nil {
return fmt.Errorf("parse dns address %s: %w", dnsAddressStr, err)
}
dir := filepath.Dir(fileUncleanShutdownResolvConfLocation)
if err := os.MkdirAll(dir, os.FileMode(0755)); err != nil {
return fmt.Errorf("create dir %s: %w", dir, err)

View File

@@ -7,7 +7,6 @@ import (
"math/rand"
"net"
"net/netip"
"os"
"reflect"
"runtime"
"slices"
@@ -42,7 +41,6 @@ import (
"github.com/netbirdio/netbird/client/internal/peer/guard"
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
"github.com/netbirdio/netbird/client/internal/peerstore"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/relay"
"github.com/netbirdio/netbird/client/internal/rosenpass"
"github.com/netbirdio/netbird/client/internal/routemanager"
@@ -238,9 +236,7 @@ func NewEngine(
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
}
sm := profilemanager.ServiceManager{}
path := sm.GetStatePath()
path := statemanager.GetDefaultStatePath()
if runtime.GOOS == "ios" {
if !fileExists(mobileDep.StateFilePath) {
err := createFile(mobileDep.StateFilePath)
@@ -1554,7 +1550,7 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) {
func (e *Engine) wgInterfaceCreate() (err error) {
switch runtime.GOOS {
case "android":
err = e.wgInterface.CreateOnAndroid(e.routeManager.InitialRouteRange(), e.dnsServer.DnsIP().String(), e.dnsServer.SearchDomains())
err = e.wgInterface.CreateOnAndroid(e.routeManager.InitialRouteRange(), e.dnsServer.DnsIP(), e.dnsServer.SearchDomains())
case "ios":
e.mobileDep.NetworkChangeListener.SetInterfaceIP(e.config.WgAddr)
err = e.wgInterface.Create()
@@ -1972,24 +1968,21 @@ func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers
}
// isChecksEqual checks if two slices of checks are equal.
func isChecksEqual(checks1, checks2 []*mgmProto.Checks) bool {
normalize := func(checks []*mgmProto.Checks) []string {
normalized := make([]string, len(checks))
for i, check := range checks {
sortedFiles := slices.Clone(check.Files)
sort.Strings(sortedFiles)
normalized[i] = strings.Join(sortedFiles, "|")
}
sort.Strings(normalized)
return normalized
func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool {
for _, check := range checks {
sort.Slice(check.Files, func(i, j int) bool {
return check.Files[i] < check.Files[j]
})
}
for _, oCheck := range oChecks {
sort.Slice(oCheck.Files, func(i, j int) bool {
return oCheck.Files[i] < oCheck.Files[j]
})
}
n1 := normalize(checks1)
n2 := normalize(checks2)
return slices.Equal(n1, n2)
return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool {
return slices.Equal(checks.Files, oChecks.Files)
})
}
func getInterfacePrefixes() ([]netip.Prefix, error) {
@@ -2066,16 +2059,3 @@ func compareNetIPLists(list1 []netip.Prefix, list2 []string) bool {
}
return true
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
func createFile(path string) error {
file, err := os.Create(path)
if err != nil {
return err
}
return file.Close()
}

View File

@@ -38,7 +38,6 @@ import (
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/peer/guard"
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/client/system"
@@ -197,7 +196,7 @@ func (m *MockWGIface) LastActivities() map[string]monotime.Time {
}
func TestMain(m *testing.M) {
_ = util.InitLog("debug", util.LogConsole)
_ = util.InitLog("debug", "console")
code := m.Run()
os.Exit(code)
}
@@ -1150,25 +1149,25 @@ func Test_ParseNATExternalIPMappings(t *testing.T) {
}{
{
name: "Parse Valid List Should Be OK",
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
inputBlacklistInterface: defaultInterfaceBlacklist,
inputMapList: []string{"1.1.1.1", "8.8.8.8/" + testingInterface},
expectedOutput: []string{"1.1.1.1", "8.8.8.8/" + testingIP},
},
{
name: "Only Interface name Should Return Nil",
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
inputBlacklistInterface: defaultInterfaceBlacklist,
inputMapList: []string{testingInterface},
expectedOutput: nil,
},
{
name: "Invalid IP Return Nil",
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
inputBlacklistInterface: defaultInterfaceBlacklist,
inputMapList: []string{"1.1.1.1000"},
expectedOutput: nil,
},
{
name: "Invalid Mapping Element Should return Nil",
inputBlacklistInterface: profilemanager.DefaultInterfaceBlacklist,
inputBlacklistInterface: defaultInterfaceBlacklist,
inputMapList: []string{"1.1.1.1/10.10.10.1/eth0"},
expectedOutput: nil,
},
@@ -1271,82 +1270,6 @@ func Test_CheckFilesEqual(t *testing.T) {
},
expectedBool: false,
},
{
name: "Compared Slices with same files but different order should return true",
inputChecks1: []*mgmtProto.Checks{
{
Files: []string{
"testfile1",
"testfile2",
},
},
{
Files: []string{
"testfile4",
"testfile3",
},
},
},
inputChecks2: []*mgmtProto.Checks{
{
Files: []string{
"testfile3",
"testfile4",
},
},
{
Files: []string{
"testfile2",
"testfile1",
},
},
},
expectedBool: true,
},
{
name: "Compared Slices with same files but different order while first is equal should return true",
inputChecks1: []*mgmtProto.Checks{
{
Files: []string{
"testfile0",
"testfile1",
},
},
{
Files: []string{
"testfile0",
"testfile2",
},
},
{
Files: []string{
"testfile0",
"testfile3",
},
},
},
inputChecks2: []*mgmtProto.Checks{
{
Files: []string{
"testfile0",
"testfile1",
},
},
{
Files: []string{
"testfile0",
"testfile3",
},
},
{
Files: []string{
"testfile0",
"testfile2",
},
},
},
expectedBool: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
@@ -1470,7 +1393,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
if runtime.GOOS == "darwin" {
ifaceName = fmt.Sprintf("utun1%d", i)
} else {
ifaceName = fmt.Sprintf("wt%d", i)
ifaceName = fmt.Sprintf("nb%d", i)
}
wgPort := 33100 + i

View File

@@ -33,15 +33,6 @@ func (m MocWGIface) UpdatePeer(string, []netip.Prefix, time.Duration, *net.UDPAd
}
// Add this method to the Manager struct
func (m *Manager) GetPeerListener(peerConnID peerid.ConnID) (*Listener, bool) {
m.mu.Lock()
defer m.mu.Unlock()
listener, exists := m.peers[peerConnID]
return listener, exists
}
func TestManager_MonitorPeerActivity(t *testing.T) {
mocWgInterface := &MocWGIface{}
@@ -60,12 +51,7 @@ func TestManager_MonitorPeerActivity(t *testing.T) {
t.Fatalf("failed to monitor peer activity: %v", err)
}
listener, exists := mgr.GetPeerListener(peerCfg1.PeerConnID)
if !exists {
t.Fatalf("peer listener not found")
}
if err := trigger(listener.conn.LocalAddr().String()); err != nil {
if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil {
t.Fatalf("failed to trigger activity: %v", err)
}
@@ -142,21 +128,11 @@ func TestManager_MultiPeerActivity(t *testing.T) {
t.Fatalf("failed to monitor peer activity: %v", err)
}
listener, exists := mgr.GetPeerListener(peerCfg1.PeerConnID)
if !exists {
t.Fatalf("peer listener for peer1 not found")
}
if err := trigger(listener.conn.LocalAddr().String()); err != nil {
if err := trigger(mgr.peers[peerCfg1.PeerConnID].conn.LocalAddr().String()); err != nil {
t.Fatalf("failed to trigger activity: %v", err)
}
listener, exists = mgr.GetPeerListener(peerCfg2.PeerConnID)
if !exists {
t.Fatalf("peer listener for peer2 not found")
}
if err := trigger(listener.conn.LocalAddr().String()); err != nil {
if err := trigger(mgr.peers[peerCfg2.PeerConnID].conn.LocalAddr().String()); err != nil {
t.Fatalf("failed to trigger activity: %v", err)
}

View File

@@ -10,7 +10,6 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/client/system"
mgm "github.com/netbirdio/netbird/management/client"
@@ -18,7 +17,7 @@ import (
)
// IsLoginRequired check that the server is support SSO or not
func IsLoginRequired(ctx context.Context, config *profilemanager.Config) (bool, error) {
func IsLoginRequired(ctx context.Context, config *Config) (bool, error) {
mgmURL := config.ManagementURL
mgmClient, err := getMgmClient(ctx, config.PrivateKey, mgmURL)
if err != nil {
@@ -48,7 +47,7 @@ func IsLoginRequired(ctx context.Context, config *profilemanager.Config) (bool,
}
// Login or register the client
func Login(ctx context.Context, config *profilemanager.Config, setupKey string, jwtToken string) error {
func Login(ctx context.Context, config *Config, setupKey string, jwtToken string) error {
mgmClient, err := getMgmClient(ctx, config.PrivateKey, config.ManagementURL)
if err != nil {
return err
@@ -101,7 +100,7 @@ func getMgmClient(ctx context.Context, privateKey string, mgmURL *url.URL) (*mgm
return mgmClient, err
}
func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *profilemanager.Config) (*wgtypes.Key, error) {
func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte, config *Config) (*wgtypes.Key, error) {
serverKey, err := mgmClient.GetServerPublicKey()
if err != nil {
log.Errorf("failed while getting Management Service public key: %v", err)
@@ -127,7 +126,7 @@ func doMgmLogin(ctx context.Context, mgmClient *mgm.GrpcClient, pubSSHKey []byte
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
// Otherwise tries to register with the provided setupKey via command line.
func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte, config *profilemanager.Config) (*mgmProto.LoginResponse, error) {
func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) {
validSetupKey, err := uuid.Parse(setupKey)
if err != nil && jwtToken == "" {
return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err)

View File

@@ -19,7 +19,7 @@ type mockIFaceMapper struct {
}
func (m *mockIFaceMapper) Name() string {
return "wt0"
return "nb0"
}
func (m *mockIFaceMapper) Address() wgaddr.Address {

View File

@@ -31,7 +31,7 @@ var connConf = ConnConfig{
}
func TestMain(m *testing.M) {
_ = util.InitLog("trace", util.LogConsole)
_ = util.InitLog("trace", "console")
code := m.Run()
os.Exit(code)
}

View File

@@ -18,15 +18,17 @@ const (
iceKeepAliveDefault = 4 * time.Second
iceDisconnectedTimeoutDefault = 6 * time.Second
iceFailedTimeoutDefault = 6 * time.Second
// iceRelayAcceptanceMinWaitDefault is the same as in the Pion ICE package
iceRelayAcceptanceMinWaitDefault = 2 * time.Second
)
var (
failedTimeout = 6 * time.Second
)
func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candidateTypes []ice.CandidateType, ufrag string, pwd string) (*ice.Agent, error) {
iceKeepAlive := iceKeepAlive()
iceDisconnectedTimeout := iceDisconnectedTimeout()
iceFailedTimeout := iceFailedTimeout()
iceRelayAcceptanceMinWait := iceRelayAcceptanceMinWait()
transportNet, err := newStdNet(iFaceDiscover, config.InterfaceBlackList)
@@ -48,7 +50,7 @@ func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candida
UDPMuxSrflx: config.UDPMuxSrflx,
NAT1To1IPs: config.NATExternalIPs,
Net: transportNet,
FailedTimeout: &iceFailedTimeout,
FailedTimeout: &failedTimeout,
DisconnectedTimeout: &iceDisconnectedTimeout,
KeepaliveInterval: &iceKeepAlive,
RelayAcceptanceMinWait: &iceRelayAcceptanceMinWait,

View File

@@ -13,7 +13,6 @@ const (
envICEForceRelayConn = "NB_ICE_FORCE_RELAY_CONN"
envICEKeepAliveIntervalSec = "NB_ICE_KEEP_ALIVE_INTERVAL_SEC"
envICEDisconnectedTimeoutSec = "NB_ICE_DISCONNECTED_TIMEOUT_SEC"
envICEFailedTimeoutSec = "NB_ICE_FAILED_TIMEOUT_SEC"
envICERelayAcceptanceMinWaitSec = "NB_ICE_RELAY_ACCEPTANCE_MIN_WAIT_SEC"
msgWarnInvalidValue = "invalid value %s set for %s, using default %v"
@@ -56,22 +55,6 @@ func iceDisconnectedTimeout() time.Duration {
return time.Duration(disconnectedTimeoutSec) * time.Second
}
func iceFailedTimeout() time.Duration {
failedTimeoutEnv := os.Getenv(envICEFailedTimeoutSec)
if failedTimeoutEnv == "" {
return iceFailedTimeoutDefault
}
log.Infof("setting ICE failed timeout to %s seconds", failedTimeoutEnv)
failedTimeoutSec, err := strconv.Atoi(failedTimeoutEnv)
if err != nil {
log.Warnf(msgWarnInvalidValue, failedTimeoutEnv, envICEFailedTimeoutSec, iceFailedTimeoutDefault)
return iceFailedTimeoutDefault
}
return time.Duration(failedTimeoutSec) * time.Second
}
func iceRelayAcceptanceMinWait() time.Duration {
iceRelayAcceptanceMinWaitEnv := os.Getenv(envICERelayAcceptanceMinWaitSec)
if iceRelayAcceptanceMinWaitEnv == "" {

View File

@@ -24,7 +24,7 @@ type WorkerRelay struct {
isController bool
config ConnConfig
conn *Conn
relayManager *relayClient.Manager
relayManager relayClient.ManagerService
relayedConn net.Conn
relayLock sync.Mutex
@@ -34,7 +34,7 @@ type WorkerRelay struct {
wgWatcher *WGWatcher
}
func NewWorkerRelay(ctx context.Context, log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager *relayClient.Manager, stateDump *stateDump) *WorkerRelay {
func NewWorkerRelay(ctx context.Context, log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService, stateDump *stateDump) *WorkerRelay {
r := &WorkerRelay{
peerCtx: ctx,
log: log,

View File

@@ -1,9 +0,0 @@
package profilemanager
import "errors"
var (
ErrProfileNotFound = errors.New("profile not found")
ErrProfileAlreadyExists = errors.New("profile already exists")
ErrNoActiveProfile = errors.New("no active profile set")
)

View File

@@ -1,133 +0,0 @@
package profilemanager
import (
"fmt"
"os"
"os/user"
"path/filepath"
"strings"
"sync"
"unicode"
log "github.com/sirupsen/logrus"
)
const (
defaultProfileName = "default"
activeProfileStateFilename = "active_profile.txt"
)
type Profile struct {
Name string
IsActive bool
}
func (p *Profile) FilePath() (string, error) {
if p.Name == "" {
return "", fmt.Errorf("active profile name is empty")
}
if p.Name == defaultProfileName {
return DefaultConfigPath, nil
}
username, err := user.Current()
if err != nil {
return "", fmt.Errorf("failed to get current user: %w", err)
}
configDir, err := getConfigDirForUser(username.Username)
if err != nil {
return "", fmt.Errorf("failed to get config directory for user %s: %w", username.Username, err)
}
return filepath.Join(configDir, p.Name+".json"), nil
}
func (p *Profile) IsDefault() bool {
return p.Name == defaultProfileName
}
type ProfileManager struct {
mu sync.Mutex
}
func NewProfileManager() *ProfileManager {
return &ProfileManager{}
}
func (pm *ProfileManager) GetActiveProfile() (*Profile, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
prof := pm.getActiveProfileState()
return &Profile{Name: prof}, nil
}
func (pm *ProfileManager) SwitchProfile(profileName string) error {
profileName = sanitizeProfileName(profileName)
if err := pm.setActiveProfileState(profileName); err != nil {
return fmt.Errorf("failed to switch profile: %w", err)
}
return nil
}
// sanitizeProfileName sanitizes the username by removing any invalid characters and spaces.
func sanitizeProfileName(name string) string {
return strings.Map(func(r rune) rune {
if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' {
return r
}
// drop everything else
return -1
}, name)
}
func (pm *ProfileManager) getActiveProfileState() string {
configDir, err := getConfigDir()
if err != nil {
log.Warnf("failed to get config directory: %v", err)
return defaultProfileName
}
statePath := filepath.Join(configDir, activeProfileStateFilename)
prof, err := os.ReadFile(statePath)
if err != nil {
if !os.IsNotExist(err) {
log.Warnf("failed to read active profile state: %v", err)
} else {
if err := pm.setActiveProfileState(defaultProfileName); err != nil {
log.Warnf("failed to set default profile state: %v", err)
}
}
return defaultProfileName
}
profileName := strings.TrimSpace(string(prof))
if profileName == "" {
log.Warnf("active profile state is empty, using default profile: %s", defaultProfileName)
return defaultProfileName
}
return profileName
}
func (pm *ProfileManager) setActiveProfileState(profileName string) error {
configDir, err := getConfigDir()
if err != nil {
return fmt.Errorf("failed to get config directory: %w", err)
}
statePath := filepath.Join(configDir, activeProfileStateFilename)
err = os.WriteFile(statePath, []byte(profileName), 0600)
if err != nil {
return fmt.Errorf("failed to write active profile state: %w", err)
}
return nil
}

View File

@@ -1,151 +0,0 @@
package profilemanager
import (
"os"
"os/user"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func withTempConfigDir(t *testing.T, testFunc func(configDir string)) {
t.Helper()
tempDir := t.TempDir()
t.Setenv("NETBIRD_CONFIG_DIR", tempDir)
defer os.Unsetenv("NETBIRD_CONFIG_DIR")
testFunc(tempDir)
}
func withPatchedGlobals(t *testing.T, configDir string, testFunc func()) {
origDefaultConfigPathDir := DefaultConfigPathDir
origDefaultConfigPath := DefaultConfigPath
origActiveProfileStatePath := ActiveProfileStatePath
origOldDefaultConfigPath := oldDefaultConfigPath
origConfigDirOverride := ConfigDirOverride
DefaultConfigPathDir = configDir
DefaultConfigPath = filepath.Join(configDir, "default.json")
ActiveProfileStatePath = filepath.Join(configDir, "active_profile.json")
oldDefaultConfigPath = filepath.Join(configDir, "old_config.json")
ConfigDirOverride = configDir
// Clean up any files in the config dir to ensure isolation
os.RemoveAll(configDir)
os.MkdirAll(configDir, 0755) //nolint: errcheck
defer func() {
DefaultConfigPathDir = origDefaultConfigPathDir
DefaultConfigPath = origDefaultConfigPath
ActiveProfileStatePath = origActiveProfileStatePath
oldDefaultConfigPath = origOldDefaultConfigPath
ConfigDirOverride = origConfigDirOverride
}()
testFunc()
}
func TestServiceManager_CreateAndGetDefaultProfile(t *testing.T) {
withTempConfigDir(t, func(configDir string) {
withPatchedGlobals(t, configDir, func() {
sm := &ServiceManager{}
err := sm.CreateDefaultProfile()
assert.NoError(t, err)
state, err := sm.GetActiveProfileState()
assert.NoError(t, err)
assert.Equal(t, state.Name, defaultProfileName) // No active profile state yet
err = sm.SetActiveProfileStateToDefault()
assert.NoError(t, err)
active, err := sm.GetActiveProfileState()
assert.NoError(t, err)
assert.Equal(t, "default", active.Name)
})
})
}
func TestServiceManager_CopyDefaultProfileIfNotExists(t *testing.T) {
withTempConfigDir(t, func(configDir string) {
withPatchedGlobals(t, configDir, func() {
sm := &ServiceManager{}
// Case: old default config does not exist
ok, err := sm.CopyDefaultProfileIfNotExists()
assert.False(t, ok)
assert.ErrorIs(t, err, ErrorOldDefaultConfigNotFound)
// Case: old default config exists, should be moved
f, err := os.Create(oldDefaultConfigPath)
assert.NoError(t, err)
f.Close()
ok, err = sm.CopyDefaultProfileIfNotExists()
assert.True(t, ok)
assert.NoError(t, err)
_, err = os.Stat(DefaultConfigPath)
assert.NoError(t, err)
})
})
}
func TestServiceManager_SetActiveProfileState(t *testing.T) {
withTempConfigDir(t, func(configDir string) {
withPatchedGlobals(t, configDir, func() {
currUser, err := user.Current()
assert.NoError(t, err)
sm := &ServiceManager{}
state := &ActiveProfileState{Name: "foo", Username: currUser.Username}
err = sm.SetActiveProfileState(state)
assert.NoError(t, err)
// Should error on nil or incomplete state
err = sm.SetActiveProfileState(nil)
assert.Error(t, err)
err = sm.SetActiveProfileState(&ActiveProfileState{Name: "", Username: ""})
assert.Error(t, err)
})
})
}
func TestServiceManager_DefaultProfilePath(t *testing.T) {
withTempConfigDir(t, func(configDir string) {
withPatchedGlobals(t, configDir, func() {
sm := &ServiceManager{}
assert.Equal(t, DefaultConfigPath, sm.DefaultProfilePath())
})
})
}
func TestSanitizeProfileName(t *testing.T) {
tests := []struct {
in, want string
}{
// unchanged
{"Alice", "Alice"},
{"bob123", "bob123"},
{"under_score", "under_score"},
{"dash-name", "dash-name"},
// spaces and forbidden chars removed
{"Alice Smith", "AliceSmith"},
{"bad/char\\name", "badcharname"},
{"colon:name*?", "colonname"},
{"quotes\"<>|", "quotes"},
// mixed
{"User_123-Test!@#", "User_123-Test"},
// empty and all-bad
{"", ""},
{"!@#$%^&*()", ""},
// unicode letters and digits
{"ÜserÇ", "ÜserÇ"},
{"漢字テスト123", "漢字テスト123"},
}
for _, tc := range tests {
got := sanitizeProfileName(tc.in)
if got != tc.want {
t.Errorf("sanitizeProfileName(%q) = %q; want %q", tc.in, got, tc.want)
}
}
}

View File

@@ -1,363 +0,0 @@
package profilemanager
import (
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/util"
)
var (
oldDefaultConfigPathDir = ""
oldDefaultConfigPath = ""
DefaultConfigPathDir = ""
DefaultConfigPath = ""
ActiveProfileStatePath = ""
)
var (
ErrorOldDefaultConfigNotFound = errors.New("old default config not found")
)
func init() {
DefaultConfigPathDir = "/var/lib/netbird/"
oldDefaultConfigPathDir = "/etc/netbird/"
if stateDir := os.Getenv("NB_STATE_DIR"); stateDir != "" {
DefaultConfigPathDir = stateDir
} else {
switch runtime.GOOS {
case "windows":
oldDefaultConfigPathDir = filepath.Join(os.Getenv("PROGRAMDATA"), "Netbird")
DefaultConfigPathDir = oldDefaultConfigPathDir
case "freebsd":
oldDefaultConfigPathDir = "/var/db/netbird/"
DefaultConfigPathDir = oldDefaultConfigPathDir
}
}
oldDefaultConfigPath = filepath.Join(oldDefaultConfigPathDir, "config.json")
DefaultConfigPath = filepath.Join(DefaultConfigPathDir, "default.json")
ActiveProfileStatePath = filepath.Join(DefaultConfigPathDir, "active_profile.json")
}
type ActiveProfileState struct {
Name string `json:"name"`
Username string `json:"username"`
}
func (a *ActiveProfileState) FilePath() (string, error) {
if a.Name == "" {
return "", fmt.Errorf("active profile name is empty")
}
if a.Name == defaultProfileName {
return DefaultConfigPath, nil
}
configDir, err := getConfigDirForUser(a.Username)
if err != nil {
return "", fmt.Errorf("failed to get config directory for user %s: %w", a.Username, err)
}
return filepath.Join(configDir, a.Name+".json"), nil
}
type ServiceManager struct{}
func (s *ServiceManager) CopyDefaultProfileIfNotExists() (bool, error) {
if err := os.MkdirAll(DefaultConfigPathDir, 0600); err != nil {
return false, fmt.Errorf("failed to create default config path directory: %w", err)
}
// check if default profile exists
if _, err := os.Stat(DefaultConfigPath); !os.IsNotExist(err) {
// default profile already exists
log.Debugf("default profile already exists at %s, skipping copy", DefaultConfigPath)
return false, nil
}
// check old default profile
if _, err := os.Stat(oldDefaultConfigPath); os.IsNotExist(err) {
// old default profile does not exist, nothing to copy
return false, ErrorOldDefaultConfigNotFound
}
// copy old default profile to new location
if err := copyFile(oldDefaultConfigPath, DefaultConfigPath, 0600); err != nil {
return false, fmt.Errorf("copy default profile from %s to %s: %w", oldDefaultConfigPath, DefaultConfigPath, err)
}
// set permissions for the new default profile
if err := os.Chmod(DefaultConfigPath, 0600); err != nil {
log.Warnf("failed to set permissions for default profile: %v", err)
}
if err := s.SetActiveProfileState(&ActiveProfileState{
Name: "default",
Username: "",
}); err != nil {
log.Errorf("failed to set active profile state: %v", err)
return false, fmt.Errorf("failed to set active profile state: %w", err)
}
return true, nil
}
// copyFile copies the contents of src to dst and sets dst's file mode to perm.
func copyFile(src, dst string, perm os.FileMode) error {
in, err := os.Open(src)
if err != nil {
return fmt.Errorf("open source file %s: %w", src, err)
}
defer in.Close()
out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
if err != nil {
return fmt.Errorf("open target file %s: %w", dst, err)
}
defer func() {
if cerr := out.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if _, err := io.Copy(out, in); err != nil {
return fmt.Errorf("copy data to %s: %w", dst, err)
}
return nil
}
func (s *ServiceManager) CreateDefaultProfile() error {
_, err := UpdateOrCreateConfig(ConfigInput{
ConfigPath: DefaultConfigPath,
})
if err != nil {
return fmt.Errorf("failed to create default profile: %w", err)
}
log.Infof("default profile created at %s", DefaultConfigPath)
return nil
}
func (s *ServiceManager) GetActiveProfileState() (*ActiveProfileState, error) {
if err := s.setDefaultActiveState(); err != nil {
return nil, fmt.Errorf("failed to set default active profile state: %w", err)
}
var activeProfile ActiveProfileState
if _, err := util.ReadJson(ActiveProfileStatePath, &activeProfile); err != nil {
if errors.Is(err, os.ErrNotExist) {
if err := s.SetActiveProfileStateToDefault(); err != nil {
return nil, fmt.Errorf("failed to set active profile to default: %w", err)
}
return &ActiveProfileState{
Name: "default",
Username: "",
}, nil
} else {
return nil, fmt.Errorf("failed to read active profile state: %w", err)
}
}
if activeProfile.Name == "" {
if err := s.SetActiveProfileStateToDefault(); err != nil {
return nil, fmt.Errorf("failed to set active profile to default: %w", err)
}
return &ActiveProfileState{
Name: "default",
Username: "",
}, nil
}
return &activeProfile, nil
}
func (s *ServiceManager) setDefaultActiveState() error {
_, err := os.Stat(ActiveProfileStatePath)
if err != nil {
if os.IsNotExist(err) {
if err := s.SetActiveProfileStateToDefault(); err != nil {
return fmt.Errorf("failed to set active profile to default: %w", err)
}
} else {
return fmt.Errorf("failed to stat active profile state path %s: %w", ActiveProfileStatePath, err)
}
}
return nil
}
func (s *ServiceManager) SetActiveProfileState(a *ActiveProfileState) error {
if a == nil || a.Name == "" {
return errors.New("invalid active profile state")
}
if a.Name != defaultProfileName && a.Username == "" {
return fmt.Errorf("username must be set for non-default profiles, got: %s", a.Name)
}
if err := util.WriteJsonWithRestrictedPermission(context.Background(), ActiveProfileStatePath, a); err != nil {
return fmt.Errorf("failed to write active profile state: %w", err)
}
log.Infof("active profile set to %s for %s", a.Name, a.Username)
return nil
}
func (s *ServiceManager) SetActiveProfileStateToDefault() error {
return s.SetActiveProfileState(&ActiveProfileState{
Name: "default",
Username: "",
})
}
func (s *ServiceManager) DefaultProfilePath() string {
return DefaultConfigPath
}
func (s *ServiceManager) AddProfile(profileName, username string) error {
configDir, err := getConfigDirForUser(username)
if err != nil {
return fmt.Errorf("failed to get config directory: %w", err)
}
profileName = sanitizeProfileName(profileName)
if profileName == defaultProfileName {
return fmt.Errorf("cannot create profile with reserved name: %s", defaultProfileName)
}
profPath := filepath.Join(configDir, profileName+".json")
if fileExists(profPath) {
return ErrProfileAlreadyExists
}
cfg, err := createNewConfig(ConfigInput{ConfigPath: profPath})
if err != nil {
return fmt.Errorf("failed to create new config: %w", err)
}
err = util.WriteJson(context.Background(), profPath, cfg)
if err != nil {
return fmt.Errorf("failed to write profile config: %w", err)
}
return nil
}
func (s *ServiceManager) RemoveProfile(profileName, username string) error {
configDir, err := getConfigDirForUser(username)
if err != nil {
return fmt.Errorf("failed to get config directory: %w", err)
}
profileName = sanitizeProfileName(profileName)
if profileName == defaultProfileName {
return fmt.Errorf("cannot remove profile with reserved name: %s", defaultProfileName)
}
profPath := filepath.Join(configDir, profileName+".json")
if !fileExists(profPath) {
return ErrProfileNotFound
}
activeProf, err := s.GetActiveProfileState()
if err != nil && !errors.Is(err, ErrNoActiveProfile) {
return fmt.Errorf("failed to get active profile: %w", err)
}
if activeProf != nil && activeProf.Name == profileName {
return fmt.Errorf("cannot remove active profile: %s", profileName)
}
err = util.RemoveJson(profPath)
if err != nil {
return fmt.Errorf("failed to remove profile config: %w", err)
}
return nil
}
func (s *ServiceManager) ListProfiles(username string) ([]Profile, error) {
configDir, err := getConfigDirForUser(username)
if err != nil {
return nil, fmt.Errorf("failed to get config directory: %w", err)
}
files, err := util.ListFiles(configDir, "*.json")
if err != nil {
return nil, fmt.Errorf("failed to list profile files: %w", err)
}
var filtered []string
for _, file := range files {
if strings.HasSuffix(file, "state.json") {
continue // skip state files
}
filtered = append(filtered, file)
}
sort.Strings(filtered)
var activeProfName string
activeProf, err := s.GetActiveProfileState()
if err == nil {
activeProfName = activeProf.Name
}
var profiles []Profile
// add default profile always
profiles = append(profiles, Profile{Name: defaultProfileName, IsActive: activeProfName == "" || activeProfName == defaultProfileName})
for _, file := range filtered {
profileName := strings.TrimSuffix(filepath.Base(file), ".json")
var isActive bool
if activeProfName != "" && activeProfName == profileName {
isActive = true
}
profiles = append(profiles, Profile{Name: profileName, IsActive: isActive})
}
return profiles, nil
}
// GetStatePath returns the path to the state file based on the operating system
// It returns an empty string if the path cannot be determined.
func (s *ServiceManager) GetStatePath() string {
if path := os.Getenv("NB_DNS_STATE_FILE"); path != "" {
return path
}
defaultStatePath := filepath.Join(DefaultConfigPathDir, "state.json")
activeProf, err := s.GetActiveProfileState()
if err != nil {
log.Warnf("failed to get active profile state: %v", err)
return defaultStatePath
}
if activeProf.Name == defaultProfileName {
return defaultStatePath
}
configDir, err := getConfigDirForUser(activeProf.Username)
if err != nil {
log.Warnf("failed to get config directory for user %s: %v", activeProf.Username, err)
return defaultStatePath
}
return filepath.Join(configDir, activeProf.Name+".state.json")
}

View File

@@ -1,57 +0,0 @@
package profilemanager
import (
"context"
"errors"
"fmt"
"path/filepath"
"github.com/netbirdio/netbird/util"
)
type ProfileState struct {
Email string `json:"email"`
}
func (pm *ProfileManager) GetProfileState(profileName string) (*ProfileState, error) {
configDir, err := getConfigDir()
if err != nil {
return nil, fmt.Errorf("get config directory: %w", err)
}
stateFile := filepath.Join(configDir, profileName+".state.json")
if !fileExists(stateFile) {
return nil, errors.New("profile state file does not exist")
}
var state ProfileState
_, err = util.ReadJson(stateFile, &state)
if err != nil {
return nil, fmt.Errorf("read profile state: %w", err)
}
return &state, nil
}
func (pm *ProfileManager) SetActiveProfileState(state *ProfileState) error {
configDir, err := getConfigDir()
if err != nil {
return fmt.Errorf("get config directory: %w", err)
}
activeProf, err := pm.GetActiveProfile()
if err != nil {
if errors.Is(err, ErrNoActiveProfile) {
return fmt.Errorf("no active profile set: %w", err)
}
return fmt.Errorf("get active profile: %w", err)
}
stateFile := filepath.Join(configDir, activeProf.Name+".state.json")
err = util.WriteJsonWithRestrictedPermission(context.Background(), stateFile, state)
if err != nil {
return fmt.Errorf("write profile state: %w", err)
}
return nil
}

View File

@@ -2,12 +2,9 @@
package systemops
import (
"strings"
"syscall"
)
import "syscall"
// filterRoutesByFlags returns true if the route message should be ignored based on its flags.
// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags.
func filterRoutesByFlags(routeMessageFlags int) bool {
if routeMessageFlags&syscall.RTF_UP == 0 {
return true
@@ -19,50 +16,3 @@ func filterRoutesByFlags(routeMessageFlags int) bool {
return false
}
// formatBSDFlags formats route flags for BSD systems (excludes FreeBSD-specific handling)
func formatBSDFlags(flags int) string {
var flagStrs []string
if flags&syscall.RTF_UP != 0 {
flagStrs = append(flagStrs, "U")
}
if flags&syscall.RTF_GATEWAY != 0 {
flagStrs = append(flagStrs, "G")
}
if flags&syscall.RTF_HOST != 0 {
flagStrs = append(flagStrs, "H")
}
if flags&syscall.RTF_REJECT != 0 {
flagStrs = append(flagStrs, "R")
}
if flags&syscall.RTF_DYNAMIC != 0 {
flagStrs = append(flagStrs, "D")
}
if flags&syscall.RTF_MODIFIED != 0 {
flagStrs = append(flagStrs, "M")
}
if flags&syscall.RTF_STATIC != 0 {
flagStrs = append(flagStrs, "S")
}
if flags&syscall.RTF_LLINFO != 0 {
flagStrs = append(flagStrs, "L")
}
if flags&syscall.RTF_LOCAL != 0 {
flagStrs = append(flagStrs, "l")
}
if flags&syscall.RTF_BLACKHOLE != 0 {
flagStrs = append(flagStrs, "B")
}
if flags&syscall.RTF_CLONING != 0 {
flagStrs = append(flagStrs, "C")
}
if flags&syscall.RTF_WASCLONED != 0 {
flagStrs = append(flagStrs, "W")
}
if len(flagStrs) == 0 {
return "-"
}
return strings.Join(flagStrs, "")
}

View File

@@ -1,64 +1,19 @@
//go:build freebsd
//go:build: freebsd
package systemops
import (
"strings"
"syscall"
)
import "syscall"
// filterRoutesByFlags returns true if the route message should be ignored based on its flags.
// filterRoutesByFlags - return true if need to ignore such route message because it consists specific flags.
func filterRoutesByFlags(routeMessageFlags int) bool {
if routeMessageFlags&syscall.RTF_UP == 0 {
return true
}
// NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0
// NOTE: syscall.RTF_WASCLONED deprecated in FreeBSD 8.0 (https://www.freebsd.org/releases/8.0R/relnotes-detailed/)
// a concept of cloned route (a route generated by an entry with RTF_CLONING flag) is deprecated.
if routeMessageFlags&(syscall.RTF_REJECT|syscall.RTF_BLACKHOLE) != 0 {
return true
}
return false
}
// formatBSDFlags formats route flags for FreeBSD (excludes deprecated RTF_CLONING and RTF_WASCLONED)
func formatBSDFlags(flags int) string {
var flagStrs []string
if flags&syscall.RTF_UP != 0 {
flagStrs = append(flagStrs, "U")
}
if flags&syscall.RTF_GATEWAY != 0 {
flagStrs = append(flagStrs, "G")
}
if flags&syscall.RTF_HOST != 0 {
flagStrs = append(flagStrs, "H")
}
if flags&syscall.RTF_REJECT != 0 {
flagStrs = append(flagStrs, "R")
}
if flags&syscall.RTF_DYNAMIC != 0 {
flagStrs = append(flagStrs, "D")
}
if flags&syscall.RTF_MODIFIED != 0 {
flagStrs = append(flagStrs, "M")
}
if flags&syscall.RTF_STATIC != 0 {
flagStrs = append(flagStrs, "S")
}
if flags&syscall.RTF_LLINFO != 0 {
flagStrs = append(flagStrs, "L")
}
if flags&syscall.RTF_LOCAL != 0 {
flagStrs = append(flagStrs, "l")
}
if flags&syscall.RTF_BLACKHOLE != 0 {
flagStrs = append(flagStrs, "B")
}
// Note: RTF_CLONING and RTF_WASCLONED deprecated in FreeBSD 8.0
if len(flagStrs) == 0 {
return "-"
}
return strings.Join(flagStrs, "")
}

View File

@@ -19,26 +19,6 @@ type Nexthop struct {
Intf *net.Interface
}
// Route represents a basic network route with core routing information
type Route struct {
Dst netip.Prefix
Gw netip.Addr
Interface *net.Interface
}
// DetailedRoute extends Route with additional metadata for display and debugging
type DetailedRoute struct {
Route
Metric int
InterfaceMetric int
InterfaceIndex int
Protocol string
Scope string
Type string
Table string
Flags string
}
// Equal checks if two nexthops are equal.
func (n Nexthop) Equal(other Nexthop) bool {
return n.IP == other.IP && (n.Intf == nil && other.Intf == nil ||

View File

@@ -16,6 +16,12 @@ import (
"golang.org/x/net/route"
)
type Route struct {
Dst netip.Prefix
Gw netip.Addr
Interface *net.Interface
}
func GetRoutesFromTable() ([]netip.Prefix, error) {
tab, err := retryFetchRIB()
if err != nil {
@@ -41,134 +47,25 @@ func GetRoutesFromTable() ([]netip.Prefix, error) {
continue
}
r, err := MsgToRoute(m)
route, err := MsgToRoute(m)
if err != nil {
log.Warnf("Failed to parse route message: %v", err)
continue
}
if r.Dst.IsValid() {
prefixList = append(prefixList, r.Dst)
if route.Dst.IsValid() {
prefixList = append(prefixList, route.Dst)
}
}
return prefixList, nil
}
func GetDetailedRoutesFromTable() ([]DetailedRoute, error) {
tab, err := retryFetchRIB()
if err != nil {
return nil, fmt.Errorf("fetch RIB: %v", err)
}
msgs, err := route.ParseRIB(route.RIBTypeRoute, tab)
if err != nil {
return nil, fmt.Errorf("parse RIB: %v", err)
}
return processRouteMessages(msgs)
}
func processRouteMessages(msgs []route.Message) ([]DetailedRoute, error) {
var detailedRoutes []DetailedRoute
for _, msg := range msgs {
m := msg.(*route.RouteMessage)
if !isValidRouteMessage(m) {
continue
}
if filterRoutesByFlags(m.Flags) {
continue
}
detailed, err := buildDetailedRouteFromMessage(m)
if err != nil {
log.Warnf("Failed to parse route message: %v", err)
continue
}
if detailed != nil {
detailedRoutes = append(detailedRoutes, *detailed)
}
}
return detailedRoutes, nil
}
func isValidRouteMessage(m *route.RouteMessage) bool {
if m.Version < 3 || m.Version > 5 {
log.Warnf("Unexpected RIB message version: %d", m.Version)
return false
}
if m.Type != syscall.RTM_GET {
log.Warnf("Unexpected RIB message type: %d", m.Type)
return false
}
return true
}
func buildDetailedRouteFromMessage(m *route.RouteMessage) (*DetailedRoute, error) {
routeMsg, err := MsgToRoute(m)
if err != nil {
return nil, err
}
if !routeMsg.Dst.IsValid() {
return nil, errors.New("invalid destination")
}
detailed := DetailedRoute{
Route: Route{
Dst: routeMsg.Dst,
Gw: routeMsg.Gw,
Interface: routeMsg.Interface,
},
Metric: extractBSDMetric(m),
Protocol: extractBSDProtocol(m.Flags),
Scope: "global",
Type: "unicast",
Table: "main",
Flags: formatBSDFlags(m.Flags),
}
return &detailed, nil
}
func buildLinkInterface(t *route.LinkAddr) *net.Interface {
interfaceName := fmt.Sprintf("link#%d", t.Index)
if t.Name != "" {
interfaceName = t.Name
}
return &net.Interface{
Index: t.Index,
Name: interfaceName,
}
}
func extractBSDMetric(m *route.RouteMessage) int {
return -1
}
func extractBSDProtocol(flags int) string {
if flags&syscall.RTF_STATIC != 0 {
return "static"
}
if flags&syscall.RTF_DYNAMIC != 0 {
return "dynamic"
}
if flags&syscall.RTF_LOCAL != 0 {
return "local"
}
return "kernel"
}
func retryFetchRIB() ([]byte, error) {
var out []byte
operation := func() error {
var err error
out, err = route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0)
if errors.Is(err, syscall.ENOMEM) {
log.Debug("Retrying fetchRIB due to 'cannot allocate memory' error")
log.Debug("~etrying fetchRIB due to 'cannot allocate memory' error")
return err
} else if err != nil {
return backoff.Permanent(err)
@@ -203,6 +100,7 @@ func toNetIP(a route.Addr) netip.Addr {
}
}
// ones returns the number of leading ones in the mask.
func ones(a route.Addr) (int, error) {
switch t := a.(type) {
case *route.Inet4Addr:
@@ -216,6 +114,7 @@ func ones(a route.Addr) (int, error) {
}
}
// MsgToRoute converts a route message to a Route.
func MsgToRoute(msg *route.RouteMessage) (*Route, error) {
dstIP, nexthop, dstMask := msg.Addrs[0], msg.Addrs[1], msg.Addrs[2]
@@ -228,7 +127,10 @@ func MsgToRoute(msg *route.RouteMessage) (*Route, error) {
case *route.Inet4Addr, *route.Inet6Addr:
nexthopAddr = toNetIP(t)
case *route.LinkAddr:
nexthopIntf = buildLinkInterface(t)
nexthopIntf = &net.Interface{
Index: t.Index,
Name: t.Name,
}
default:
return nil, fmt.Errorf("unexpected next hop type: %T", t)
}
@@ -254,4 +156,5 @@ func MsgToRoute(msg *route.RouteMessage) (*Route, error) {
Gw: nexthopAddr,
Interface: nexthopIntf,
}, nil
}

View File

@@ -14,7 +14,6 @@ import (
"github.com/hashicorp/go-multierror"
log "github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
@@ -23,25 +22,6 @@ import (
nbnet "github.com/netbirdio/netbird/util/net"
)
// IPRule contains IP rule information for debugging
type IPRule struct {
Priority int
From netip.Prefix
To netip.Prefix
IIF string
OIF string
Table string
Action string
Mark uint32
Mask uint32
TunID uint32
Goto uint32
Flow uint32
SuppressPlen int
SuppressIFL int
Invert bool
}
const (
// NetbirdVPNTableID is the ID of the custom routing table used by Netbird.
NetbirdVPNTableID = 0x1BD0
@@ -57,8 +37,6 @@ const (
var ErrTableIDExists = errors.New("ID exists with different name")
const errParsePrefixMsg = "failed to parse prefix %s: %w"
// originalSysctl stores the original sysctl values before they are modified
var originalSysctl map[string]int
@@ -231,277 +209,6 @@ func GetRoutesFromTable() ([]netip.Prefix, error) {
return append(v4Routes, v6Routes...), nil
}
// GetDetailedRoutesFromTable returns detailed route information from all routing tables
func GetDetailedRoutesFromTable() ([]DetailedRoute, error) {
tables := discoverRoutingTables()
return collectRoutesFromTables(tables), nil
}
func discoverRoutingTables() []int {
tables, err := getAllRoutingTables()
if err != nil {
log.Warnf("Failed to get all routing tables, using fallback list: %v", err)
return []int{
syscall.RT_TABLE_MAIN,
syscall.RT_TABLE_LOCAL,
NetbirdVPNTableID,
}
}
return tables
}
func collectRoutesFromTables(tables []int) []DetailedRoute {
var allRoutes []DetailedRoute
for _, tableID := range tables {
routes := collectRoutesFromTable(tableID)
allRoutes = append(allRoutes, routes...)
}
return allRoutes
}
func collectRoutesFromTable(tableID int) []DetailedRoute {
var routes []DetailedRoute
if v4Routes := getRoutesForFamily(tableID, netlink.FAMILY_V4); len(v4Routes) > 0 {
routes = append(routes, v4Routes...)
}
if v6Routes := getRoutesForFamily(tableID, netlink.FAMILY_V6); len(v6Routes) > 0 {
routes = append(routes, v6Routes...)
}
return routes
}
func getRoutesForFamily(tableID, family int) []DetailedRoute {
routes, err := getDetailedRoutes(tableID, family)
if err != nil {
log.Debugf("Failed to get routes from table %d family %d: %v", tableID, family, err)
return nil
}
return routes
}
func getAllRoutingTables() ([]int, error) {
tablesMap := make(map[int]bool)
families := []int{netlink.FAMILY_V4, netlink.FAMILY_V6}
// Use table 0 (RT_TABLE_UNSPEC) to discover all tables
for _, family := range families {
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: 0}, netlink.RT_FILTER_TABLE)
if err != nil {
log.Debugf("Failed to list routes from table 0 for family %d: %v", family, err)
continue
}
// Extract unique table IDs from all routes
for _, route := range routes {
if route.Table > 0 {
tablesMap[route.Table] = true
}
}
}
var tables []int
for tableID := range tablesMap {
tables = append(tables, tableID)
}
standardTables := []int{syscall.RT_TABLE_MAIN, syscall.RT_TABLE_LOCAL, NetbirdVPNTableID}
for _, table := range standardTables {
if !tablesMap[table] {
tables = append(tables, table)
}
}
return tables, nil
}
// getDetailedRoutes fetches detailed routes from a specific routing table
func getDetailedRoutes(tableID, family int) ([]DetailedRoute, error) {
var detailedRoutes []DetailedRoute
routes, err := netlink.RouteListFiltered(family, &netlink.Route{Table: tableID}, netlink.RT_FILTER_TABLE)
if err != nil {
return nil, fmt.Errorf("list routes from table %d: %v", tableID, err)
}
for _, route := range routes {
detailed := buildDetailedRoute(route, tableID, family)
if detailed != nil {
detailedRoutes = append(detailedRoutes, *detailed)
}
}
return detailedRoutes, nil
}
func buildDetailedRoute(route netlink.Route, tableID, family int) *DetailedRoute {
detailed := DetailedRoute{
Route: Route{},
Metric: route.Priority,
InterfaceMetric: -1, // Interface metrics not typically used on Linux
InterfaceIndex: route.LinkIndex,
Protocol: routeProtocolToString(int(route.Protocol)),
Scope: routeScopeToString(route.Scope),
Type: routeTypeToString(route.Type),
Table: routeTableToString(tableID),
Flags: "-",
}
if !processRouteDestination(&detailed, route, family) {
return nil
}
processRouteGateway(&detailed, route)
processRouteInterface(&detailed, route)
return &detailed
}
func processRouteDestination(detailed *DetailedRoute, route netlink.Route, family int) bool {
if route.Dst != nil {
addr, ok := netip.AddrFromSlice(route.Dst.IP)
if !ok {
return false
}
ones, _ := route.Dst.Mask.Size()
prefix := netip.PrefixFrom(addr.Unmap(), ones)
if prefix.IsValid() {
detailed.Route.Dst = prefix
} else {
return false
}
} else {
if family == netlink.FAMILY_V4 {
detailed.Route.Dst = netip.MustParsePrefix("0.0.0.0/0")
} else {
detailed.Route.Dst = netip.MustParsePrefix("::/0")
}
}
return true
}
func processRouteGateway(detailed *DetailedRoute, route netlink.Route) {
if route.Gw != nil {
if gateway, ok := netip.AddrFromSlice(route.Gw); ok {
detailed.Route.Gw = gateway.Unmap()
}
}
}
func processRouteInterface(detailed *DetailedRoute, route netlink.Route) {
if route.LinkIndex > 0 {
if link, err := netlink.LinkByIndex(route.LinkIndex); err == nil {
detailed.Route.Interface = &net.Interface{
Index: link.Attrs().Index,
Name: link.Attrs().Name,
}
} else {
detailed.Route.Interface = &net.Interface{
Index: route.LinkIndex,
Name: fmt.Sprintf("index-%d", route.LinkIndex),
}
}
}
}
// Helper functions to convert netlink constants to strings
func routeProtocolToString(protocol int) string {
switch protocol {
case syscall.RTPROT_UNSPEC:
return "unspec"
case syscall.RTPROT_REDIRECT:
return "redirect"
case syscall.RTPROT_KERNEL:
return "kernel"
case syscall.RTPROT_BOOT:
return "boot"
case syscall.RTPROT_STATIC:
return "static"
case syscall.RTPROT_DHCP:
return "dhcp"
case unix.RTPROT_RA:
return "ra"
case unix.RTPROT_ZEBRA:
return "zebra"
case unix.RTPROT_BIRD:
return "bird"
case unix.RTPROT_DNROUTED:
return "dnrouted"
case unix.RTPROT_XORP:
return "xorp"
case unix.RTPROT_NTK:
return "ntk"
default:
return fmt.Sprintf("%d", protocol)
}
}
func routeScopeToString(scope netlink.Scope) string {
switch scope {
case netlink.SCOPE_UNIVERSE:
return "global"
case netlink.SCOPE_SITE:
return "site"
case netlink.SCOPE_LINK:
return "link"
case netlink.SCOPE_HOST:
return "host"
case netlink.SCOPE_NOWHERE:
return "nowhere"
default:
return fmt.Sprintf("%d", scope)
}
}
func routeTypeToString(routeType int) string {
switch routeType {
case syscall.RTN_UNSPEC:
return "unspec"
case syscall.RTN_UNICAST:
return "unicast"
case syscall.RTN_LOCAL:
return "local"
case syscall.RTN_BROADCAST:
return "broadcast"
case syscall.RTN_ANYCAST:
return "anycast"
case syscall.RTN_MULTICAST:
return "multicast"
case syscall.RTN_BLACKHOLE:
return "blackhole"
case syscall.RTN_UNREACHABLE:
return "unreachable"
case syscall.RTN_PROHIBIT:
return "prohibit"
case syscall.RTN_THROW:
return "throw"
case syscall.RTN_NAT:
return "nat"
case syscall.RTN_XRESOLVE:
return "xresolve"
default:
return fmt.Sprintf("%d", routeType)
}
}
func routeTableToString(tableID int) string {
switch tableID {
case syscall.RT_TABLE_MAIN:
return "main"
case syscall.RT_TABLE_LOCAL:
return "local"
case NetbirdVPNTableID:
return "netbird"
default:
return fmt.Sprintf("%d", tableID)
}
}
// getRoutes fetches routes from a specific routing table identified by tableID.
func getRoutes(tableID, family int) ([]netip.Prefix, error) {
var prefixList []netip.Prefix
@@ -530,115 +237,6 @@ func getRoutes(tableID, family int) ([]netip.Prefix, error) {
return prefixList, nil
}
// GetIPRules returns IP rules for debugging
func GetIPRules() ([]IPRule, error) {
v4Rules, err := getIPRules(netlink.FAMILY_V4)
if err != nil {
return nil, fmt.Errorf("get v4 rules: %w", err)
}
v6Rules, err := getIPRules(netlink.FAMILY_V6)
if err != nil {
return nil, fmt.Errorf("get v6 rules: %w", err)
}
return append(v4Rules, v6Rules...), nil
}
// getIPRules fetches IP rules for the specified address family
func getIPRules(family int) ([]IPRule, error) {
rules, err := netlink.RuleList(family)
if err != nil {
return nil, fmt.Errorf("list rules for family %d: %w", family, err)
}
var ipRules []IPRule
for _, rule := range rules {
ipRule := buildIPRule(rule)
ipRules = append(ipRules, ipRule)
}
return ipRules, nil
}
func buildIPRule(rule netlink.Rule) IPRule {
var mask uint32
if rule.Mask != nil {
mask = *rule.Mask
}
ipRule := IPRule{
Priority: rule.Priority,
IIF: rule.IifName,
OIF: rule.OifName,
Table: ruleTableToString(rule.Table),
Action: ruleActionToString(int(rule.Type)),
Mark: rule.Mark,
Mask: mask,
TunID: uint32(rule.TunID),
Goto: uint32(rule.Goto),
Flow: uint32(rule.Flow),
SuppressPlen: rule.SuppressPrefixlen,
SuppressIFL: rule.SuppressIfgroup,
Invert: rule.Invert,
}
if rule.Src != nil {
ipRule.From = parseRulePrefix(rule.Src)
}
if rule.Dst != nil {
ipRule.To = parseRulePrefix(rule.Dst)
}
return ipRule
}
func parseRulePrefix(ipNet *net.IPNet) netip.Prefix {
if addr, ok := netip.AddrFromSlice(ipNet.IP); ok {
ones, _ := ipNet.Mask.Size()
prefix := netip.PrefixFrom(addr.Unmap(), ones)
if prefix.IsValid() {
return prefix
}
}
return netip.Prefix{}
}
func ruleTableToString(table int) string {
switch table {
case syscall.RT_TABLE_MAIN:
return "main"
case syscall.RT_TABLE_LOCAL:
return "local"
case syscall.RT_TABLE_DEFAULT:
return "default"
case NetbirdVPNTableID:
return "netbird"
default:
return fmt.Sprintf("%d", table)
}
}
func ruleActionToString(action int) string {
switch action {
case unix.FR_ACT_UNSPEC:
return "unspec"
case unix.FR_ACT_TO_TBL:
return "lookup"
case unix.FR_ACT_GOTO:
return "goto"
case unix.FR_ACT_NOP:
return "nop"
case unix.FR_ACT_BLACKHOLE:
return "blackhole"
case unix.FR_ACT_UNREACHABLE:
return "unreachable"
case unix.FR_ACT_PROHIBIT:
return "prohibit"
default:
return fmt.Sprintf("%d", action)
}
}
// addRoute adds a route to a specific routing table identified by tableID.
func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
route := &netlink.Route{
@@ -649,7 +247,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil {
return fmt.Errorf(errParsePrefixMsg, prefix, err)
return fmt.Errorf("parse prefix %s: %w", prefix, err)
}
route.Dst = ipNet
@@ -670,7 +268,7 @@ func addRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil {
return fmt.Errorf(errParsePrefixMsg, prefix, err)
return fmt.Errorf("parse prefix %s: %w", prefix, err)
}
route := &netlink.Route{
@@ -690,7 +288,7 @@ func addUnreachableRoute(prefix netip.Prefix, tableID int) error {
func removeUnreachableRoute(prefix netip.Prefix, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil {
return fmt.Errorf(errParsePrefixMsg, prefix, err)
return fmt.Errorf("parse prefix %s: %w", prefix, err)
}
route := &netlink.Route{
@@ -715,7 +313,7 @@ func removeUnreachableRoute(prefix netip.Prefix, tableID int) error {
func removeRoute(prefix netip.Prefix, nexthop Nexthop, tableID int) error {
_, ipNet, err := net.ParseCIDR(prefix.String())
if err != nil {
return fmt.Errorf(errParsePrefixMsg, prefix, err)
return fmt.Errorf("parse prefix %s: %w", prefix, err)
}
route := &netlink.Route{

View File

@@ -10,25 +10,6 @@ import (
log "github.com/sirupsen/logrus"
)
// IPRule contains IP rule information for debugging
type IPRule struct {
Priority int
From netip.Prefix
To netip.Prefix
IIF string
OIF string
Table string
Action string
Mark uint32
Mask uint32
TunID uint32
Goto uint32
Flow uint32
SuppressPlen int
SuppressIFL int
Invert bool
}
func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
if err := r.validateRoute(prefix); err != nil {
return err
@@ -51,9 +32,3 @@ func EnableIPForwarding() error {
func hasSeparateRouting() ([]netip.Prefix, error) {
return GetRoutesFromTable()
}
// GetIPRules returns IP rules for debugging (not supported on non-Linux platforms)
func GetIPRules() ([]IPRule, error) {
log.Infof("IP rules collection is not supported on %s", runtime.GOOS)
return []IPRule{}, nil
}

View File

@@ -252,7 +252,7 @@ func TestSysOps_validateRoute_InvalidPrefix(t *testing.T) {
IP: wgNetwork.Addr(),
Network: wgNetwork,
},
name: "wt0",
name: "nb0",
}
sysOps := &SysOps{

View File

@@ -40,6 +40,13 @@ type RouteMonitor struct {
done chan struct{}
}
// Route represents a single routing table entry.
type Route struct {
Destination netip.Prefix
Nexthop netip.Addr
Interface *net.Interface
}
type MSFT_NetRoute struct {
DestinationPrefix string
NextHop string
@@ -71,12 +78,6 @@ type MIB_IPFORWARD_ROW2 struct {
Origin uint32
}
// MIB_IPFORWARD_TABLE2 represents a table of IP forward entries
type MIB_IPFORWARD_TABLE2 struct {
NumEntries uint32
Table [1]MIB_IPFORWARD_ROW2 // Flexible array member
}
// IP_ADDRESS_PREFIX is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-ip_address_prefix
type IP_ADDRESS_PREFIX struct {
Prefix SOCKADDR_INET
@@ -107,45 +108,6 @@ type SOCKADDR_INET_NEXTHOP struct {
// MIB_NOTIFICATION_TYPE is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ne-netioapi-mib_notification_type
type MIB_NOTIFICATION_TYPE int32
// MIB_IPINTERFACE_ROW is defined in https://learn.microsoft.com/en-us/windows/win32/api/netioapi/ns-netioapi-mib_ipinterface_row
type MIB_IPINTERFACE_ROW struct {
Family uint16
InterfaceLuid luid
InterfaceIndex uint32
MaxReassemblySize uint32
InterfaceIdentifier uint64
MinRouterAdvertisementInterval uint32
MaxRouterAdvertisementInterval uint32
AdvertisingEnabled uint8
ForwardingEnabled uint8
WeakHostSend uint8
WeakHostReceive uint8
UseAutomaticMetric uint8
UseNeighborUnreachabilityDetection uint8
ManagedAddressConfigurationSupported uint8
OtherStatefulConfigurationSupported uint8
AdvertiseDefaultRoute uint8
RouterDiscoveryBehavior uint32
DadTransmits uint32
BaseReachableTime uint32
RetransmitTime uint32
PathMtuDiscoveryTimeout uint32
LinkLocalAddressBehavior uint32
LinkLocalAddressTimeout uint32
ZoneIndices [16]uint32
SitePrefixLength uint32
Metric uint32
NlMtu uint32
Connected uint8
SupportsWakeUpPatterns uint8
SupportsNeighborDiscovery uint8
SupportsRouterDiscovery uint8
ReachableTime uint32
TransmitOffload uint32
ReceiveOffload uint32
DisableDefaultRoutes uint8
}
var (
modiphlpapi = windows.NewLazyDLL("iphlpapi.dll")
procNotifyRouteChange2 = modiphlpapi.NewProc("NotifyRouteChange2")
@@ -153,11 +115,8 @@ var (
procCreateIpForwardEntry2 = modiphlpapi.NewProc("CreateIpForwardEntry2")
procDeleteIpForwardEntry2 = modiphlpapi.NewProc("DeleteIpForwardEntry2")
procGetIpForwardEntry2 = modiphlpapi.NewProc("GetIpForwardEntry2")
procGetIpForwardTable2 = modiphlpapi.NewProc("GetIpForwardTable2")
procInitializeIpForwardEntry = modiphlpapi.NewProc("InitializeIpForwardEntry")
procConvertInterfaceIndexToLuid = modiphlpapi.NewProc("ConvertInterfaceIndexToLuid")
procGetIpInterfaceEntry = modiphlpapi.NewProc("GetIpInterfaceEntry")
procFreeMibTable = modiphlpapi.NewProc("FreeMibTable")
prefixList []netip.Prefix
lastUpdate time.Time
@@ -470,8 +429,6 @@ func (rm *RouteMonitor) parseUpdate(row *MIB_IPFORWARD_ROW2, notificationType MI
updateType = RouteAdded
case MibDeleteInstance:
updateType = RouteDeleted
case MibInitialNotification:
updateType = RouteAdded // Treat initial notifications as additions
}
update.Type = updateType
@@ -551,7 +508,7 @@ func GetRoutesFromTable() ([]netip.Prefix, error) {
prefixList = nil
for _, route := range routes {
prefixList = append(prefixList, route.Dst)
prefixList = append(prefixList, route.Destination)
}
lastUpdate = time.Now()
@@ -594,159 +551,15 @@ func GetRoutes() ([]Route, error) {
}
routes = append(routes, Route{
Dst: dest,
Gw: nexthop,
Interface: intf,
Destination: dest,
Nexthop: nexthop,
Interface: intf,
})
}
return routes, nil
}
// GetDetailedRoutesFromTable returns detailed route information using Windows syscalls
func GetDetailedRoutesFromTable() ([]DetailedRoute, error) {
table, err := getWindowsRoutingTable()
if err != nil {
return nil, err
}
defer freeWindowsRoutingTable(table)
return parseWindowsRoutingTable(table), nil
}
func getWindowsRoutingTable() (*MIB_IPFORWARD_TABLE2, error) {
var table *MIB_IPFORWARD_TABLE2
ret, _, err := procGetIpForwardTable2.Call(
uintptr(windows.AF_UNSPEC),
uintptr(unsafe.Pointer(&table)),
)
if ret != 0 {
return nil, fmt.Errorf("GetIpForwardTable2 failed: %w", err)
}
if table == nil {
return nil, fmt.Errorf("received nil routing table")
}
return table, nil
}
func freeWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) {
if table != nil {
ret, _, _ := procFreeMibTable.Call(uintptr(unsafe.Pointer(table)))
if ret != 0 {
log.Warnf("FreeMibTable failed with return code: %d", ret)
}
}
}
func parseWindowsRoutingTable(table *MIB_IPFORWARD_TABLE2) []DetailedRoute {
var detailedRoutes []DetailedRoute
entrySize := unsafe.Sizeof(MIB_IPFORWARD_ROW2{})
basePtr := uintptr(unsafe.Pointer(&table.Table[0]))
for i := uint32(0); i < table.NumEntries; i++ {
entryPtr := basePtr + uintptr(i)*entrySize
entry := (*MIB_IPFORWARD_ROW2)(unsafe.Pointer(entryPtr))
detailed := buildWindowsDetailedRoute(entry)
if detailed != nil {
detailedRoutes = append(detailedRoutes, *detailed)
}
}
return detailedRoutes
}
func buildWindowsDetailedRoute(entry *MIB_IPFORWARD_ROW2) *DetailedRoute {
dest := parseIPPrefix(entry.DestinationPrefix, int(entry.InterfaceIndex))
if !dest.IsValid() {
return nil
}
gateway := parseIPNexthop(entry.NextHop, int(entry.InterfaceIndex))
var intf *net.Interface
if entry.InterfaceIndex != 0 {
if netIntf, err := net.InterfaceByIndex(int(entry.InterfaceIndex)); err == nil {
intf = netIntf
} else {
// Create a synthetic interface for display when we can't resolve the name
intf = &net.Interface{
Index: int(entry.InterfaceIndex),
Name: fmt.Sprintf("index-%d", entry.InterfaceIndex),
}
}
}
detailed := DetailedRoute{
Route: Route{
Dst: dest,
Gw: gateway,
Interface: intf,
},
Metric: int(entry.Metric),
InterfaceMetric: getInterfaceMetric(entry.InterfaceIndex, entry.DestinationPrefix.Prefix.sin6_family),
InterfaceIndex: int(entry.InterfaceIndex),
Protocol: windowsProtocolToString(entry.Protocol),
Scope: formatRouteAge(entry.Age),
Type: windowsOriginToString(entry.Origin),
Table: "main",
Flags: "-",
}
return &detailed
}
func windowsProtocolToString(protocol uint32) string {
switch protocol {
case 1:
return "other"
case 2:
return "local"
case 3:
return "netmgmt"
case 4:
return "icmp"
case 5:
return "egp"
case 6:
return "ggp"
case 7:
return "hello"
case 8:
return "rip"
case 9:
return "isis"
case 10:
return "esis"
case 11:
return "cisco"
case 12:
return "bbn"
case 13:
return "ospf"
case 14:
return "bgp"
case 15:
return "idpr"
case 16:
return "eigrp"
case 17:
return "dvmrp"
case 18:
return "rpl"
case 19:
return "dhcp"
default:
return fmt.Sprintf("unknown-%d", protocol)
}
}
func isCacheDisabled() bool {
return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true"
}
@@ -801,59 +614,3 @@ func addZone(ip netip.Addr, interfaceIndex int) netip.Addr {
}
return ip
}
// getInterfaceMetric retrieves the interface metric for a given interface and address family
func getInterfaceMetric(interfaceIndex uint32, family int16) int {
if interfaceIndex == 0 {
return -1
}
var ipInterfaceRow MIB_IPINTERFACE_ROW
ipInterfaceRow.Family = uint16(family)
ipInterfaceRow.InterfaceIndex = interfaceIndex
ret, _, _ := procGetIpInterfaceEntry.Call(uintptr(unsafe.Pointer(&ipInterfaceRow)))
if ret != 0 {
log.Debugf("GetIpInterfaceEntry failed for interface %d: %d", interfaceIndex, ret)
return -1
}
return int(ipInterfaceRow.Metric)
}
// formatRouteAge formats the route age in seconds to a human-readable string
func formatRouteAge(ageSeconds uint32) string {
if ageSeconds == 0 {
return "0s"
}
age := time.Duration(ageSeconds) * time.Second
switch {
case age < time.Minute:
return fmt.Sprintf("%ds", int(age.Seconds()))
case age < time.Hour:
return fmt.Sprintf("%dm", int(age.Minutes()))
case age < 24*time.Hour:
return fmt.Sprintf("%dh", int(age.Hours()))
default:
return fmt.Sprintf("%dd", int(age.Hours()/24))
}
}
// windowsOriginToString converts Windows route origin to string
func windowsOriginToString(origin uint32) string {
switch origin {
case 0:
return "manual"
case 1:
return "wellknown"
case 2:
return "dhcp"
case 3:
return "routeradvert"
case 4:
return "6to4"
default:
return fmt.Sprintf("unknown-%d", origin)
}
}

View File

@@ -0,0 +1,16 @@
package statemanager
import (
"github.com/netbirdio/netbird/client/configs"
"os"
"path/filepath"
)
// GetDefaultStatePath returns the path to the state file based on the operating system
// It returns an empty string if the path cannot be determined.
func GetDefaultStatePath() string {
if path := os.Getenv("NB_DNS_STATE_FILE"); path != "" {
return path
}
return filepath.Join(configs.StateDir, "state.json")
}

View File

@@ -17,7 +17,6 @@ import (
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
"github.com/netbirdio/netbird/management/domain"
@@ -93,7 +92,7 @@ func NewClient(cfgFile, stateFile, deviceName string, osVersion string, osName s
func (c *Client) Run(fd int32, interfaceName string) error {
log.Infof("Starting NetBird client")
log.Debugf("Tunnel uses interface: %s", interfaceName)
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
StateFilePath: c.stateFile,
})
@@ -204,7 +203,7 @@ func (c *Client) IsLoginRequired() bool {
defer c.ctxCancelLock.Unlock()
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
cfg, _ := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
cfg, _ := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
})
@@ -224,7 +223,7 @@ func (c *Client) LoginForMobile() string {
defer c.ctxCancelLock.Unlock()
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
cfg, _ := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
cfg, _ := internal.UpdateOrCreateConfig(internal.ConfigInput{
ConfigPath: c.cfgFile,
})

View File

@@ -12,7 +12,6 @@ import (
"github.com/netbirdio/netbird/client/cmd"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/system"
)
@@ -37,17 +36,17 @@ type URLOpener interface {
// Auth can register or login new client
type Auth struct {
ctx context.Context
config *profilemanager.Config
config *internal.Config
cfgPath string
}
// NewAuth instantiate Auth struct and validate the management URL
func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
inputCfg := profilemanager.ConfigInput{
inputCfg := internal.ConfigInput{
ManagementURL: mgmURL,
}
cfg, err := profilemanager.CreateInMemoryConfig(inputCfg)
cfg, err := internal.CreateInMemoryConfig(inputCfg)
if err != nil {
return nil, err
}
@@ -60,7 +59,7 @@ func NewAuth(cfgPath string, mgmURL string) (*Auth, error) {
}
// NewAuthWithConfig instantiate Auth based on existing config
func NewAuthWithConfig(ctx context.Context, config *profilemanager.Config) *Auth {
func NewAuthWithConfig(ctx context.Context, config *internal.Config) *Auth {
return &Auth{
ctx: ctx,
config: config,
@@ -95,7 +94,7 @@ func (a *Auth) SaveConfigIfSSOSupported() (bool, error) {
return false, fmt.Errorf("backoff cycle failed: %v", err)
}
err = profilemanager.WriteOutConfig(a.cfgPath, a.config)
err = internal.WriteOutConfig(a.cfgPath, a.config)
return true, err
}
@@ -116,7 +115,7 @@ func (a *Auth) LoginWithSetupKeyAndSaveConfig(setupKey string, deviceName string
return fmt.Errorf("backoff cycle failed: %v", err)
}
return profilemanager.WriteOutConfig(a.cfgPath, a.config)
return internal.WriteOutConfig(a.cfgPath, a.config)
}
func (a *Auth) Login() error {

View File

@@ -1,17 +1,17 @@
package NetBirdSDK
import (
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal"
)
// Preferences export a subset of the internal config for gomobile
type Preferences struct {
configInput profilemanager.ConfigInput
configInput internal.ConfigInput
}
// NewPreferences create new Preferences instance
func NewPreferences(configPath string, stateFilePath string) *Preferences {
ci := profilemanager.ConfigInput{
ci := internal.ConfigInput{
ConfigPath: configPath,
StateFilePath: stateFilePath,
}
@@ -24,7 +24,7 @@ func (p *Preferences) GetManagementURL() (string, error) {
return p.configInput.ManagementURL, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
@@ -42,7 +42,7 @@ func (p *Preferences) GetAdminURL() (string, error) {
return p.configInput.AdminURL, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
@@ -60,7 +60,7 @@ func (p *Preferences) GetPreSharedKey() (string, error) {
return *p.configInput.PreSharedKey, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return "", err
}
@@ -83,7 +83,7 @@ func (p *Preferences) GetRosenpassEnabled() (bool, error) {
return *p.configInput.RosenpassEnabled, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -101,7 +101,7 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) {
return *p.configInput.RosenpassPermissive, nil
}
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
cfg, err := internal.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
@@ -110,6 +110,6 @@ func (p *Preferences) GetRosenpassPermissive() (bool, error) {
// Commit write out the changes into config file
func (p *Preferences) Commit() error {
_, err := profilemanager.UpdateOrCreateConfig(p.configInput)
_, err := internal.UpdateOrCreateConfig(p.configInput)
return err
}

View File

@@ -4,7 +4,7 @@ import (
"path/filepath"
"testing"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal"
)
func TestPreferences_DefaultValues(t *testing.T) {
@@ -16,7 +16,7 @@ func TestPreferences_DefaultValues(t *testing.T) {
t.Fatalf("failed to read default value: %s", err)
}
if defaultVar != profilemanager.DefaultAdminURL {
if defaultVar != internal.DefaultAdminURL {
t.Errorf("invalid default admin url: %s", defaultVar)
}
@@ -25,7 +25,7 @@ func TestPreferences_DefaultValues(t *testing.T) {
t.Fatalf("failed to read default management URL: %s", err)
}
if defaultVar != profilemanager.DefaultManagementURL {
if defaultVar != internal.DefaultManagementURL {
t.Errorf("invalid default management url: %s", defaultVar)
}

View File

@@ -1,105 +0,0 @@
#!/usr/bin/env bash
set -eEuo pipefail
: ${NB_ENTRYPOINT_SERVICE_TIMEOUT:="5"}
: ${NB_ENTRYPOINT_LOGIN_TIMEOUT:="1"}
NETBIRD_BIN="${NETBIRD_BIN:-"netbird"}"
export NB_LOG_FILE="${NB_LOG_FILE:-"console,/var/log/netbird/client.log"}"
service_pids=()
log_file_path=""
_log() {
# mimic Go logger's output for easier parsing
# 2025-04-15T21:32:00+08:00 INFO client/internal/config.go:495: setting notifications to disabled by default
printf "$(date -Isec) ${1} ${BASH_SOURCE[1]}:${BASH_LINENO[1]}: ${2}\n" "${@:3}" >&2
}
info() {
_log INFO "$@"
}
warn() {
_log WARN "$@"
}
on_exit() {
info "Shutting down NetBird daemon..."
if test "${#service_pids[@]}" -gt 0; then
info "terminating service process IDs: ${service_pids[@]@Q}"
kill -TERM "${service_pids[@]}" 2>/dev/null || true
wait "${service_pids[@]}" 2>/dev/null || true
else
info "there are no service processes to terminate"
fi
}
wait_for_message() {
local timeout="${1}" message="${2}"
if test "${timeout}" -eq 0; then
info "not waiting for log line ${message@Q} due to zero timeout."
elif test -n "${log_file_path}"; then
info "waiting for log line ${message@Q} for ${timeout} seconds..."
grep -q "${message}" <(timeout "${timeout}" tail -F "${log_file_path}" 2>/dev/null)
else
info "log file unsupported, sleeping for ${timeout} seconds..."
sleep "${timeout}"
fi
}
locate_log_file() {
local log_files_string="${1}"
while read -r log_file; do
case "${log_file}" in
console | syslog) ;;
*)
log_file_path="${log_file}"
return
;;
esac
done < <(sed 's#,#\n#g' <<<"${log_files_string}")
warn "log files parsing for ${log_files_string@Q} is not supported by debug bundles"
warn "please consider removing the \$NB_LOG_FILE or setting it to real file, before gathering debug bundles."
}
wait_for_daemon_startup() {
local timeout="${1}"
if test -n "${log_file_path}"; then
if ! wait_for_message "${timeout}" "started daemon server"; then
warn "log line containing 'started daemon server' not found after ${timeout} seconds"
warn "daemon failed to start, exiting..."
exit 1
fi
else
warn "daemon service startup not discovered, sleeping ${timeout} instead"
sleep "${timeout}"
fi
}
login_if_needed() {
local timeout="${1}"
if test -n "${log_file_path}" && wait_for_message "${timeout}" 'peer has been successfully registered'; then
info "already logged in, skipping 'netbird up'..."
else
info "logging in..."
"${NETBIRD_BIN}" up
fi
}
main() {
trap 'on_exit' SIGTERM SIGINT EXIT
"${NETBIRD_BIN}" service run &
service_pids+=("$!")
info "registered new service process 'netbird service run', currently running: ${service_pids[@]@Q}"
locate_log_file "${NB_LOG_FILE}"
wait_for_daemon_startup "${NB_ENTRYPOINT_SERVICE_TIMEOUT}"
login_if_needed "${NB_ENTRYPOINT_LOGIN_TIMEOUT}"
wait "${service_pids[@]}"
}
main "$@"

File diff suppressed because it is too large Load Diff

View File

@@ -67,18 +67,6 @@ service DaemonService {
rpc SubscribeEvents(SubscribeRequest) returns (stream SystemEvent) {}
rpc GetEvents(GetEventsRequest) returns (GetEventsResponse) {}
rpc SwitchProfile(SwitchProfileRequest) returns (SwitchProfileResponse) {}
rpc SetConfig(SetConfigRequest) returns (SetConfigResponse) {}
rpc AddProfile(AddProfileRequest) returns (AddProfileResponse) {}
rpc RemoveProfile(RemoveProfileRequest) returns (RemoveProfileResponse) {}
rpc ListProfiles(ListProfilesRequest) returns (ListProfilesResponse) {}
rpc GetActiveProfile(GetActiveProfileRequest) returns (GetActiveProfileResponse) {}
}
@@ -148,9 +136,6 @@ message LoginRequest {
optional bool lazyConnectionEnabled = 28;
optional bool block_inbound = 29;
optional string profileName = 30;
optional string username = 31;
}
message LoginResponse {
@@ -165,14 +150,9 @@ message WaitSSOLoginRequest {
string hostname = 2;
}
message WaitSSOLoginResponse {
string email = 1;
}
message WaitSSOLoginResponse {}
message UpRequest {
optional string profileName = 1;
optional string username = 2;
}
message UpRequest {}
message UpResponse {}
@@ -193,10 +173,7 @@ message DownRequest {}
message DownResponse {}
message GetConfigRequest {
string profileName = 1;
string username = 2;
}
message GetConfigRequest {}
message GetConfigResponse {
// managementUrl settings value.
@@ -520,98 +497,3 @@ message GetEventsRequest {}
message GetEventsResponse {
repeated SystemEvent events = 1;
}
message SwitchProfileRequest {
optional string profileName = 1;
optional string username = 2;
}
message SwitchProfileResponse {}
message SetConfigRequest {
string username = 1;
string profileName = 2;
// managementUrl to authenticate.
string managementUrl = 3;
// adminUrl to manage keys.
string adminURL = 4;
optional bool rosenpassEnabled = 5;
optional string interfaceName = 6;
optional int64 wireguardPort = 7;
optional string optionalPreSharedKey = 8;
optional bool disableAutoConnect = 9;
optional bool serverSSHAllowed = 10;
optional bool rosenpassPermissive = 11;
optional bool networkMonitor = 12;
optional bool disable_client_routes = 13;
optional bool disable_server_routes = 14;
optional bool disable_dns = 15;
optional bool disable_firewall = 16;
optional bool block_lan_access = 17;
optional bool disable_notifications = 18;
optional bool lazyConnectionEnabled = 19;
optional bool block_inbound = 20;
repeated string natExternalIPs = 21;
bool cleanNATExternalIPs = 22;
bytes customDNSAddress = 23;
repeated string extraIFaceBlacklist = 24;
repeated string dns_labels = 25;
// cleanDNSLabels clean map list of DNS labels.
bool cleanDNSLabels = 26;
optional google.protobuf.Duration dnsRouteInterval = 27;
}
message SetConfigResponse{}
message AddProfileRequest {
string username = 1;
string profileName = 2;
}
message AddProfileResponse {}
message RemoveProfileRequest {
string username = 1;
string profileName = 2;
}
message RemoveProfileResponse {}
message ListProfilesRequest {
string username = 1;
}
message ListProfilesResponse {
repeated Profile profiles = 1;
}
message Profile {
string name = 1;
bool is_active = 2;
}
message GetActiveProfileRequest {}
message GetActiveProfileResponse {
string profileName = 1;
string username = 2;
}

View File

@@ -55,12 +55,6 @@ type DaemonServiceClient interface {
TracePacket(ctx context.Context, in *TracePacketRequest, opts ...grpc.CallOption) (*TracePacketResponse, error)
SubscribeEvents(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (DaemonService_SubscribeEventsClient, error)
GetEvents(ctx context.Context, in *GetEventsRequest, opts ...grpc.CallOption) (*GetEventsResponse, error)
SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error)
SetConfig(ctx context.Context, in *SetConfigRequest, opts ...grpc.CallOption) (*SetConfigResponse, error)
AddProfile(ctx context.Context, in *AddProfileRequest, opts ...grpc.CallOption) (*AddProfileResponse, error)
RemoveProfile(ctx context.Context, in *RemoveProfileRequest, opts ...grpc.CallOption) (*RemoveProfileResponse, error)
ListProfiles(ctx context.Context, in *ListProfilesRequest, opts ...grpc.CallOption) (*ListProfilesResponse, error)
GetActiveProfile(ctx context.Context, in *GetActiveProfileRequest, opts ...grpc.CallOption) (*GetActiveProfileResponse, error)
}
type daemonServiceClient struct {
@@ -274,60 +268,6 @@ func (c *daemonServiceClient) GetEvents(ctx context.Context, in *GetEventsReques
return out, nil
}
func (c *daemonServiceClient) SwitchProfile(ctx context.Context, in *SwitchProfileRequest, opts ...grpc.CallOption) (*SwitchProfileResponse, error) {
out := new(SwitchProfileResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/SwitchProfile", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) SetConfig(ctx context.Context, in *SetConfigRequest, opts ...grpc.CallOption) (*SetConfigResponse, error) {
out := new(SetConfigResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/SetConfig", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) AddProfile(ctx context.Context, in *AddProfileRequest, opts ...grpc.CallOption) (*AddProfileResponse, error) {
out := new(AddProfileResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/AddProfile", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) RemoveProfile(ctx context.Context, in *RemoveProfileRequest, opts ...grpc.CallOption) (*RemoveProfileResponse, error) {
out := new(RemoveProfileResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/RemoveProfile", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) ListProfiles(ctx context.Context, in *ListProfilesRequest, opts ...grpc.CallOption) (*ListProfilesResponse, error) {
out := new(ListProfilesResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/ListProfiles", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *daemonServiceClient) GetActiveProfile(ctx context.Context, in *GetActiveProfileRequest, opts ...grpc.CallOption) (*GetActiveProfileResponse, error) {
out := new(GetActiveProfileResponse)
err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetActiveProfile", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// DaemonServiceServer is the server API for DaemonService service.
// All implementations must embed UnimplementedDaemonServiceServer
// for forward compatibility
@@ -369,12 +309,6 @@ type DaemonServiceServer interface {
TracePacket(context.Context, *TracePacketRequest) (*TracePacketResponse, error)
SubscribeEvents(*SubscribeRequest, DaemonService_SubscribeEventsServer) error
GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error)
SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error)
SetConfig(context.Context, *SetConfigRequest) (*SetConfigResponse, error)
AddProfile(context.Context, *AddProfileRequest) (*AddProfileResponse, error)
RemoveProfile(context.Context, *RemoveProfileRequest) (*RemoveProfileResponse, error)
ListProfiles(context.Context, *ListProfilesRequest) (*ListProfilesResponse, error)
GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error)
mustEmbedUnimplementedDaemonServiceServer()
}
@@ -442,24 +376,6 @@ func (UnimplementedDaemonServiceServer) SubscribeEvents(*SubscribeRequest, Daemo
func (UnimplementedDaemonServiceServer) GetEvents(context.Context, *GetEventsRequest) (*GetEventsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetEvents not implemented")
}
func (UnimplementedDaemonServiceServer) SwitchProfile(context.Context, *SwitchProfileRequest) (*SwitchProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SwitchProfile not implemented")
}
func (UnimplementedDaemonServiceServer) SetConfig(context.Context, *SetConfigRequest) (*SetConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetConfig not implemented")
}
func (UnimplementedDaemonServiceServer) AddProfile(context.Context, *AddProfileRequest) (*AddProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method AddProfile not implemented")
}
func (UnimplementedDaemonServiceServer) RemoveProfile(context.Context, *RemoveProfileRequest) (*RemoveProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method RemoveProfile not implemented")
}
func (UnimplementedDaemonServiceServer) ListProfiles(context.Context, *ListProfilesRequest) (*ListProfilesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListProfiles not implemented")
}
func (UnimplementedDaemonServiceServer) GetActiveProfile(context.Context, *GetActiveProfileRequest) (*GetActiveProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetActiveProfile not implemented")
}
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
// UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
@@ -836,114 +752,6 @@ func _DaemonService_GetEvents_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _DaemonService_SwitchProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SwitchProfileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).SwitchProfile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/SwitchProfile",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).SwitchProfile(ctx, req.(*SwitchProfileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_SetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetConfigRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).SetConfig(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/SetConfig",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).SetConfig(ctx, req.(*SetConfigRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_AddProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AddProfileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).AddProfile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/AddProfile",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).AddProfile(ctx, req.(*AddProfileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_RemoveProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveProfileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).RemoveProfile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/RemoveProfile",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).RemoveProfile(ctx, req.(*RemoveProfileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_ListProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListProfilesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).ListProfiles(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/ListProfiles",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).ListProfiles(ctx, req.(*ListProfilesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _DaemonService_GetActiveProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetActiveProfileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(DaemonServiceServer).GetActiveProfile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/daemon.DaemonService/GetActiveProfile",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(DaemonServiceServer).GetActiveProfile(ctx, req.(*GetActiveProfileRequest))
}
return interceptor(ctx, in, info, handler)
}
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -1027,30 +835,6 @@ var DaemonService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetEvents",
Handler: _DaemonService_GetEvents_Handler,
},
{
MethodName: "SwitchProfile",
Handler: _DaemonService_SwitchProfile_Handler,
},
{
MethodName: "SetConfig",
Handler: _DaemonService_SetConfig_Handler,
},
{
MethodName: "AddProfile",
Handler: _DaemonService_AddProfile_Handler,
},
{
MethodName: "RemoveProfile",
Handler: _DaemonService_RemoveProfile_Handler,
},
{
MethodName: "ListProfiles",
Handler: _DaemonService_ListProfiles_Handler,
},
{
MethodName: "GetActiveProfile",
Handler: _DaemonService_GetActiveProfile_Handler,
},
},
Streams: []grpc.StreamDesc{
{

View File

@@ -1,6 +1,3 @@
//go:build windows
// +build windows
package server
import (

Some files were not shown because too many files have changed in this diff Show More