Merge branch 'main' into dns-fail-count

This commit is contained in:
Viktor Liu
2025-08-02 13:34:41 +02:00
252 changed files with 12044 additions and 3271 deletions

View File

@@ -9,7 +9,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
libayatana-appindicator3-dev=0.5.5-2+deb11u2 \ libayatana-appindicator3-dev=0.5.5-2+deb11u2 \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& go install -v golang.org/x/tools/gopls@latest && go install -v golang.org/x/tools/gopls@v0.18.1
WORKDIR /app WORKDIR /app

3
.dockerignore-client Normal file
View File

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

View File

@@ -16,6 +16,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: git-town/action@v1 - uses: git-town/action@v1.2.1
with: with:
skip-single-stacks: true skip-single-stacks: true

View File

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

View File

@@ -43,7 +43,7 @@ jobs:
- name: gomobile init - name: gomobile init
run: gomobile init run: gomobile init
- name: build android netbird lib - name: build android netbird lib
run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-checklinkname=0 -X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android
env: env:
CGO_ENABLED: 0 CGO_ENABLED: 0
ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620 ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620

View File

@@ -9,7 +9,7 @@ on:
pull_request: pull_request:
env: env:
SIGN_PIPE_VER: "v0.0.20" SIGN_PIPE_VER: "v0.0.21"
GORELEASER_VER: "v2.3.2" GORELEASER_VER: "v2.3.2"
PRODUCT_NAME: "NetBird" PRODUCT_NAME: "NetBird"
COPYRIGHT: "NetBird GmbH" COPYRIGHT: "NetBird GmbH"
@@ -231,3 +231,17 @@ jobs:
ref: ${{ env.SIGN_PIPE_VER }} ref: ${{ env.SIGN_PIPE_VER }}
token: ${{ secrets.SIGN_GITHUB_TOKEN }} token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}", "skipRelease": false }' inputs: '{ "tag": "${{ github.ref }}", "skipRelease": false }'
post_on_forum:
runs-on: ubuntu-latest
continue-on-error: true
needs: [trigger_signer]
steps:
- uses: Codixer/discourse-topic-github-release-action@v2.0.1
with:
discourse-api-key: ${{ secrets.DISCOURSE_RELEASES_API_KEY }}
discourse-base-url: https://forum.netbird.io
discourse-author-username: NetBird
discourse-category: 17
discourse-tags:
releases

1
.gitignore vendored
View File

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

View File

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

View File

@@ -50,10 +50,9 @@
**Secure.** NetBird enables secure remote access by applying granular access policies while allowing you to manage them intuitively from a single place. Works universally on any infrastructure. **Secure.** NetBird enables secure remote access by applying granular access policies while allowing you to manage them intuitively from a single place. Works universally on any infrastructure.
### Open-Source Network Security in a Single Platform ### Open Source Network Security in a Single Platform
<img width="1188" alt="centralized-network-management 1" src="https://github.com/user-attachments/assets/c28cc8e4-15d2-4d2f-bb97-a6433db39d56" />
![netbird_2](https://github.com/netbirdio/netbird/assets/700848/46bc3b73-508d-4a0e-bb9a-f465d68646ab)
### NetBird on Lawrence Systems (Video) ### NetBird on Lawrence Systems (Video)
[![Watch the video](https://img.youtube.com/vi/Kwrff6h0rEw/0.jpg)](https://www.youtube.com/watch?v=Kwrff6h0rEw) [![Watch the video](https://img.youtube.com/vi/Kwrff6h0rEw/0.jpg)](https://www.youtube.com/watch?v=Kwrff6h0rEw)

View File

@@ -1,9 +1,27 @@
FROM alpine:3.21.3 # 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
# iproute2: busybox doesn't display ip rules properly # iproute2: busybox doesn't display ip rules properly
RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables 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" ]
ARG NETBIRD_BINARY=netbird ARG NETBIRD_BINARY=netbird
COPY ${NETBIRD_BINARY} /usr/local/bin/netbird COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
COPY "${NETBIRD_BINARY}" /usr/local/bin/netbird
ENV NB_FOREGROUND_MODE=true
ENTRYPOINT [ "/usr/local/bin/netbird","up"]

View File

@@ -1,18 +1,33 @@
FROM alpine:3.21.0 # 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
ARG NETBIRD_BINARY=netbird FROM alpine:3.22.0
COPY ${NETBIRD_BINARY} /usr/local/bin/netbird
RUN apk add --no-cache ca-certificates \ RUN apk add --no-cache \
bash \
ca-certificates \
&& adduser -D -h /var/lib/netbird netbird && adduser -D -h /var/lib/netbird netbird
WORKDIR /var/lib/netbird WORKDIR /var/lib/netbird
USER netbird:netbird USER netbird:netbird
ENV NB_FOREGROUND_MODE=true ENV \
ENV NB_USE_NETSTACK_MODE=true NETBIRD_BIN="/usr/local/bin/netbird" \
ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true NB_USE_NETSTACK_MODE="true" \
ENV NB_CONFIG=config.json NB_ENABLE_NETSTACK_LOCAL_FORWARDING="true" \
ENV NB_DAEMON_ADDR=unix://netbird.sock NB_CONFIG="/var/lib/netbird/config.json" \
ENV NB_DISABLE_DNS=true 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"
ENTRYPOINT [ "/usr/local/bin/netbird", "up" ] 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

View File

@@ -13,6 +13,7 @@ import (
"github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer" "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/internal/stdnet"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter" "github.com/netbirdio/netbird/formatter"
@@ -64,7 +65,9 @@ type Client struct {
} }
// NewClient instantiate a new Client // NewClient instantiate a new Client
func NewClient(cfgFile, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client { func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client {
execWorkaround(androidSDKVersion)
net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket) net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
return &Client{ return &Client{
cfgFile: cfgFile, cfgFile: cfgFile,
@@ -80,7 +83,7 @@ func NewClient(cfgFile, deviceName string, uiVersion string, tunAdapter TunAdapt
// Run start the internal client. It is a blocker function // Run start the internal client. It is a blocker function
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener) error { func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener) error {
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{ cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
ConfigPath: c.cfgFile, ConfigPath: c.cfgFile,
}) })
if err != nil { if err != nil {
@@ -115,7 +118,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). // 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. // In this case make no sense handle registration steps.
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error { func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error {
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{ cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
ConfigPath: c.cfgFile, ConfigPath: c.cfgFile,
}) })
if err != nil { if err != nil {

26
client/android/exec.go Normal file
View File

@@ -0,0 +1,26 @@
//go:build android
package android
import (
"fmt"
_ "unsafe"
)
// https://github.com/golang/go/pull/69543/commits/aad6b3b32c81795f86bc4a9e81aad94899daf520
// In Android version 11 and earlier, pidfd-related system calls
// are not allowed by the seccomp policy, which causes crashes due
// to SIGSYS signals.
//go:linkname checkPidfdOnce os.checkPidfdOnce
var checkPidfdOnce func() error
func execWorkaround(androidSDKVersion int) {
if androidSDKVersion > 30 { // above Android 11
return
}
checkPidfdOnce = func() error {
return fmt.Errorf("unsupported Android version")
}
}

View File

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

View File

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

View File

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

View File

@@ -13,14 +13,23 @@ import (
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/debug" "github.com/netbirdio/netbird/client/internal/debug"
"github.com/netbirdio/netbird/client/internal/peer" "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/proto"
"github.com/netbirdio/netbird/client/server" "github.com/netbirdio/netbird/client/server"
nbstatus "github.com/netbirdio/netbird/client/status" nbstatus "github.com/netbirdio/netbird/client/status"
mgmProto "github.com/netbirdio/netbird/management/proto" mgmProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/upload-server/types"
) )
const errCloseConnection = "Failed to close connection: %v" const errCloseConnection = "Failed to close connection: %v"
var (
logFileCount uint32
systemInfoFlag bool
uploadBundleFlag bool
uploadBundleURLFlag string
)
var debugCmd = &cobra.Command{ var debugCmd = &cobra.Command{
Use: "debug", Use: "debug",
Short: "Debugging commands", Short: "Debugging commands",
@@ -88,12 +97,13 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
client := proto.NewDaemonServiceClient(conn) client := proto.NewDaemonServiceClient(conn)
request := &proto.DebugBundleRequest{ request := &proto.DebugBundleRequest{
Anonymize: anonymizeFlag, Anonymize: anonymizeFlag,
Status: getStatusOutput(cmd, anonymizeFlag), Status: getStatusOutput(cmd, anonymizeFlag),
SystemInfo: debugSystemInfoFlag, SystemInfo: systemInfoFlag,
LogFileCount: logFileCount,
} }
if debugUploadBundle { if uploadBundleFlag {
request.UploadURL = debugUploadBundleURL request.UploadURL = uploadBundleURLFlag
} }
resp, err := client.DebugBundle(cmd.Context(), request) resp, err := client.DebugBundle(cmd.Context(), request)
if err != nil { if err != nil {
@@ -105,7 +115,7 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason()) return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
} }
if debugUploadBundle { if uploadBundleFlag {
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey()) cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
} }
@@ -223,12 +233,13 @@ func runForDuration(cmd *cobra.Command, args []string) error {
headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration) headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration)
statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag)) statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag))
request := &proto.DebugBundleRequest{ request := &proto.DebugBundleRequest{
Anonymize: anonymizeFlag, Anonymize: anonymizeFlag,
Status: statusOutput, Status: statusOutput,
SystemInfo: debugSystemInfoFlag, SystemInfo: systemInfoFlag,
LogFileCount: logFileCount,
} }
if debugUploadBundle { if uploadBundleFlag {
request.UploadURL = debugUploadBundleURL request.UploadURL = uploadBundleURLFlag
} }
resp, err := client.DebugBundle(cmd.Context(), request) resp, err := client.DebugBundle(cmd.Context(), request)
if err != nil { if err != nil {
@@ -255,7 +266,7 @@ func runForDuration(cmd *cobra.Command, args []string) error {
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason()) return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
} }
if debugUploadBundle { if uploadBundleFlag {
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey()) cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
} }
@@ -297,7 +308,7 @@ func getStatusOutput(cmd *cobra.Command, anon bool) string {
cmd.PrintErrf("Failed to get status: %v\n", err) cmd.PrintErrf("Failed to get status: %v\n", err)
} else { } else {
statusOutputString = nbstatus.ParseToFullDetailSummary( statusOutputString = nbstatus.ParseToFullDetailSummary(
nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil), nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, "", ""),
) )
} }
return statusOutputString return statusOutputString
@@ -345,7 +356,7 @@ func formatDuration(d time.Duration) string {
return fmt.Sprintf("%02d:%02d:%02d", h, m, s) return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
} }
func generateDebugBundle(config *internal.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) { func generateDebugBundle(config *profilemanager.Config, recorder *peer.Status, connectClient *internal.ConnectClient, logFilePath string) {
var networkMap *mgmProto.NetworkMap var networkMap *mgmProto.NetworkMap
var err error var err error
@@ -375,3 +386,15 @@ func generateDebugBundle(config *internal.Config, recorder *peer.Status, connect
} }
log.Infof("Generated debug bundle from SIGUSR1 at: %s", path) log.Infof("Generated debug bundle from SIGUSR1 at: %s", path)
} }
func init() {
debugBundleCmd.Flags().Uint32VarP(&logFileCount, "log-file-count", "C", 1, "Number of rotated log files to include in debug bundle")
debugBundleCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle")
debugBundleCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server")
debugBundleCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
forCmd.Flags().Uint32VarP(&logFileCount, "log-file-count", "C", 1, "Number of rotated log files to include in debug bundle")
forCmd.Flags().BoolVarP(&systemInfoFlag, "system-info", "S", true, "Adds system information to the debug bundle")
forCmd.Flags().BoolVarP(&uploadBundleFlag, "upload-bundle", "U", false, "Uploads the debug bundle to a server")
forCmd.Flags().StringVar(&uploadBundleURLFlag, "upload-bundle-url", types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
}

View File

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

View File

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

View File

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

View File

@@ -4,10 +4,12 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"os/user"
"runtime" "runtime"
"strings" "strings"
"time" "time"
log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open" "github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
@@ -15,6 +17,7 @@ import (
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/auth" "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/proto"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
@@ -22,19 +25,16 @@ import (
func init() { func init() {
loginCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) 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{ var loginCmd = &cobra.Command{
Use: "login", Use: "login",
Short: "login to the Netbird Management Service (first run)", Short: "login to the Netbird Management Service (first run)",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd) if err := setEnvAndFlags(cmd); err != nil {
return fmt.Errorf("set env and flags: %v", err)
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()) ctx := internal.CtxInitState(context.Background())
@@ -43,6 +43,17 @@ var loginCmd = &cobra.Command{
// nolint // nolint
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName) 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() providedSetupKey, err := getSetupKey()
if err != nil { if err != nil {
@@ -50,97 +61,15 @@ var loginCmd = &cobra.Command{
} }
// workaround to run without service // workaround to run without service
if logFile == "console" { if util.FindFirstLogPath(logFiles) == "" {
err = handleRebrand(cmd) if err := doForegroundLogin(ctx, cmd, providedSetupKey, activeProf); err != nil {
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) return fmt.Errorf("foreground login failed: %v", err)
} }
cmd.Println("Logging successfully")
return nil return nil
} }
conn, err := DialClientGRPCServer(ctx, daemonAddr) if err := doDaemonLogin(ctx, cmd, providedSetupKey, activeProf, username.Username, pm); err != nil {
if err != nil { return fmt.Errorf("daemon login failed: %v", err)
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") cmd.Println("Logging successfully")
@@ -149,7 +78,196 @@ var loginCmd = &cobra.Command{
}, },
} }
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.Config, setupKey string) error { 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 {
needsLogin := false needsLogin := false
err := WithBackOff(func() error { err := WithBackOff(func() error {
@@ -195,7 +313,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
return nil return nil
} }
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) { func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config) (*auth.TokenInfo, error) {
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop()) oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop())
if err != nil { if err != nil {
return nil, err return nil, err
@@ -251,3 +369,16 @@ func isUnixRunningDesktop() bool {
} }
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != "" 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 ( import (
"fmt" "fmt"
"os/user"
"strings" "strings"
"testing" "testing"
"github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
) )
@@ -14,40 +14,41 @@ func TestLogin(t *testing.T) {
mgmAddr := startTestingServices(t) mgmAddr := startTestingServices(t)
tempDir := t.TempDir() tempDir := t.TempDir()
confPath := tempDir + "/config.json"
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
})
mgmtURL := fmt.Sprintf("http://%s", mgmAddr) mgmtURL := fmt.Sprintf("http://%s", mgmAddr)
rootCmd.SetArgs([]string{ rootCmd.SetArgs([]string{
"login", "login",
"--config",
confPath,
"--log-file", "--log-file",
"console", util.LogConsole,
"--setup-key", "--setup-key",
strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"), strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"),
"--management-url", "--management-url",
mgmtURL, mgmtURL,
}) })
err := rootCmd.Execute() // TODO(hakan): fix this test
if err != nil { _ = rootCmd.Execute()
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")
}
} }

236
client/cmd/profile.go Normal file
View File

@@ -0,0 +1,236 @@
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,6 +10,7 @@ import (
"os/signal" "os/signal"
"path" "path"
"runtime" "runtime"
"slices"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@@ -21,8 +22,7 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/upload-server/types"
) )
const ( const (
@@ -38,14 +38,10 @@ const (
serverSSHAllowedFlag = "allow-server-ssh" serverSSHAllowedFlag = "allow-server-ssh"
extraIFaceBlackListFlag = "extra-iface-blacklist" extraIFaceBlackListFlag = "extra-iface-blacklist"
dnsRouteIntervalFlag = "dns-router-interval" dnsRouteIntervalFlag = "dns-router-interval"
systemInfoFlag = "system-info"
enableLazyConnectionFlag = "enable-lazy-connection" enableLazyConnectionFlag = "enable-lazy-connection"
uploadBundle = "upload-bundle"
uploadBundleURL = "upload-bundle-url"
) )
var ( var (
configPath string
defaultConfigPathDir string defaultConfigPathDir string
defaultConfigPath string defaultConfigPath string
oldDefaultConfigPathDir string oldDefaultConfigPathDir string
@@ -55,7 +51,7 @@ var (
defaultLogFile string defaultLogFile string
oldDefaultLogFileDir string oldDefaultLogFileDir string
oldDefaultLogFile string oldDefaultLogFile string
logFile string logFiles []string
daemonAddr string daemonAddr string
managementURL string managementURL string
adminURL string adminURL string
@@ -71,15 +67,12 @@ var (
interfaceName string interfaceName string
wireguardPort uint16 wireguardPort uint16
networkMonitor bool networkMonitor bool
serviceName string
autoConnectDisabled bool autoConnectDisabled bool
extraIFaceBlackList []string extraIFaceBlackList []string
anonymizeFlag bool anonymizeFlag bool
debugSystemInfoFlag bool
dnsRouteInterval time.Duration dnsRouteInterval time.Duration
debugUploadBundle bool
debugUploadBundleURL string
lazyConnEnabled bool lazyConnEnabled bool
profilesDisabled bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "netbird", Use: "netbird",
@@ -123,26 +116,19 @@ func init() {
defaultDaemonAddr = "tcp://127.0.0.1:41731" 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().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\")", internal.DefaultManagementURL)) 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\")", internal.DefaultAdminURL)) rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", profilemanager.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().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
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().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().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)") 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.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.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().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().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().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(upCmd)
rootCmd.AddCommand(downCmd) rootCmd.AddCommand(downCmd)
rootCmd.AddCommand(statusCmd) rootCmd.AddCommand(statusCmd)
@@ -152,9 +138,7 @@ func init() {
rootCmd.AddCommand(networksCMD) rootCmd.AddCommand(networksCMD)
rootCmd.AddCommand(forwardingRulesCmd) rootCmd.AddCommand(forwardingRulesCmd)
rootCmd.AddCommand(debugCmd) 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(routesListCmd)
networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd) networksCMD.AddCommand(routesSelectCmd, routesDeselectCmd)
@@ -167,6 +151,12 @@ func init() {
debugCmd.AddCommand(forCmd) debugCmd.AddCommand(forCmd)
debugCmd.AddCommand(persistenceCmd) debugCmd.AddCommand(persistenceCmd)
// profile commands
profileCmd.AddCommand(profileListCmd)
profileCmd.AddCommand(profileAddCmd)
profileCmd.AddCommand(profileRemoveCmd)
profileCmd.AddCommand(profileSelectCmd)
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil, upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
`Sets external IPs maps between local addresses and interfaces.`+ `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. `+ `You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
@@ -184,11 +174,8 @@ func init() {
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.") upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted") upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted")
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.") upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand.") upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand. Note: this setting may be overridden by management configuration.")
debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle")
debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL))
debugCmd.PersistentFlags().StringVar(&debugUploadBundleURL, uploadBundleURL, types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
} }
// SetupCloseHandler handles SIGTERM signal and exits with success // SetupCloseHandler handles SIGTERM signal and exits with success
@@ -196,14 +183,13 @@ func SetupCloseHandler(ctx context.Context, cancel context.CancelFunc) {
termCh := make(chan os.Signal, 1) termCh := make(chan os.Signal, 1)
signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
go func() { go func() {
done := ctx.Done() defer cancel()
select { select {
case <-done: case <-ctx.Done():
case <-termCh: case <-termCh:
} }
log.Info("shutdown signal received") log.Info("shutdown signal received")
cancel()
}() }()
} }
@@ -287,7 +273,7 @@ func getSetupKeyFromFile(setupKeyPath string) (string, error) {
func handleRebrand(cmd *cobra.Command) error { func handleRebrand(cmd *cobra.Command) error {
var err error var err error
if logFile == defaultLogFile { if slices.Contains(logFiles, defaultLogFile) {
if migrateToNetbird(oldDefaultLogFile, defaultLogFile) { if migrateToNetbird(oldDefaultLogFile, defaultLogFile) {
cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir) cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir)
err = cpDir(oldDefaultLogFileDir, defaultLogFileDir) err = cpDir(oldDefaultLogFileDir, defaultLogFileDir)
@@ -296,15 +282,14 @@ func handleRebrand(cmd *cobra.Command) error {
} }
} }
} }
if configPath == defaultConfigPath { if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) {
if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) { cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir)
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir) err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir)
err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir) if err != nil {
if err != nil { return err
return err
}
} }
} }
return nil return nil
} }

View File

@@ -1,12 +1,15 @@
//go:build !ios && !android
package cmd package cmd
import ( import (
"context" "context"
"fmt"
"runtime" "runtime"
"strings"
"sync" "sync"
"github.com/kardianos/service" "github.com/kardianos/service"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc" "google.golang.org/grpc"
@@ -14,6 +17,16 @@ import (
"github.com/netbirdio/netbird/client/server" "github.com/netbirdio/netbird/client/server"
) )
var serviceCmd = &cobra.Command{
Use: "service",
Short: "manages Netbird service",
}
var (
serviceName string
serviceEnvVars []string
)
type program struct { type program struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@@ -22,12 +35,32 @@ type program struct {
serverInstanceMu sync.Mutex 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 { func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
ctx = internal.CtxInitState(ctx) ctx = internal.CtxInitState(ctx)
return &program{ctx: ctx, cancel: cancel} return &program{ctx: ctx, cancel: cancel}
} }
func newSVCConfig() *service.Config { func newSVCConfig() (*service.Config, error) {
config := &service.Config{ config := &service.Config{
Name: serviceName, Name: serviceName,
DisplayName: "Netbird", DisplayName: "Netbird",
@@ -36,23 +69,47 @@ func newSVCConfig() *service.Config {
EnvVars: make(map[string]string), 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" { if runtime.GOOS == "linux" {
config.EnvVars["SYSTEMD_UNIT"] = serviceName config.EnvVars["SYSTEMD_UNIT"] = serviceName
} }
return config return config, nil
} }
func newSVC(prg *program, conf *service.Config) (service.Service, error) { func newSVC(prg *program, conf *service.Config) (service.Service, error) {
s, err := service.New(prg, conf) return service.New(prg, conf)
if err != nil {
log.Fatal(err)
return nil, err
}
return s, nil
} }
var serviceCmd = &cobra.Command{ func parseServiceEnvVars(envVars []string) (map[string]string, error) {
Use: "service", envMap := make(map[string]string)
Short: "manages Netbird service",
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
}
return envMap, nil
} }

View File

@@ -1,3 +1,5 @@
//go:build !ios && !android
package cmd package cmd
import ( import (
@@ -47,20 +49,19 @@ func (p *program) Start(svc service.Service) error {
listen, err := net.Listen(split[0], split[1]) listen, err := net.Listen(split[0], split[1])
if err != nil { if err != nil {
return fmt.Errorf("failed to listen daemon interface: %w", err) return fmt.Errorf("listen daemon interface: %w", err)
} }
go func() { go func() {
defer listen.Close() defer listen.Close()
if split[0] == "unix" { if split[0] == "unix" {
err = os.Chmod(split[1], 0666) if err := os.Chmod(split[1], 0666); err != nil {
if err != nil {
log.Errorf("failed setting daemon permissions: %v", split[1]) log.Errorf("failed setting daemon permissions: %v", split[1])
return return
} }
} }
serverInstance := server.New(p.ctx, configPath, logFile) serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), profilesDisabled)
if err := serverInstance.Start(); err != nil { if err := serverInstance.Start(); err != nil {
log.Fatalf("failed to start daemon: %v", err) log.Fatalf("failed to start daemon: %v", err)
} }
@@ -100,37 +101,49 @@ func (p *program) Stop(srv service.Service) error {
return nil 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{ var runCmd = &cobra.Command{
Use: "run", Use: "run",
Short: "runs Netbird as service", Short: "runs Netbird as service",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context()) ctx, cancel := context.WithCancel(cmd.Context())
SetupCloseHandler(ctx, cancel)
SetupDebugHandler(ctx, nil, nil, nil, logFile)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig()) SetupCloseHandler(ctx, cancel)
SetupDebugHandler(ctx, nil, nil, nil, util.FindFirstLogPath(logFiles))
s, err := setupServiceControlCommand(cmd, ctx, cancel)
if err != nil { if err != nil {
return err return err
} }
err = s.Run()
if err != nil { return s.Run()
return err
}
return nil
}, },
} }
@@ -138,31 +151,14 @@ var startCmd = &cobra.Command{
Use: "start", Use: "start",
Short: "starts Netbird service", Short: "starts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(cmd.Context()) ctx, cancel := context.WithCancel(cmd.Context())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil { if err != nil {
cmd.PrintErrln(err)
return err return err
} }
err = s.Start()
if err != nil { if err := s.Start(); err != nil {
cmd.PrintErrln(err) return fmt.Errorf("start service: %w", err)
return err
} }
cmd.Println("Netbird service has been started") cmd.Println("Netbird service has been started")
return nil return nil
@@ -173,29 +169,14 @@ var stopCmd = &cobra.Command{
Use: "stop", Use: "stop",
Short: "stops Netbird service", Short: "stops Netbird service",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context()) ctx, cancel := context.WithCancel(cmd.Context())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil { if err != nil {
return err return err
} }
err = s.Stop()
if err != nil { if err := s.Stop(); err != nil {
return err return fmt.Errorf("stop service: %w", err)
} }
cmd.Println("Netbird service has been stopped") cmd.Println("Netbird service has been stopped")
return nil return nil
@@ -206,31 +187,48 @@ var restartCmd = &cobra.Command{
Use: "restart", Use: "restart",
Short: "restarts Netbird service", Short: "restarts Netbird service",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd)
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err
}
err = util.InitLog(logLevel, logFile)
if err != nil {
return fmt.Errorf("failed initializing log %v", err)
}
ctx, cancel := context.WithCancel(cmd.Context()) ctx, cancel := context.WithCancel(cmd.Context())
s, err := setupServiceControlCommand(cmd, ctx, cancel)
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
if err != nil { if err != nil {
return err return err
} }
err = s.Restart()
if err != nil { if err := s.Restart(); err != nil {
return err return fmt.Errorf("restart service: %w", err)
} }
cmd.Println("Netbird service has been restarted") cmd.Println("Netbird service has been restarted")
return nil 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,87 +1,121 @@
//go:build !ios && !android
package cmd package cmd
import ( import (
"context" "context"
"errors"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"github.com/kardianos/service"
"github.com/spf13/cobra" "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{ var installCmd = &cobra.Command{
Use: "install", Use: "install",
Short: "installs Netbird service", Short: "installs Netbird service",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd) if err := setupServiceCommand(cmd); err != nil {
cmd.SetOut(cmd.OutOrStdout())
err := handleRebrand(cmd)
if err != nil {
return err return err
} }
svcConfig := newSVCConfig() svcConfig, err := createServiceConfigForInstall()
if err != nil {
svcConfig.Arguments = []string{ return err
"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()) ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
s, err := newSVC(newProgram(ctx, cancel), svcConfig) s, err := newSVC(newProgram(ctx, cancel), svcConfig)
if err != nil { if err != nil {
cmd.PrintErrln(err)
return err return err
} }
err = s.Install() if err := s.Install(); err != nil {
if err != nil { return fmt.Errorf("install service: %w", err)
cmd.PrintErrln(err)
return err
} }
cmd.Println("Netbird service has been installed") cmd.Println("Netbird service has been installed")
@@ -93,27 +127,109 @@ var uninstallCmd = &cobra.Command{
Use: "uninstall", Use: "uninstall",
Short: "uninstalls Netbird service from system", Short: "uninstalls Netbird service from system",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
SetFlagsFromEnvVars(rootCmd) if err := setupServiceCommand(cmd); err != nil {
return err
}
cmd.SetOut(cmd.OutOrStdout()) cfg, err := newSVCConfig()
if err != nil {
return fmt.Errorf("create service config: %w", err)
}
err := handleRebrand(cmd) ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
s, err := newSVC(newProgram(ctx, cancel), cfg)
if err != nil {
return err
}
if err := s.Uninstall(); err != nil {
return fmt.Errorf("uninstall service: %w", 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 { if err != nil {
return err return err
} }
ctx, cancel := context.WithCancel(cmd.Context()) ctx, cancel := context.WithCancel(cmd.Context())
defer cancel()
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig()) s, err := newSVC(newProgram(ctx, cancel), svcConfig)
if err != nil { if err != nil {
return err return fmt.Errorf("create service: %w", err)
} }
err = s.Uninstall() if wasRunning {
if err != nil { cmd.Println("Stopping Netbird service...")
return err if err := s.Stop(); err != nil {
cmd.Printf("Warning: failed to stop service: %v\n", err)
}
} }
cmd.Println("Netbird service has been uninstalled")
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 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
}

263
client/cmd/service_test.go Normal file
View File

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

View File

@@ -11,6 +11,7 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
"github.com/netbirdio/netbird/client/proto" "github.com/netbirdio/netbird/client/proto"
nbstatus "github.com/netbirdio/netbird/client/status" nbstatus "github.com/netbirdio/netbird/client/status"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
@@ -26,6 +27,7 @@ var (
statusFilter string statusFilter string
ipsFilterMap map[string]struct{} ipsFilterMap map[string]struct{}
prefixNamesFilterMap map[string]struct{} prefixNamesFilterMap map[string]struct{}
connectionTypeFilter string
) )
var statusCmd = &cobra.Command{ var statusCmd = &cobra.Command{
@@ -45,6 +47,7 @@ func init() {
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200") statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud") statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected") statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected")
statusCmd.PersistentFlags().StringVar(&connectionTypeFilter, "filter-by-connection-type", "", "filters the detailed output by connection type (P2P|Relayed), e.g., --filter-by-connection-type P2P")
} }
func statusFunc(cmd *cobra.Command, args []string) error { func statusFunc(cmd *cobra.Command, args []string) error {
@@ -57,7 +60,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
return err return err
} }
err = util.InitLog(logLevel, "console") err = util.InitLog(logLevel, util.LogConsole)
if err != nil { if err != nil {
return fmt.Errorf("failed initializing log %v", err) return fmt.Errorf("failed initializing log %v", err)
} }
@@ -89,7 +92,13 @@ func statusFunc(cmd *cobra.Command, args []string) error {
return nil return nil
} }
var outputInformationHolder = nbstatus.ConvertToStatusOutputOverview(resp, anonymizeFlag, statusFilter, prefixNamesFilter, prefixNamesFilterMap, ipsFilterMap) 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 statusOutputString string var statusOutputString string
switch { switch {
case detailFlag: case detailFlag:
@@ -156,6 +165,15 @@ func parseFilters() error {
enableDetailFlagWhenFilterFlag() enableDetailFlagWhenFilterFlag()
} }
switch strings.ToLower(connectionTypeFilter) {
case "", "p2p", "relayed":
if strings.ToLower(connectionTypeFilter) != "" {
enableDetailFlagWhenFilterFlag()
}
default:
return fmt.Errorf("wrong connection-type filter, should be one of P2P|Relayed, got: %s", connectionTypeFilter)
}
return nil return nil
} }

View File

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

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"os/user"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@@ -12,12 +13,14 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status" gstatus "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
"github.com/netbirdio/netbird/client/iface" "github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer" "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/proto"
"github.com/netbirdio/netbird/client/system" "github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/domain"
@@ -35,6 +38,9 @@ const (
noBrowserFlag = "no-browser" noBrowserFlag = "no-browser"
noBrowserDesc = "do not open the browser for SSO login" 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 ( var (
@@ -42,6 +48,8 @@ var (
dnsLabels []string dnsLabels []string
dnsLabelsValidated domain.List dnsLabelsValidated domain.List
noBrowser bool noBrowser bool
profileName string
configPath string
upCmd = &cobra.Command{ upCmd = &cobra.Command{
Use: "up", Use: "up",
@@ -70,6 +78,8 @@ func init() {
) )
upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc) upCmd.PersistentFlags().BoolVar(&noBrowser, noBrowserFlag, false, noBrowserDesc)
upCmd.PersistentFlags().StringVar(&profileName, profileNameFlag, "", profileNameDesc)
upCmd.PersistentFlags().StringVarP(&configPath, "config", "c", "", "(DEPRECATED) Netbird config file location")
} }
@@ -79,7 +89,7 @@ func upFunc(cmd *cobra.Command, args []string) error {
cmd.SetOut(cmd.OutOrStdout()) cmd.SetOut(cmd.OutOrStdout())
err := util.InitLog(logLevel, "console") err := util.InitLog(logLevel, util.LogConsole)
if err != nil { if err != nil {
return fmt.Errorf("failed initializing log %v", err) return fmt.Errorf("failed initializing log %v", err)
} }
@@ -101,13 +111,41 @@ func upFunc(cmd *cobra.Command, args []string) error {
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName) ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
} }
if foregroundMode { pm := profilemanager.NewProfileManager()
return runInForegroundMode(ctx, cmd)
username, err := user.Current()
if err != nil {
return fmt.Errorf("get current user: %v", err)
} }
return runInDaemonMode(ctx, cmd)
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 runInDaemonMode(ctx, cmd, pm, activeProf, profileSwitched)
} }
func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error { func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *profilemanager.Profile) error {
err := handleRebrand(cmd) err := handleRebrand(cmd)
if err != nil { if err != nil {
return err return err
@@ -118,7 +156,12 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
return err return err
} }
ic, err := setupConfig(customDNSAddressConverted, cmd) configFilePath, err := activeProf.FilePath()
if err != nil {
return fmt.Errorf("get active profile file path: %v", err)
}
ic, err := setupConfig(customDNSAddressConverted, cmd, configFilePath)
if err != nil { if err != nil {
return fmt.Errorf("setup config: %v", err) return fmt.Errorf("setup config: %v", err)
} }
@@ -128,12 +171,12 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
return err return err
} }
config, err := internal.UpdateOrCreateConfig(*ic) config, err := profilemanager.UpdateOrCreateConfig(*ic)
if err != nil { if err != nil {
return fmt.Errorf("get config file: %v", err) return fmt.Errorf("get config file: %v", err)
} }
config, _ = internal.UpdateOldManagementURL(ctx, config, configPath) _, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
err = foregroundLogin(ctx, cmd, config, providedSetupKey) err = foregroundLogin(ctx, cmd, config, providedSetupKey)
if err != nil { if err != nil {
@@ -153,10 +196,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
return connectClient.Run(nil) return connectClient.Run(nil)
} }
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error { func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, profileSwitched bool) error {
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed) customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
if err != nil { if err != nil {
return err return fmt.Errorf("parse custom DNS address: %v", err)
} }
conn, err := DialClientGRPCServer(ctx, daemonAddr) conn, err := DialClientGRPCServer(ctx, daemonAddr)
@@ -181,10 +224,41 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
} }
if status.Status == string(internal.StatusConnected) { if status.Status == string(internal.StatusConnected) {
cmd.Println("Already connected") if !profileSwitched {
return nil 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
}
} }
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() providedSetupKey, err := getSetupKey()
if err != nil { if err != nil {
return fmt.Errorf("get setup key: %v", err) return fmt.Errorf("get setup key: %v", err)
@@ -195,6 +269,9 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
return fmt.Errorf("setup login request: %v", err) return fmt.Errorf("setup login request: %v", err)
} }
loginRequest.ProfileName = &activeProf.Name
loginRequest.Username = &username
var loginErr error var loginErr error
var loginResp *proto.LoginResponse var loginResp *proto.LoginResponse
@@ -219,27 +296,105 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
} }
if loginResp.NeedsSSOLogin { if loginResp.NeedsSSOLogin {
if err := handleSSOLogin(ctx, cmd, loginResp, client, pm); err != nil {
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser) return fmt.Errorf("sso login failed: %v", err)
_, 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{}); err != nil { if _, err := client.Up(ctx, &proto.UpRequest{
ProfileName: &activeProf.Name,
Username: &username,
}); err != nil {
return fmt.Errorf("call service up method: %v", err) return fmt.Errorf("call service up method: %v", err)
} }
cmd.Println("Connected")
return nil return nil
} }
func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command) (*internal.ConfigInput, error) { func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, profileName, username string) *proto.SetConfigRequest {
ic := internal.ConfigInput{ 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{
ManagementURL: managementURL, ManagementURL: managementURL,
AdminURL: adminURL, ConfigPath: configFilePath,
ConfigPath: configPath,
NATExternalIPs: natExternalIPs, NATExternalIPs: natExternalIPs,
CustomDNSAddress: customDNSAddressConverted, CustomDNSAddress: customDNSAddressConverted,
ExtraIFaceBlackList: extraIFaceBlackList, ExtraIFaceBlackList: extraIFaceBlackList,
@@ -325,7 +480,6 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
loginRequest := proto.LoginRequest{ loginRequest := proto.LoginRequest{
SetupKey: providedSetupKey, SetupKey: providedSetupKey,
ManagementUrl: managementURL, ManagementUrl: managementURL,
AdminURL: adminURL,
NatExternalIPs: natExternalIPs, NatExternalIPs: natExternalIPs,
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0, CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
CustomDNSAddress: customDNSAddressConverted, CustomDNSAddress: customDNSAddressConverted,
@@ -484,7 +638,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) {
if !isValidAddrPort(customDNSAddress) { 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) return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress)
} }
if customDNSAddress == "" && logFile != "console" { if customDNSAddress == "" && util.FindFirstLogPath(logFiles) != "" {
parsed = []byte("empty") parsed = []byte("empty")
} else { } else {
parsed = []byte(customDNSAddress) parsed = []byte(customDNSAddress)

View File

@@ -3,18 +3,55 @@ package cmd
import ( import (
"context" "context"
"os" "os"
"os/user"
"testing" "testing"
"time" "time"
"github.com/netbirdio/netbird/client/internal" "github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/profilemanager"
) )
var cliAddr string var cliAddr string
func TestUpDaemon(t *testing.T) { func TestUpDaemon(t *testing.T) {
mgmAddr := startTestingServices(t)
tempDir := t.TempDir() 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)
confPath := tempDir + "/config.json" confPath := tempDir + "/config.json"
ctx := internal.CtxInitState(context.Background()) ctx := internal.CtxInitState(context.Background())

View File

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

View File

@@ -221,7 +221,7 @@ func (t *ICMPTracker) track(
// non echo requests don't need tracking // non echo requests don't need tracking
if typ != uint8(layers.ICMPv4TypeEchoRequest) { if typ != uint8(layers.ICMPv4TypeEchoRequest) {
t.logger.Trace("New %s ICMP connection %s - %s", direction, key, icmpInfo) t.logger.Trace3("New %s ICMP connection %s - %s", direction, key, icmpInfo)
t.sendStartEvent(direction, srcIP, dstIP, typ, code, ruleId, size) t.sendStartEvent(direction, srcIP, dstIP, typ, code, ruleId, size)
return return
} }
@@ -243,7 +243,7 @@ func (t *ICMPTracker) track(
t.connections[key] = conn t.connections[key] = conn
t.mutex.Unlock() t.mutex.Unlock()
t.logger.Trace("New %s ICMP connection %s - %s", direction, key, icmpInfo) t.logger.Trace3("New %s ICMP connection %s - %s", direction, key, icmpInfo)
t.sendEvent(nftypes.TypeStart, conn, ruleId) t.sendEvent(nftypes.TypeStart, conn, ruleId)
} }
@@ -294,7 +294,7 @@ func (t *ICMPTracker) cleanup() {
if conn.timeoutExceeded(t.timeout) { if conn.timeoutExceeded(t.timeout) {
delete(t.connections, key) delete(t.connections, key)
t.logger.Trace("Removed ICMP connection %s (timeout) [in: %d Pkts/%d B out: %d Pkts/%d B]", t.logger.Trace5("Removed ICMP connection %s (timeout) [in: %d Pkts/%d B out: %d Pkts/%d B]",
key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load())
t.sendEvent(nftypes.TypeEnd, conn, nil) t.sendEvent(nftypes.TypeEnd, conn, nil)
} }

View File

@@ -211,7 +211,7 @@ func (t *TCPTracker) track(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, fla
conn.tombstone.Store(false) conn.tombstone.Store(false)
conn.state.Store(int32(TCPStateNew)) conn.state.Store(int32(TCPStateNew))
t.logger.Trace("New %s TCP connection: %s", direction, key) t.logger.Trace2("New %s TCP connection: %s", direction, key)
t.updateState(key, conn, flags, direction, size) t.updateState(key, conn, flags, direction, size)
t.mutex.Lock() t.mutex.Lock()
@@ -240,7 +240,7 @@ func (t *TCPTracker) IsValidInbound(srcIP, dstIP netip.Addr, srcPort, dstPort ui
currentState := conn.GetState() currentState := conn.GetState()
if !t.isValidStateForFlags(currentState, flags) { if !t.isValidStateForFlags(currentState, flags) {
t.logger.Warn("TCP state %s is not valid with flags %x for connection %s", currentState, flags, key) t.logger.Warn3("TCP state %s is not valid with flags %x for connection %s", currentState, flags, key)
// allow all flags for established for now // allow all flags for established for now
if currentState == TCPStateEstablished { if currentState == TCPStateEstablished {
return true return true
@@ -262,7 +262,7 @@ func (t *TCPTracker) updateState(key ConnKey, conn *TCPConnTrack, flags uint8, p
if flags&TCPRst != 0 { if flags&TCPRst != 0 {
if conn.CompareAndSwapState(currentState, TCPStateClosed) { if conn.CompareAndSwapState(currentState, TCPStateClosed) {
conn.SetTombstone() conn.SetTombstone()
t.logger.Trace("TCP connection reset: %s (dir: %s) [in: %d Pkts/%d B, out: %d Pkts/%d B]", t.logger.Trace6("TCP connection reset: %s (dir: %s) [in: %d Pkts/%d B, out: %d Pkts/%d B]",
key, packetDir, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) key, packetDir, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load())
t.sendEvent(nftypes.TypeEnd, conn, nil) t.sendEvent(nftypes.TypeEnd, conn, nil)
} }
@@ -340,17 +340,17 @@ func (t *TCPTracker) updateState(key ConnKey, conn *TCPConnTrack, flags uint8, p
} }
if newState != 0 && conn.CompareAndSwapState(currentState, newState) { if newState != 0 && conn.CompareAndSwapState(currentState, newState) {
t.logger.Trace("TCP connection %s transitioned from %s to %s (dir: %s)", key, currentState, newState, packetDir) t.logger.Trace4("TCP connection %s transitioned from %s to %s (dir: %s)", key, currentState, newState, packetDir)
switch newState { switch newState {
case TCPStateTimeWait: case TCPStateTimeWait:
t.logger.Trace("TCP connection %s completed [in: %d Pkts/%d B, out: %d Pkts/%d B]", t.logger.Trace5("TCP connection %s completed [in: %d Pkts/%d B, out: %d Pkts/%d B]",
key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load())
t.sendEvent(nftypes.TypeEnd, conn, nil) t.sendEvent(nftypes.TypeEnd, conn, nil)
case TCPStateClosed: case TCPStateClosed:
conn.SetTombstone() conn.SetTombstone()
t.logger.Trace("TCP connection %s closed gracefully [in: %d Pkts/%d, B out: %d Pkts/%d B]", t.logger.Trace5("TCP connection %s closed gracefully [in: %d Pkts/%d, B out: %d Pkts/%d B]",
key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load())
t.sendEvent(nftypes.TypeEnd, conn, nil) t.sendEvent(nftypes.TypeEnd, conn, nil)
} }
@@ -438,7 +438,7 @@ func (t *TCPTracker) cleanup() {
if conn.timeoutExceeded(timeout) { if conn.timeoutExceeded(timeout) {
delete(t.connections, key) delete(t.connections, key)
t.logger.Trace("Cleaned up timed-out TCP connection %s (%s) [in: %d Pkts/%d, B out: %d Pkts/%d B]", t.logger.Trace6("Cleaned up timed-out TCP connection %s (%s) [in: %d Pkts/%d, B out: %d Pkts/%d B]",
key, conn.GetState(), conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) key, conn.GetState(), conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load())
// event already handled by state change // event already handled by state change

View File

@@ -116,7 +116,7 @@ func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, d
t.connections[key] = conn t.connections[key] = conn
t.mutex.Unlock() t.mutex.Unlock()
t.logger.Trace("New %s UDP connection: %s", direction, key) t.logger.Trace2("New %s UDP connection: %s", direction, key)
t.sendEvent(nftypes.TypeStart, conn, ruleID) t.sendEvent(nftypes.TypeStart, conn, ruleID)
} }
@@ -165,7 +165,7 @@ func (t *UDPTracker) cleanup() {
if conn.timeoutExceeded(t.timeout) { if conn.timeoutExceeded(t.timeout) {
delete(t.connections, key) delete(t.connections, key)
t.logger.Trace("Removed UDP connection %s (timeout) [in: %d Pkts/%d B, out: %d Pkts/%d B]", t.logger.Trace5("Removed UDP connection %s (timeout) [in: %d Pkts/%d B, out: %d Pkts/%d B]",
key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load()) key, conn.PacketsRx.Load(), conn.BytesRx.Load(), conn.PacketsTx.Load(), conn.BytesTx.Load())
t.sendEvent(nftypes.TypeEnd, conn, nil) t.sendEvent(nftypes.TypeEnd, conn, nil)
} }

View File

@@ -601,7 +601,7 @@ func (m *Manager) filterOutbound(packetData []byte, size int) bool {
srcIP, dstIP := m.extractIPs(d) srcIP, dstIP := m.extractIPs(d)
if !srcIP.IsValid() { if !srcIP.IsValid() {
m.logger.Error("Unknown network layer: %v", d.decoded[0]) m.logger.Error1("Unknown network layer: %v", d.decoded[0])
return false return false
} }
@@ -727,13 +727,13 @@ func (m *Manager) filterInbound(packetData []byte, size int) bool {
srcIP, dstIP := m.extractIPs(d) srcIP, dstIP := m.extractIPs(d)
if !srcIP.IsValid() { if !srcIP.IsValid() {
m.logger.Error("Unknown network layer: %v", d.decoded[0]) m.logger.Error1("Unknown network layer: %v", d.decoded[0])
return true return true
} }
// TODO: pass fragments of routed packets to forwarder // TODO: pass fragments of routed packets to forwarder
if fragment { if fragment {
m.logger.Trace("packet is a fragment: src=%v dst=%v id=%v flags=%v", m.logger.Trace4("packet is a fragment: src=%v dst=%v id=%v flags=%v",
srcIP, dstIP, d.ip4.Id, d.ip4.Flags) srcIP, dstIP, d.ip4.Id, d.ip4.Flags)
return false return false
} }
@@ -741,7 +741,7 @@ func (m *Manager) filterInbound(packetData []byte, size int) bool {
if translated := m.translateInboundReverse(packetData, d); translated { if translated := m.translateInboundReverse(packetData, d); translated {
// Re-decode after translation to get original addresses // Re-decode after translation to get original addresses
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
m.logger.Error("Failed to re-decode packet after reverse DNAT: %v", err) m.logger.Error1("Failed to re-decode packet after reverse DNAT: %v", err)
return true return true
} }
srcIP, dstIP = m.extractIPs(d) srcIP, dstIP = m.extractIPs(d)
@@ -766,7 +766,7 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet
_, pnum := getProtocolFromPacket(d) _, pnum := getProtocolFromPacket(d)
srcPort, dstPort := getPortsFromPacket(d) srcPort, dstPort := getPortsFromPacket(d)
m.logger.Trace("Dropping local packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", m.logger.Trace6("Dropping local packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d",
ruleID, pnum, srcIP, srcPort, dstIP, dstPort) ruleID, pnum, srcIP, srcPort, dstIP, dstPort)
m.flowLogger.StoreEvent(nftypes.EventFields{ m.flowLogger.StoreEvent(nftypes.EventFields{
@@ -807,7 +807,7 @@ func (m *Manager) handleForwardedLocalTraffic(packetData []byte) bool {
} }
if err := fwd.InjectIncomingPacket(packetData); err != nil { if err := fwd.InjectIncomingPacket(packetData); err != nil {
m.logger.Error("Failed to inject local packet: %v", err) m.logger.Error1("Failed to inject local packet: %v", err)
} }
// don't process this packet further // don't process this packet further
@@ -819,7 +819,7 @@ func (m *Manager) handleForwardedLocalTraffic(packetData []byte) bool {
func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packetData []byte, size int) bool { func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packetData []byte, size int) bool {
// Drop if routing is disabled // Drop if routing is disabled
if !m.routingEnabled.Load() { if !m.routingEnabled.Load() {
m.logger.Trace("Dropping routed packet (routing disabled): src=%s dst=%s", m.logger.Trace2("Dropping routed packet (routing disabled): src=%s dst=%s",
srcIP, dstIP) srcIP, dstIP)
return true return true
} }
@@ -835,7 +835,7 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe
ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort)
if !pass { if !pass {
m.logger.Trace("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d", m.logger.Trace6("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d",
ruleID, pnum, srcIP, srcPort, dstIP, dstPort) ruleID, pnum, srcIP, srcPort, dstIP, dstPort)
m.flowLogger.StoreEvent(nftypes.EventFields{ m.flowLogger.StoreEvent(nftypes.EventFields{
@@ -863,7 +863,7 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe
fwd.RegisterRuleID(srcIP, dstIP, srcPort, dstPort, ruleID) fwd.RegisterRuleID(srcIP, dstIP, srcPort, dstPort, ruleID)
if err := fwd.InjectIncomingPacket(packetData); err != nil { if err := fwd.InjectIncomingPacket(packetData); err != nil {
m.logger.Error("Failed to inject routed packet: %v", err) m.logger.Error1("Failed to inject routed packet: %v", err)
fwd.DeleteRuleID(srcIP, dstIP, srcPort, dstPort) fwd.DeleteRuleID(srcIP, dstIP, srcPort, dstPort)
} }
} }
@@ -901,7 +901,7 @@ func getPortsFromPacket(d *decoder) (srcPort, dstPort uint16) {
// It returns true, true if the packet is a fragment and valid. // It returns true, true if the packet is a fragment and valid.
func (m *Manager) isValidPacket(d *decoder, packetData []byte) (bool, bool) { func (m *Manager) isValidPacket(d *decoder, packetData []byte) (bool, bool) {
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil { if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
m.logger.Trace("couldn't decode packet, err: %s", err) m.logger.Trace1("couldn't decode packet, err: %s", err)
return false, false return false, false
} }

View File

@@ -57,7 +57,7 @@ func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error)
address := netHeader.DestinationAddress() address := netHeader.DestinationAddress()
err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice()) err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice())
if err != nil { if err != nil {
e.logger.Error("CreateOutboundPacket: %v", err) e.logger.Error1("CreateOutboundPacket: %v", err)
continue continue
} }
written++ written++

View File

@@ -34,14 +34,14 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
// TODO: support non-root // TODO: support non-root
conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0") conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0")
if err != nil { if err != nil {
f.logger.Error("forwarder: Failed to create ICMP socket for %v: %v", epID(id), err) f.logger.Error2("forwarder: Failed to create ICMP socket for %v: %v", epID(id), err)
// This will make netstack reply on behalf of the original destination, that's ok for now // This will make netstack reply on behalf of the original destination, that's ok for now
return false return false
} }
defer func() { defer func() {
if err := conn.Close(); err != nil { if err := conn.Close(); err != nil {
f.logger.Debug("forwarder: Failed to close ICMP socket: %v", err) f.logger.Debug1("forwarder: Failed to close ICMP socket: %v", err)
} }
}() }()
@@ -52,11 +52,11 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
payload := fullPacket.AsSlice() payload := fullPacket.AsSlice()
if _, err = conn.WriteTo(payload, dst); err != nil { if _, err = conn.WriteTo(payload, dst); err != nil {
f.logger.Error("forwarder: Failed to write ICMP packet for %v: %v", epID(id), err) f.logger.Error2("forwarder: Failed to write ICMP packet for %v: %v", epID(id), err)
return true return true
} }
f.logger.Trace("forwarder: Forwarded ICMP packet %v type %v code %v", f.logger.Trace3("forwarder: Forwarded ICMP packet %v type %v code %v",
epID(id), icmpHdr.Type(), icmpHdr.Code()) epID(id), icmpHdr.Type(), icmpHdr.Code())
// For Echo Requests, send and handle response // For Echo Requests, send and handle response
@@ -72,7 +72,7 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) int { func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) int {
if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
f.logger.Error("forwarder: Failed to set read deadline for ICMP response: %v", err) f.logger.Error1("forwarder: Failed to set read deadline for ICMP response: %v", err)
return 0 return 0
} }
@@ -80,7 +80,7 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketCon
n, _, err := conn.ReadFrom(response) n, _, err := conn.ReadFrom(response)
if err != nil { if err != nil {
if !isTimeout(err) { if !isTimeout(err) {
f.logger.Error("forwarder: Failed to read ICMP response: %v", err) f.logger.Error1("forwarder: Failed to read ICMP response: %v", err)
} }
return 0 return 0
} }
@@ -101,12 +101,12 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketCon
fullPacket = append(fullPacket, response[:n]...) fullPacket = append(fullPacket, response[:n]...)
if err := f.InjectIncomingPacket(fullPacket); err != nil { if err := f.InjectIncomingPacket(fullPacket); err != nil {
f.logger.Error("forwarder: Failed to inject ICMP response: %v", err) f.logger.Error1("forwarder: Failed to inject ICMP response: %v", err)
return 0 return 0
} }
f.logger.Trace("forwarder: Forwarded ICMP echo reply for %v type %v code %v", f.logger.Trace3("forwarder: Forwarded ICMP echo reply for %v type %v code %v",
epID(id), icmpHdr.Type(), icmpHdr.Code()) epID(id), icmpHdr.Type(), icmpHdr.Code())
return len(fullPacket) return len(fullPacket)

View File

@@ -38,7 +38,7 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
outConn, err := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr) outConn, err := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr)
if err != nil { if err != nil {
r.Complete(true) r.Complete(true)
f.logger.Trace("forwarder: dial error for %v: %v", epID(id), err) f.logger.Trace2("forwarder: dial error for %v: %v", epID(id), err)
return return
} }
@@ -47,9 +47,9 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
ep, epErr := r.CreateEndpoint(&wq) ep, epErr := r.CreateEndpoint(&wq)
if epErr != nil { if epErr != nil {
f.logger.Error("forwarder: failed to create TCP endpoint: %v", epErr) f.logger.Error1("forwarder: failed to create TCP endpoint: %v", epErr)
if err := outConn.Close(); err != nil { if err := outConn.Close(); err != nil {
f.logger.Debug("forwarder: outConn close error: %v", err) f.logger.Debug1("forwarder: outConn close error: %v", err)
} }
r.Complete(true) r.Complete(true)
return return
@@ -61,7 +61,7 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
inConn := gonet.NewTCPConn(&wq, ep) inConn := gonet.NewTCPConn(&wq, ep)
success = true success = true
f.logger.Trace("forwarder: established TCP connection %v", epID(id)) f.logger.Trace1("forwarder: established TCP connection %v", epID(id))
go f.proxyTCP(id, inConn, outConn, ep, flowID) go f.proxyTCP(id, inConn, outConn, ep, flowID)
} }
@@ -75,10 +75,10 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn
<-ctx.Done() <-ctx.Done()
// Close connections and endpoint. // Close connections and endpoint.
if err := inConn.Close(); err != nil && !isClosedError(err) { if err := inConn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug("forwarder: inConn close error: %v", err) f.logger.Debug1("forwarder: inConn close error: %v", err)
} }
if err := outConn.Close(); err != nil && !isClosedError(err) { if err := outConn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug("forwarder: outConn close error: %v", err) f.logger.Debug1("forwarder: outConn close error: %v", err)
} }
ep.Close() ep.Close()
@@ -111,12 +111,12 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn
if errInToOut != nil { if errInToOut != nil {
if !isClosedError(errInToOut) { if !isClosedError(errInToOut) {
f.logger.Error("proxyTCP: copy error (in → out) for %s: %v", epID(id), errInToOut) f.logger.Error2("proxyTCP: copy error (in → out) for %s: %v", epID(id), errInToOut)
} }
} }
if errOutToIn != nil { if errOutToIn != nil {
if !isClosedError(errOutToIn) { if !isClosedError(errOutToIn) {
f.logger.Error("proxyTCP: copy error (out → in) for %s: %v", epID(id), errOutToIn) f.logger.Error2("proxyTCP: copy error (out → in) for %s: %v", epID(id), errOutToIn)
} }
} }
@@ -127,7 +127,7 @@ func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn
txPackets = tcpStats.SegmentsReceived.Value() txPackets = tcpStats.SegmentsReceived.Value()
} }
f.logger.Trace("forwarder: Removed TCP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesFromOutToIn, txPackets, bytesFromInToOut) f.logger.Trace5("forwarder: Removed TCP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesFromOutToIn, txPackets, bytesFromInToOut)
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesFromOutToIn), uint64(bytesFromInToOut), rxPackets, txPackets) f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesFromOutToIn), uint64(bytesFromInToOut), rxPackets, txPackets)
} }

View File

@@ -78,10 +78,10 @@ func (f *udpForwarder) Stop() {
for id, conn := range f.conns { for id, conn := range f.conns {
conn.cancel() conn.cancel()
if err := conn.conn.Close(); err != nil { if err := conn.conn.Close(); err != nil {
f.logger.Debug("forwarder: UDP conn close error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP conn close error for %v: %v", epID(id), err)
} }
if err := conn.outConn.Close(); err != nil { if err := conn.outConn.Close(); err != nil {
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err)
} }
conn.ep.Close() conn.ep.Close()
@@ -112,10 +112,10 @@ func (f *udpForwarder) cleanup() {
for _, idle := range idleConns { for _, idle := range idleConns {
idle.conn.cancel() idle.conn.cancel()
if err := idle.conn.conn.Close(); err != nil { if err := idle.conn.conn.Close(); err != nil {
f.logger.Debug("forwarder: UDP conn close error for %v: %v", epID(idle.id), err) f.logger.Debug2("forwarder: UDP conn close error for %v: %v", epID(idle.id), err)
} }
if err := idle.conn.outConn.Close(); err != nil { if err := idle.conn.outConn.Close(); err != nil {
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(idle.id), err) f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(idle.id), err)
} }
idle.conn.ep.Close() idle.conn.ep.Close()
@@ -124,7 +124,7 @@ func (f *udpForwarder) cleanup() {
delete(f.conns, idle.id) delete(f.conns, idle.id)
f.Unlock() f.Unlock()
f.logger.Trace("forwarder: cleaned up idle UDP connection %v", epID(idle.id)) f.logger.Trace1("forwarder: cleaned up idle UDP connection %v", epID(idle.id))
} }
} }
} }
@@ -143,7 +143,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
_, exists := f.udpForwarder.conns[id] _, exists := f.udpForwarder.conns[id]
f.udpForwarder.RUnlock() f.udpForwarder.RUnlock()
if exists { if exists {
f.logger.Trace("forwarder: existing UDP connection for %v", epID(id)) f.logger.Trace1("forwarder: existing UDP connection for %v", epID(id))
return return
} }
@@ -160,7 +160,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
dstAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort) dstAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort)
outConn, err := (&net.Dialer{}).DialContext(f.ctx, "udp", dstAddr) outConn, err := (&net.Dialer{}).DialContext(f.ctx, "udp", dstAddr)
if err != nil { if err != nil {
f.logger.Debug("forwarder: UDP dial error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP dial error for %v: %v", epID(id), err)
// TODO: Send ICMP error message // TODO: Send ICMP error message
return return
} }
@@ -169,9 +169,9 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
wq := waiter.Queue{} wq := waiter.Queue{}
ep, epErr := r.CreateEndpoint(&wq) ep, epErr := r.CreateEndpoint(&wq)
if epErr != nil { if epErr != nil {
f.logger.Debug("forwarder: failed to create UDP endpoint: %v", epErr) f.logger.Debug1("forwarder: failed to create UDP endpoint: %v", epErr)
if err := outConn.Close(); err != nil { if err := outConn.Close(); err != nil {
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err)
} }
return return
} }
@@ -194,10 +194,10 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
f.udpForwarder.Unlock() f.udpForwarder.Unlock()
pConn.cancel() pConn.cancel()
if err := inConn.Close(); err != nil { if err := inConn.Close(); err != nil {
f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP inConn close error for %v: %v", epID(id), err)
} }
if err := outConn.Close(); err != nil { if err := outConn.Close(); err != nil {
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err)
} }
return return
} }
@@ -205,7 +205,7 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
f.udpForwarder.Unlock() f.udpForwarder.Unlock()
success = true success = true
f.logger.Trace("forwarder: established UDP connection %v", epID(id)) f.logger.Trace1("forwarder: established UDP connection %v", epID(id))
go f.proxyUDP(connCtx, pConn, id, ep) go f.proxyUDP(connCtx, pConn, id, ep)
} }
@@ -220,10 +220,10 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack
pConn.cancel() pConn.cancel()
if err := pConn.conn.Close(); err != nil && !isClosedError(err) { if err := pConn.conn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP inConn close error for %v: %v", epID(id), err)
} }
if err := pConn.outConn.Close(); err != nil && !isClosedError(err) { if err := pConn.outConn.Close(); err != nil && !isClosedError(err) {
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err) f.logger.Debug2("forwarder: UDP outConn close error for %v: %v", epID(id), err)
} }
ep.Close() ep.Close()
@@ -250,10 +250,10 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack
wg.Wait() wg.Wait()
if outboundErr != nil && !isClosedError(outboundErr) { if outboundErr != nil && !isClosedError(outboundErr) {
f.logger.Error("proxyUDP: copy error (outbound→inbound) for %s: %v", epID(id), outboundErr) f.logger.Error2("proxyUDP: copy error (outbound→inbound) for %s: %v", epID(id), outboundErr)
} }
if inboundErr != nil && !isClosedError(inboundErr) { if inboundErr != nil && !isClosedError(inboundErr) {
f.logger.Error("proxyUDP: copy error (inbound→outbound) for %s: %v", epID(id), inboundErr) f.logger.Error2("proxyUDP: copy error (inbound→outbound) for %s: %v", epID(id), inboundErr)
} }
var rxPackets, txPackets uint64 var rxPackets, txPackets uint64
@@ -263,7 +263,7 @@ func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack
txPackets = udpStats.PacketsReceived.Value() txPackets = udpStats.PacketsReceived.Value()
} }
f.logger.Trace("forwarder: Removed UDP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, rxBytes, txPackets, txBytes) f.logger.Trace5("forwarder: Removed UDP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, rxBytes, txPackets, txBytes)
f.udpForwarder.Lock() f.udpForwarder.Lock()
delete(f.udpForwarder.conns, id) delete(f.udpForwarder.conns, id)

View File

@@ -44,7 +44,12 @@ var levelStrings = map[Level]string{
type logMessage struct { type logMessage struct {
level Level level Level
format string format string
args []any arg1 any
arg2 any
arg3 any
arg4 any
arg5 any
arg6 any
} }
// Logger is a high-performance, non-blocking logger // Logger is a high-performance, non-blocking logger
@@ -89,62 +94,198 @@ func (l *Logger) SetLevel(level Level) {
log.Debugf("Set uspfilter logger loglevel to %v", levelStrings[level]) log.Debugf("Set uspfilter logger loglevel to %v", levelStrings[level])
} }
func (l *Logger) log(level Level, format string, args ...any) {
select {
case l.msgChannel <- logMessage{level: level, format: format, args: args}:
default:
}
}
// Error logs a message at error level func (l *Logger) Error(format string) {
func (l *Logger) Error(format string, args ...any) {
if l.level.Load() >= uint32(LevelError) { if l.level.Load() >= uint32(LevelError) {
l.log(LevelError, format, args...) select {
case l.msgChannel <- logMessage{level: LevelError, format: format}:
default:
}
} }
} }
// Warn logs a message at warning level func (l *Logger) Warn(format string) {
func (l *Logger) Warn(format string, args ...any) {
if l.level.Load() >= uint32(LevelWarn) { if l.level.Load() >= uint32(LevelWarn) {
l.log(LevelWarn, format, args...) select {
case l.msgChannel <- logMessage{level: LevelWarn, format: format}:
default:
}
} }
} }
// Info logs a message at info level func (l *Logger) Info(format string) {
func (l *Logger) Info(format string, args ...any) {
if l.level.Load() >= uint32(LevelInfo) { if l.level.Load() >= uint32(LevelInfo) {
l.log(LevelInfo, format, args...) select {
case l.msgChannel <- logMessage{level: LevelInfo, format: format}:
default:
}
} }
} }
// Debug logs a message at debug level func (l *Logger) Debug(format string) {
func (l *Logger) Debug(format string, args ...any) {
if l.level.Load() >= uint32(LevelDebug) { if l.level.Load() >= uint32(LevelDebug) {
l.log(LevelDebug, format, args...) select {
case l.msgChannel <- logMessage{level: LevelDebug, format: format}:
default:
}
} }
} }
// Trace logs a message at trace level func (l *Logger) Trace(format string) {
func (l *Logger) Trace(format string, args ...any) {
if l.level.Load() >= uint32(LevelTrace) { if l.level.Load() >= uint32(LevelTrace) {
l.log(LevelTrace, format, args...) select {
case l.msgChannel <- logMessage{level: LevelTrace, format: format}:
default:
}
} }
} }
func (l *Logger) formatMessage(buf *[]byte, level Level, format string, args ...any) { func (l *Logger) Error1(format string, arg1 any) {
if l.level.Load() >= uint32(LevelError) {
select {
case l.msgChannel <- logMessage{level: LevelError, format: format, arg1: arg1}:
default:
}
}
}
func (l *Logger) Error2(format string, arg1, arg2 any) {
if l.level.Load() >= uint32(LevelError) {
select {
case l.msgChannel <- logMessage{level: LevelError, format: format, arg1: arg1, arg2: arg2}:
default:
}
}
}
func (l *Logger) Warn3(format string, arg1, arg2, arg3 any) {
if l.level.Load() >= uint32(LevelWarn) {
select {
case l.msgChannel <- logMessage{level: LevelWarn, format: format, arg1: arg1, arg2: arg2, arg3: arg3}:
default:
}
}
}
func (l *Logger) Debug1(format string, arg1 any) {
if l.level.Load() >= uint32(LevelDebug) {
select {
case l.msgChannel <- logMessage{level: LevelDebug, format: format, arg1: arg1}:
default:
}
}
}
func (l *Logger) Debug2(format string, arg1, arg2 any) {
if l.level.Load() >= uint32(LevelDebug) {
select {
case l.msgChannel <- logMessage{level: LevelDebug, format: format, arg1: arg1, arg2: arg2}:
default:
}
}
}
func (l *Logger) Trace1(format string, arg1 any) {
if l.level.Load() >= uint32(LevelTrace) {
select {
case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1}:
default:
}
}
}
func (l *Logger) Trace2(format string, arg1, arg2 any) {
if l.level.Load() >= uint32(LevelTrace) {
select {
case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2}:
default:
}
}
}
func (l *Logger) Trace3(format string, arg1, arg2, arg3 any) {
if l.level.Load() >= uint32(LevelTrace) {
select {
case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3}:
default:
}
}
}
func (l *Logger) Trace4(format string, arg1, arg2, arg3, arg4 any) {
if l.level.Load() >= uint32(LevelTrace) {
select {
case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3, arg4: arg4}:
default:
}
}
}
func (l *Logger) Trace5(format string, arg1, arg2, arg3, arg4, arg5 any) {
if l.level.Load() >= uint32(LevelTrace) {
select {
case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3, arg4: arg4, arg5: arg5}:
default:
}
}
}
func (l *Logger) Trace6(format string, arg1, arg2, arg3, arg4, arg5, arg6 any) {
if l.level.Load() >= uint32(LevelTrace) {
select {
case l.msgChannel <- logMessage{level: LevelTrace, format: format, arg1: arg1, arg2: arg2, arg3: arg3, arg4: arg4, arg5: arg5, arg6: arg6}:
default:
}
}
}
func (l *Logger) formatMessage(buf *[]byte, msg logMessage) {
*buf = (*buf)[:0] *buf = (*buf)[:0]
*buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00") *buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00")
*buf = append(*buf, ' ') *buf = append(*buf, ' ')
*buf = append(*buf, levelStrings[level]...) *buf = append(*buf, levelStrings[msg.level]...)
*buf = append(*buf, ' ') *buf = append(*buf, ' ')
var msg string // Count non-nil arguments for switch
if len(args) > 0 { argCount := 0
msg = fmt.Sprintf(format, args...) if msg.arg1 != nil {
} else { argCount++
msg = format if msg.arg2 != nil {
argCount++
if msg.arg3 != nil {
argCount++
if msg.arg4 != nil {
argCount++
if msg.arg5 != nil {
argCount++
if msg.arg6 != nil {
argCount++
}
}
}
}
}
} }
*buf = append(*buf, msg...)
var formatted string
switch argCount {
case 0:
formatted = msg.format
case 1:
formatted = fmt.Sprintf(msg.format, msg.arg1)
case 2:
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2)
case 3:
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3)
case 4:
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4)
case 5:
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5)
case 6:
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5, msg.arg6)
}
*buf = append(*buf, formatted...)
*buf = append(*buf, '\n') *buf = append(*buf, '\n')
if len(*buf) > maxMessageSize { if len(*buf) > maxMessageSize {
@@ -157,7 +298,7 @@ func (l *Logger) processMessage(msg logMessage, buffer *[]byte) {
bufp := l.bufPool.Get().(*[]byte) bufp := l.bufPool.Get().(*[]byte)
defer l.bufPool.Put(bufp) defer l.bufPool.Put(bufp)
l.formatMessage(bufp, msg.level, msg.format, msg.args...) l.formatMessage(bufp, msg)
if len(*buffer)+len(*bufp) > maxBatchSize { if len(*buffer)+len(*bufp) > maxBatchSize {
_, _ = l.output.Write(*buffer) _, _ = l.output.Write(*buffer)
@@ -249,4 +390,4 @@ func (l *Logger) Stop(ctx context.Context) error {
case <-done: case <-done:
return nil return nil
} }
} }

View File

@@ -19,22 +19,17 @@ func (d *discard) Write(p []byte) (n int, err error) {
func BenchmarkLogger(b *testing.B) { func BenchmarkLogger(b *testing.B) {
simpleMessage := "Connection established" simpleMessage := "Connection established"
conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d"
srcIP := "192.168.1.1" srcIP := "192.168.1.1"
srcPort := uint16(12345) srcPort := uint16(12345)
dstIP := "10.0.0.1" dstIP := "10.0.0.1"
dstPort := uint16(443) dstPort := uint16(443)
state := 4 // TCPStateEstablished state := 4 // TCPStateEstablished
complexMessage := "Packet inspection result: protocol=%s, direction=%s, flags=0x%x, sequence=%d, acknowledged=%d, payload_size=%d, fragmented=%v, connection_id=%s"
protocol := "TCP" protocol := "TCP"
direction := "outbound" direction := "outbound"
flags := uint16(0x18) // ACK + PSH flags := uint16(0x18) // ACK + PSH
sequence := uint32(123456789) sequence := uint32(123456789)
acknowledged := uint32(987654321) acknowledged := uint32(987654321)
payloadSize := 1460
fragmented := false
connID := "f7a12b3e-c456-7890-d123-456789abcdef"
b.Run("SimpleMessage", func(b *testing.B) { b.Run("SimpleMessage", func(b *testing.B) {
logger := createTestLogger() logger := createTestLogger()
@@ -52,7 +47,7 @@ func BenchmarkLogger(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) logger.Trace5("TCP connection %s:%d → %s:%d state %d", srcIP, srcPort, dstIP, dstPort, state)
} }
}) })
@@ -62,7 +57,7 @@ func BenchmarkLogger(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
logger.Trace(complexMessage, protocol, direction, flags, sequence, acknowledged, payloadSize, fragmented, connID) logger.Trace6("Complex trace: proto=%s dir=%s flags=%d seq=%d ack=%d size=%d", protocol, direction, flags, sequence, acknowledged, 1460)
} }
}) })
} }
@@ -72,7 +67,6 @@ func BenchmarkLoggerParallel(b *testing.B) {
logger := createTestLogger() logger := createTestLogger()
defer cleanupLogger(logger) defer cleanupLogger(logger)
conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d"
srcIP := "192.168.1.1" srcIP := "192.168.1.1"
srcPort := uint16(12345) srcPort := uint16(12345)
dstIP := "10.0.0.1" dstIP := "10.0.0.1"
@@ -82,7 +76,7 @@ func BenchmarkLoggerParallel(b *testing.B) {
b.ResetTimer() b.ResetTimer()
b.RunParallel(func(pb *testing.PB) { b.RunParallel(func(pb *testing.PB) {
for pb.Next() { for pb.Next() {
logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) logger.Trace5("TCP connection %s:%d → %s:%d state %d", srcIP, srcPort, dstIP, dstPort, state)
} }
}) })
} }
@@ -92,7 +86,6 @@ func BenchmarkLoggerBurst(b *testing.B) {
logger := createTestLogger() logger := createTestLogger()
defer cleanupLogger(logger) defer cleanupLogger(logger)
conntrackMessage := "TCP connection %s:%d -> %s:%d state changed to %d"
srcIP := "192.168.1.1" srcIP := "192.168.1.1"
srcPort := uint16(12345) srcPort := uint16(12345)
dstIP := "10.0.0.1" dstIP := "10.0.0.1"
@@ -102,7 +95,7 @@ func BenchmarkLoggerBurst(b *testing.B) {
b.ResetTimer() b.ResetTimer()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
for j := 0; j < 100; j++ { for j := 0; j < 100; j++ {
logger.Trace(conntrackMessage, srcIP, srcPort, dstIP, dstPort, state) logger.Trace5("TCP connection %s:%d → %s:%d state %d", srcIP, srcPort, dstIP, dstPort, state)
} }
} }
} }

View File

@@ -211,11 +211,11 @@ func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool {
} }
if err := m.rewritePacketDestination(packetData, d, translatedIP); err != nil { if err := m.rewritePacketDestination(packetData, d, translatedIP); err != nil {
m.logger.Error("Failed to rewrite packet destination: %v", err) m.logger.Error1("Failed to rewrite packet destination: %v", err)
return false return false
} }
m.logger.Trace("DNAT: %s -> %s", dstIP, translatedIP) m.logger.Trace2("DNAT: %s -> %s", dstIP, translatedIP)
return true return true
} }
@@ -237,11 +237,11 @@ func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool {
} }
if err := m.rewritePacketSource(packetData, d, originalIP); err != nil { if err := m.rewritePacketSource(packetData, d, originalIP); err != nil {
m.logger.Error("Failed to rewrite packet source: %v", err) m.logger.Error1("Failed to rewrite packet source: %v", err)
return false return false
} }
m.logger.Trace("Reverse DNAT: %s -> %s", srcIP, originalIP) m.logger.Trace2("Reverse DNAT: %s -> %s", srcIP, originalIP)
return true return true
} }

View File

@@ -34,14 +34,14 @@ func NewActivityRecorder() *ActivityRecorder {
} }
// GetLastActivities returns a snapshot of peer last activity // GetLastActivities returns a snapshot of peer last activity
func (r *ActivityRecorder) GetLastActivities() map[string]time.Time { func (r *ActivityRecorder) GetLastActivities() map[string]monotime.Time {
r.mu.RLock() r.mu.RLock()
defer r.mu.RUnlock() defer r.mu.RUnlock()
activities := make(map[string]time.Time, len(r.peers)) activities := make(map[string]monotime.Time, len(r.peers))
for key, record := range r.peers { for key, record := range r.peers {
unixNano := record.LastActivity.Load() monoTime := record.LastActivity.Load()
activities[key] = time.Unix(0, unixNano) activities[key] = monotime.Time(monoTime)
} }
return activities return activities
} }
@@ -51,18 +51,20 @@ func (r *ActivityRecorder) UpsertAddress(publicKey string, address netip.AddrPor
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
if pr, exists := r.peers[publicKey]; exists { var record *PeerRecord
delete(r.addrToPeer, pr.Address) record, exists := r.peers[publicKey]
pr.Address = address if exists {
delete(r.addrToPeer, record.Address)
record.Address = address
} else { } else {
record := &PeerRecord{ record = &PeerRecord{
Address: address, Address: address,
} }
record.LastActivity.Store(monotime.Now()) record.LastActivity.Store(int64(monotime.Now()))
r.peers[publicKey] = record r.peers[publicKey] = record
} }
r.addrToPeer[address] = r.peers[publicKey] r.addrToPeer[address] = record
} }
func (r *ActivityRecorder) Remove(publicKey string) { func (r *ActivityRecorder) Remove(publicKey string) {
@@ -84,7 +86,7 @@ func (r *ActivityRecorder) record(address netip.AddrPort) {
return return
} }
now := monotime.Now() now := int64(monotime.Now())
last := record.LastActivity.Load() last := record.LastActivity.Load()
if now-last < saveFrequency { if now-last < saveFrequency {
return return

View File

@@ -4,6 +4,8 @@ import (
"net/netip" "net/netip"
"testing" "testing"
"time" "time"
"github.com/netbirdio/netbird/monotime"
) )
func TestActivityRecorder_GetLastActivities(t *testing.T) { func TestActivityRecorder_GetLastActivities(t *testing.T) {
@@ -17,11 +19,7 @@ func TestActivityRecorder_GetLastActivities(t *testing.T) {
t.Fatalf("Expected activity for peer %s, but got none", peer) t.Fatalf("Expected activity for peer %s, but got none", peer)
} }
if p.IsZero() { if monotime.Since(p) > 5*time.Second {
t.Fatalf("Expected activity for peer %s, but got zero", peer)
}
if p.Before(time.Now().Add(-2 * time.Minute)) {
t.Fatalf("Expected activity for peer %s to be recent, but got %v", peer, p) t.Fatalf("Expected activity for peer %s to be recent, but got %v", peer, p)
} }
} }

View File

@@ -0,0 +1,15 @@
package bind
import (
wireguard "golang.zx2c4.com/wireguard/conn"
nbnet "github.com/netbirdio/netbird/util/net"
)
// TODO: This is most likely obsolete since the control fns should be called by the wrapped udpconn (ice_bind.go)
func init() {
listener := nbnet.NewListener()
if listener.ListenConfig.Control != nil {
*wireguard.ControlFns = append(*wireguard.ControlFns, listener.ListenConfig.Control)
}
}

View File

@@ -1,12 +0,0 @@
package bind
import (
wireguard "golang.zx2c4.com/wireguard/conn"
nbnet "github.com/netbirdio/netbird/util/net"
)
func init() {
// ControlFns is not thread safe and should only be modified during init.
*wireguard.ControlFns = append(*wireguard.ControlFns, nbnet.ControlProtectSocket)
}

View File

@@ -16,6 +16,7 @@ import (
wgConn "golang.zx2c4.com/wireguard/conn" wgConn "golang.zx2c4.com/wireguard/conn"
"github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/iface/wgaddr"
nbnet "github.com/netbirdio/netbird/util/net"
) )
type RecvMessage struct { type RecvMessage struct {
@@ -153,7 +154,7 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r
s.udpMux = NewUniversalUDPMuxDefault( s.udpMux = NewUniversalUDPMuxDefault(
UniversalUDPMuxParams{ UniversalUDPMuxParams{
UDPConn: conn, UDPConn: nbnet.WrapPacketConn(conn),
Net: s.transportNet, Net: s.transportNet,
FilterFn: s.filterFn, FilterFn: s.filterFn,
WGAddress: s.address, WGAddress: s.address,

View File

@@ -296,14 +296,20 @@ func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) {
return return
} }
m.addressMapMu.Lock() var allAddresses []string
defer m.addressMapMu.Unlock()
for _, c := range removedConns { for _, c := range removedConns {
addresses := c.getAddresses() addresses := c.getAddresses()
for _, addr := range addresses { allAddresses = append(allAddresses, addresses...)
delete(m.addressMap, addr) }
}
m.addressMapMu.Lock()
for _, addr := range allAddresses {
delete(m.addressMap, addr)
}
m.addressMapMu.Unlock()
for _, addr := range allAddresses {
m.notifyAddressRemoval(addr)
} }
} }
@@ -351,14 +357,13 @@ func (m *UDPMuxDefault) registerConnForAddress(conn *udpMuxedConn, addr string)
} }
m.addressMapMu.Lock() m.addressMapMu.Lock()
defer m.addressMapMu.Unlock()
existing, ok := m.addressMap[addr] existing, ok := m.addressMap[addr]
if !ok { if !ok {
existing = []*udpMuxedConn{} existing = []*udpMuxedConn{}
} }
existing = append(existing, conn) existing = append(existing, conn)
m.addressMap[addr] = existing m.addressMap[addr] = existing
m.addressMapMu.Unlock()
log.Debugf("ICE: registered %s for %s", addr, conn.params.Key) log.Debugf("ICE: registered %s for %s", addr, conn.params.Key)
} }
@@ -386,12 +391,12 @@ func (m *UDPMuxDefault) HandleSTUNMessage(msg *stun.Message, addr net.Addr) erro
// If you are using the same socket for the Host and SRFLX candidates, it might be that there are more than one // If you are using the same socket for the Host and SRFLX candidates, it might be that there are more than one
// muxed connection - one for the SRFLX candidate and the other one for the HOST one. // muxed connection - one for the SRFLX candidate and the other one for the HOST one.
// We will then forward STUN packets to each of these connections. // We will then forward STUN packets to each of these connections.
m.addressMapMu.Lock() m.addressMapMu.RLock()
var destinationConnList []*udpMuxedConn var destinationConnList []*udpMuxedConn
if storedConns, ok := m.addressMap[addr.String()]; ok { if storedConns, ok := m.addressMap[addr.String()]; ok {
destinationConnList = append(destinationConnList, storedConns...) destinationConnList = append(destinationConnList, storedConns...)
} }
m.addressMapMu.Unlock() m.addressMapMu.RUnlock()
var isIPv6 bool var isIPv6 bool
if udpAddr, _ := addr.(*net.UDPAddr); udpAddr != nil && udpAddr.IP.To4() == nil { if udpAddr, _ := addr.(*net.UDPAddr); udpAddr != nil && udpAddr.IP.To4() == nil {

View File

@@ -0,0 +1,22 @@
//go:build !ios
package bind
import (
nbnet "github.com/netbirdio/netbird/util/net"
)
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)
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)
}
}
}

View File

@@ -0,0 +1,7 @@
//go:build ios
package bind
func (m *UDPMuxDefault) notifyAddressRemoval(addr string) {
// iOS doesn't support nbnet hooks, so this is a no-op
}

View File

@@ -62,7 +62,7 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef
// wrap UDP connection, process server reflexive messages // wrap UDP connection, process server reflexive messages
// before they are passed to the UDPMux connection handler (connWorker) // before they are passed to the UDPMux connection handler (connWorker)
m.params.UDPConn = &udpConn{ m.params.UDPConn = &UDPConn{
PacketConn: params.UDPConn, PacketConn: params.UDPConn,
mux: m, mux: m,
logger: params.Logger, logger: params.Logger,
@@ -70,7 +70,6 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef
address: params.WGAddress, address: params.WGAddress,
} }
// embed UDPMux
udpMuxParams := UDPMuxParams{ udpMuxParams := UDPMuxParams{
Logger: params.Logger, Logger: params.Logger,
UDPConn: m.params.UDPConn, UDPConn: m.params.UDPConn,
@@ -114,8 +113,8 @@ func (m *UniversalUDPMuxDefault) ReadFromConn(ctx context.Context) {
} }
} }
// udpConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets // UDPConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets
type udpConn struct { type UDPConn struct {
net.PacketConn net.PacketConn
mux *UniversalUDPMuxDefault mux *UniversalUDPMuxDefault
logger logging.LeveledLogger logger logging.LeveledLogger
@@ -125,7 +124,12 @@ type udpConn struct {
address wgaddr.Address address wgaddr.Address
} }
func (u *udpConn) WriteTo(b []byte, addr net.Addr) (int, error) { // GetPacketConn returns the underlying PacketConn
func (u *UDPConn) GetPacketConn() net.PacketConn {
return u.PacketConn
}
func (u *UDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
if u.filterFn == nil { if u.filterFn == nil {
return u.PacketConn.WriteTo(b, addr) return u.PacketConn.WriteTo(b, addr)
} }
@@ -137,21 +141,21 @@ func (u *udpConn) WriteTo(b []byte, addr net.Addr) (int, error) {
return u.handleUncachedAddress(b, addr) return u.handleUncachedAddress(b, addr)
} }
func (u *udpConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (int, error) { func (u *UDPConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (int, error) {
if isRouted { if isRouted {
return 0, fmt.Errorf("address %s is part of a routed network, refusing to write", addr) return 0, fmt.Errorf("address %s is part of a routed network, refusing to write", addr)
} }
return u.PacketConn.WriteTo(b, addr) return u.PacketConn.WriteTo(b, addr)
} }
func (u *udpConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) { func (u *UDPConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) {
if err := u.performFilterCheck(addr); err != nil { if err := u.performFilterCheck(addr); err != nil {
return 0, err return 0, err
} }
return u.PacketConn.WriteTo(b, addr) return u.PacketConn.WriteTo(b, addr)
} }
func (u *udpConn) performFilterCheck(addr net.Addr) error { func (u *UDPConn) performFilterCheck(addr net.Addr) error {
host, err := getHostFromAddr(addr) host, err := getHostFromAddr(addr)
if err != nil { if err != nil {
log.Errorf("Failed to get host from address %s: %v", addr, err) log.Errorf("Failed to get host from address %s: %v", addr, err)

View File

@@ -11,6 +11,8 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/monotime"
) )
var zeroKey wgtypes.Key var zeroKey wgtypes.Key
@@ -277,6 +279,6 @@ func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) {
return stats, nil return stats, nil
} }
func (c *KernelConfigurer) LastActivities() map[string]time.Time { func (c *KernelConfigurer) LastActivities() map[string]monotime.Time {
return nil return nil
} }

View File

@@ -17,6 +17,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/bind"
"github.com/netbirdio/netbird/monotime"
nbnet "github.com/netbirdio/netbird/util/net" nbnet "github.com/netbirdio/netbird/util/net"
) )
@@ -223,7 +224,7 @@ func (c *WGUSPConfigurer) FullStats() (*Stats, error) {
return parseStatus(c.deviceName, ipcStr) return parseStatus(c.deviceName, ipcStr)
} }
func (c *WGUSPConfigurer) LastActivities() map[string]time.Time { func (c *WGUSPConfigurer) LastActivities() map[string]monotime.Time {
return c.activityRecorder.GetLastActivities() return c.activityRecorder.GetLastActivities()
} }
@@ -529,7 +530,7 @@ func parseStatus(deviceName, ipcStr string) (*Stats, error) {
if currentPeer == nil { if currentPeer == nil {
continue continue
} }
if val != "" { if val != "" && val != "0000000000000000000000000000000000000000000000000000000000000000" {
currentPeer.PresharedKey = true currentPeer.PresharedKey = true
} }
} }

View File

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

View File

@@ -8,6 +8,7 @@ import (
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/iface/configurer" "github.com/netbirdio/netbird/client/iface/configurer"
"github.com/netbirdio/netbird/monotime"
) )
type WGConfigurer interface { type WGConfigurer interface {
@@ -19,5 +20,5 @@ type WGConfigurer interface {
Close() Close()
GetStats() (map[string]configurer.WGStats, error) GetStats() (map[string]configurer.WGStats, error)
FullStats() (*configurer.Stats, error) FullStats() (*configurer.Stats, error)
LastActivities() map[string]time.Time LastActivities() map[string]monotime.Time
} }

View File

@@ -21,6 +21,7 @@ import (
"github.com/netbirdio/netbird/client/iface/device" "github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/wgaddr" "github.com/netbirdio/netbird/client/iface/wgaddr"
"github.com/netbirdio/netbird/client/iface/wgproxy" "github.com/netbirdio/netbird/client/iface/wgproxy"
"github.com/netbirdio/netbird/monotime"
) )
const ( const (
@@ -29,6 +30,11 @@ const (
WgInterfaceDefault = configurer.WgInterfaceDefault WgInterfaceDefault = configurer.WgInterfaceDefault
) )
var (
// ErrIfaceNotFound is returned when the WireGuard interface is not found
ErrIfaceNotFound = fmt.Errorf("wireguard interface not found")
)
type wgProxyFactory interface { type wgProxyFactory interface {
GetProxy() wgproxy.Proxy GetProxy() wgproxy.Proxy
Free() error Free() error
@@ -117,6 +123,9 @@ func (w *WGIface) UpdateAddr(newAddr string) error {
func (w *WGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error { func (w *WGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.configurer == nil {
return ErrIfaceNotFound
}
log.Debugf("updating interface %s peer %s, endpoint %s, allowedIPs %v", w.tun.DeviceName(), peerKey, endpoint, allowedIps) log.Debugf("updating interface %s peer %s, endpoint %s, allowedIPs %v", w.tun.DeviceName(), peerKey, endpoint, allowedIps)
return w.configurer.UpdatePeer(peerKey, allowedIps, keepAlive, endpoint, preSharedKey) return w.configurer.UpdatePeer(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
@@ -126,6 +135,9 @@ func (w *WGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAliv
func (w *WGIface) RemovePeer(peerKey string) error { func (w *WGIface) RemovePeer(peerKey string) error {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.configurer == nil {
return ErrIfaceNotFound
}
log.Debugf("Removing peer %s from interface %s ", peerKey, w.tun.DeviceName()) log.Debugf("Removing peer %s from interface %s ", peerKey, w.tun.DeviceName())
return w.configurer.RemovePeer(peerKey) return w.configurer.RemovePeer(peerKey)
@@ -135,6 +147,9 @@ func (w *WGIface) RemovePeer(peerKey string) error {
func (w *WGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error { func (w *WGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.configurer == nil {
return ErrIfaceNotFound
}
log.Debugf("Adding allowed IP to interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) log.Debugf("Adding allowed IP to interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP)
return w.configurer.AddAllowedIP(peerKey, allowedIP) return w.configurer.AddAllowedIP(peerKey, allowedIP)
@@ -144,6 +159,9 @@ func (w *WGIface) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error {
func (w *WGIface) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error { func (w *WGIface) RemoveAllowedIP(peerKey string, allowedIP netip.Prefix) error {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.configurer == nil {
return ErrIfaceNotFound
}
log.Debugf("Removing allowed IP from interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP) log.Debugf("Removing allowed IP from interface %s and peer %s: allowed IP %s ", w.tun.DeviceName(), peerKey, allowedIP)
return w.configurer.RemoveAllowedIP(peerKey, allowedIP) return w.configurer.RemoveAllowedIP(peerKey, allowedIP)
@@ -214,18 +232,29 @@ func (w *WGIface) GetWGDevice() *wgdevice.Device {
// GetStats returns the last handshake time, rx and tx bytes // GetStats returns the last handshake time, rx and tx bytes
func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) { func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) {
if w.configurer == nil {
return nil, ErrIfaceNotFound
}
return w.configurer.GetStats() return w.configurer.GetStats()
} }
func (w *WGIface) LastActivities() map[string]time.Time { func (w *WGIface) LastActivities() map[string]monotime.Time {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.configurer == nil {
return nil
}
return w.configurer.LastActivities() return w.configurer.LastActivities()
} }
func (w *WGIface) FullStats() (*configurer.Stats, error) { func (w *WGIface) FullStats() (*configurer.Stats, error) {
if w.configurer == nil {
return nil, ErrIfaceNotFound
}
return w.configurer.FullStats() return w.configurer.FullStats()
} }

View File

@@ -41,9 +41,12 @@ func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) {
} }
t.tundev = nsTunDev t.tundev = nsTunDev
skipProxy, err := strconv.ParseBool(os.Getenv(EnvSkipProxy)) var skipProxy bool
if err != nil { if val := os.Getenv(EnvSkipProxy); val != "" {
log.Errorf("failed to parse %s: %s", EnvSkipProxy, err) skipProxy, err = strconv.ParseBool(val)
if err != nil {
log.Errorf("failed to parse %s: %s", EnvSkipProxy, err)
}
} }
if skipProxy { if skipProxy {
return nsTunDev, tunNet, nil return nsTunDev, tunNet, nil

View File

@@ -12,6 +12,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/iface/bind" "github.com/netbirdio/netbird/client/iface/bind"
"github.com/netbirdio/netbird/client/iface/wgproxy/listener"
) )
type ProxyBind struct { type ProxyBind struct {
@@ -28,6 +29,17 @@ type ProxyBind struct {
pausedMu sync.Mutex pausedMu sync.Mutex
paused bool paused bool
isStarted bool isStarted bool
closeListener *listener.CloseListener
}
func NewProxyBind(bind *bind.ICEBind) *ProxyBind {
p := &ProxyBind{
Bind: bind,
closeListener: listener.NewCloseListener(),
}
return p
} }
// AddTurnConn adds a new connection to the bind. // AddTurnConn adds a new connection to the bind.
@@ -54,6 +66,10 @@ func (p *ProxyBind) EndpointAddr() *net.UDPAddr {
} }
} }
func (p *ProxyBind) SetDisconnectListener(disconnected func()) {
p.closeListener.SetCloseListener(disconnected)
}
func (p *ProxyBind) Work() { func (p *ProxyBind) Work() {
if p.remoteConn == nil { if p.remoteConn == nil {
return return
@@ -96,6 +112,9 @@ func (p *ProxyBind) close() error {
if p.closed { if p.closed {
return nil return nil
} }
p.closeListener.SetCloseListener(nil)
p.closed = true p.closed = true
p.cancel() p.cancel()
@@ -122,6 +141,7 @@ func (p *ProxyBind) proxyToLocal(ctx context.Context) {
if ctx.Err() != nil { if ctx.Err() != nil {
return return
} }
p.closeListener.Notify()
log.Errorf("failed to read from remote conn: %s, %s", p.remoteConn.RemoteAddr(), err) log.Errorf("failed to read from remote conn: %s, %s", p.remoteConn.RemoteAddr(), err)
return return
} }

View File

@@ -11,6 +11,8 @@ import (
"sync" "sync"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/iface/wgproxy/listener"
) )
// ProxyWrapper help to keep the remoteConn instance for net.Conn.Close function call // ProxyWrapper help to keep the remoteConn instance for net.Conn.Close function call
@@ -26,6 +28,15 @@ type ProxyWrapper struct {
pausedMu sync.Mutex pausedMu sync.Mutex
paused bool paused bool
isStarted bool isStarted bool
closeListener *listener.CloseListener
}
func NewProxyWrapper(WgeBPFProxy *WGEBPFProxy) *ProxyWrapper {
return &ProxyWrapper{
WgeBPFProxy: WgeBPFProxy,
closeListener: listener.NewCloseListener(),
}
} }
func (p *ProxyWrapper) AddTurnConn(ctx context.Context, endpoint *net.UDPAddr, remoteConn net.Conn) error { func (p *ProxyWrapper) AddTurnConn(ctx context.Context, endpoint *net.UDPAddr, remoteConn net.Conn) error {
@@ -43,6 +54,10 @@ func (p *ProxyWrapper) EndpointAddr() *net.UDPAddr {
return p.wgEndpointAddr return p.wgEndpointAddr
} }
func (p *ProxyWrapper) SetDisconnectListener(disconnected func()) {
p.closeListener.SetCloseListener(disconnected)
}
func (p *ProxyWrapper) Work() { func (p *ProxyWrapper) Work() {
if p.remoteConn == nil { if p.remoteConn == nil {
return return
@@ -77,6 +92,8 @@ func (e *ProxyWrapper) CloseConn() error {
e.cancel() e.cancel()
e.closeListener.SetCloseListener(nil)
if err := e.remoteConn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { if err := e.remoteConn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
return fmt.Errorf("failed to close remote conn: %w", err) return fmt.Errorf("failed to close remote conn: %w", err)
} }
@@ -117,6 +134,7 @@ func (p *ProxyWrapper) readFromRemote(ctx context.Context, buf []byte) (int, err
if ctx.Err() != nil { if ctx.Err() != nil {
return 0, ctx.Err() return 0, ctx.Err()
} }
p.closeListener.Notify()
if !errors.Is(err, io.EOF) { if !errors.Is(err, io.EOF) {
log.Errorf("failed to read from turn conn (endpoint: :%d): %s", p.wgEndpointAddr.Port, err) log.Errorf("failed to read from turn conn (endpoint: :%d): %s", p.wgEndpointAddr.Port, err)
} }

View File

@@ -36,9 +36,8 @@ func (w *KernelFactory) GetProxy() Proxy {
return udpProxy.NewWGUDPProxy(w.wgPort) return udpProxy.NewWGUDPProxy(w.wgPort)
} }
return &ebpf.ProxyWrapper{ return ebpf.NewProxyWrapper(w.ebpfProxy)
WgeBPFProxy: w.ebpfProxy,
}
} }
func (w *KernelFactory) Free() error { func (w *KernelFactory) Free() error {

View File

@@ -20,9 +20,7 @@ func NewUSPFactory(iceBind *bind.ICEBind) *USPFactory {
} }
func (w *USPFactory) GetProxy() Proxy { func (w *USPFactory) GetProxy() Proxy {
return &proxyBind.ProxyBind{ return proxyBind.NewProxyBind(w.bind)
Bind: w.bind,
}
} }
func (w *USPFactory) Free() error { func (w *USPFactory) Free() error {

View File

@@ -0,0 +1,32 @@
package listener
import "sync"
type CloseListener struct {
listener func()
mu sync.Mutex
}
func NewCloseListener() *CloseListener {
return &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
}
listener := c.listener
c.mu.Unlock()
listener()
}

View File

@@ -12,4 +12,5 @@ type Proxy interface {
Work() // Work start or resume the proxy Work() // Work start or resume the proxy
Pause() // Pause to forward the packages from remote connection to WireGuard. The opposite way still works. Pause() // Pause to forward the packages from remote connection to WireGuard. The opposite way still works.
CloseConn() error CloseConn() error
SetDisconnectListener(disconnected func())
} }

View File

@@ -17,7 +17,7 @@ import (
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
_ = util.InitLog("trace", "console") _ = util.InitLog("trace", util.LogConsole)
code := m.Run() code := m.Run()
os.Exit(code) os.Exit(code)
} }
@@ -98,9 +98,7 @@ func TestProxyCloseByRemoteConn(t *testing.T) {
t.Errorf("failed to free ebpf proxy: %s", err) t.Errorf("failed to free ebpf proxy: %s", err)
} }
}() }()
proxyWrapper := &ebpf.ProxyWrapper{ proxyWrapper := ebpf.NewProxyWrapper(ebpfProxy)
WgeBPFProxy: ebpfProxy,
}
tests = append(tests, struct { tests = append(tests, struct {
name string name string

View File

@@ -12,6 +12,7 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
cerrors "github.com/netbirdio/netbird/client/errors" cerrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/iface/wgproxy/listener"
) )
// WGUDPProxy proxies // WGUDPProxy proxies
@@ -28,6 +29,8 @@ type WGUDPProxy struct {
pausedMu sync.Mutex pausedMu sync.Mutex
paused bool paused bool
isStarted bool isStarted bool
closeListener *listener.CloseListener
} }
// NewWGUDPProxy instantiate a UDP based WireGuard proxy. This is not a thread safe implementation // NewWGUDPProxy instantiate a UDP based WireGuard proxy. This is not a thread safe implementation
@@ -35,6 +38,7 @@ func NewWGUDPProxy(wgPort int) *WGUDPProxy {
log.Debugf("Initializing new user space proxy with port %d", wgPort) log.Debugf("Initializing new user space proxy with port %d", wgPort)
p := &WGUDPProxy{ p := &WGUDPProxy{
localWGListenPort: wgPort, localWGListenPort: wgPort,
closeListener: listener.NewCloseListener(),
} }
return p return p
} }
@@ -67,6 +71,10 @@ func (p *WGUDPProxy) EndpointAddr() *net.UDPAddr {
return endpointUdpAddr return endpointUdpAddr
} }
func (p *WGUDPProxy) SetDisconnectListener(disconnected func()) {
p.closeListener.SetCloseListener(disconnected)
}
// Work starts the proxy or resumes it if it was paused // Work starts the proxy or resumes it if it was paused
func (p *WGUDPProxy) Work() { func (p *WGUDPProxy) Work() {
if p.remoteConn == nil { if p.remoteConn == nil {
@@ -111,6 +119,8 @@ func (p *WGUDPProxy) close() error {
if p.closed { if p.closed {
return nil return nil
} }
p.closeListener.SetCloseListener(nil)
p.closed = true p.closed = true
p.cancel() p.cancel()
@@ -141,6 +151,7 @@ func (p *WGUDPProxy) proxyToRemote(ctx context.Context) {
if ctx.Err() != nil { if ctx.Err() != nil {
return return
} }
p.closeListener.Notify()
log.Debugf("failed to read from wg interface conn: %s", err) log.Debugf("failed to read from wg interface conn: %s", err)
return return
} }
@@ -172,6 +183,11 @@ func (p *WGUDPProxy) proxyToLocal(ctx context.Context) {
for { for {
n, err := p.remoteConnRead(ctx, buf) n, err := p.remoteConnRead(ctx, buf)
if err != nil { if err != nil {
if ctx.Err() != nil {
return
}
p.closeListener.Notify()
return return
} }

View File

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

View File

@@ -6,6 +6,7 @@ import (
"crypto/subtle" "crypto/subtle"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
@@ -230,9 +231,46 @@ func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo,
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err) 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 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 { func createCodeChallenge(codeVerifier string) string {
sha2 := sha256.Sum256([]byte(codeVerifier)) sha2 := sha256.Sum256([]byte(codeVerifier))
return base64.RawURLEncoding.EncodeToString(sha2[:]) return base64.RawURLEncoding.EncodeToString(sha2[:])

View File

@@ -226,7 +226,6 @@ func (e *ConnMgr) ActivatePeer(ctx context.Context, conn *peer.Conn) {
} }
if found := e.lazyConnMgr.ActivatePeer(conn.GetKey()); found { if found := e.lazyConnMgr.ActivatePeer(conn.GetKey()); found {
conn.Log.Infof("activated peer from inactive state")
if err := conn.Open(ctx); err != nil { if err := conn.Open(ctx); err != nil {
conn.Log.Errorf("failed to open connection: %v", err) conn.Log.Errorf("failed to open connection: %v", err)
} }

View File

@@ -21,6 +21,7 @@ import (
"github.com/netbirdio/netbird/client/internal/dns" "github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer" "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/internal/stdnet"
cProto "github.com/netbirdio/netbird/client/proto" cProto "github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/ssh" "github.com/netbirdio/netbird/client/ssh"
@@ -37,7 +38,7 @@ import (
type ConnectClient struct { type ConnectClient struct {
ctx context.Context ctx context.Context
config *Config config *profilemanager.Config
statusRecorder *peer.Status statusRecorder *peer.Status
engine *Engine engine *Engine
engineMutex sync.Mutex engineMutex sync.Mutex
@@ -47,7 +48,7 @@ type ConnectClient struct {
func NewConnectClient( func NewConnectClient(
ctx context.Context, ctx context.Context,
config *Config, config *profilemanager.Config,
statusRecorder *peer.Status, statusRecorder *peer.Status,
) *ConnectClient { ) *ConnectClient {
@@ -414,7 +415,7 @@ func (c *ConnectClient) SetNetworkMapPersistence(enabled bool) {
} }
// createEngineConfig converts configuration received from Management Service to EngineConfig // createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) { func createEngineConfig(key wgtypes.Key, config *profilemanager.Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
nm := false nm := false
if config.NetworkMonitor != nil { if config.NetworkMonitor != nil {
nm = *config.NetworkMonitor nm = *config.NetworkMonitor
@@ -484,7 +485,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) // 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 *Config) (*mgmProto.LoginResponse, error) { func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *profilemanager.Config) (*mgmProto.LoginResponse, error) {
serverPublicKey, err := client.GetServerPublicKey() serverPublicKey, err := client.GetServerPublicKey()
if err != nil { if err != nil {

View File

@@ -16,6 +16,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
"slices"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -24,10 +25,10 @@ import (
"google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/encoding/protojson"
"github.com/netbirdio/netbird/client/anonymize" "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/peer"
"github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/profilemanager"
mgmProto "github.com/netbirdio/netbird/management/proto" mgmProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/util"
) )
const readmeContent = `Netbird debug bundle const readmeContent = `Netbird debug bundle
@@ -38,10 +39,12 @@ status.txt: Anonymized status information of the NetBird client.
client.log: Most recent, anonymized client log file of the NetBird client. client.log: Most recent, anonymized client log file of the NetBird client.
netbird.err: Most recent, anonymized stderr log file of the NetBird client. netbird.err: Most recent, anonymized stderr log file of the NetBird client.
netbird.out: Most recent, anonymized stdout log file of the NetBird client. netbird.out: Most recent, anonymized stdout log file of the NetBird client.
routes.txt: Anonymized system routes, if --system-info flag was provided. routes.txt: Detailed system routing table in tabular format including destination, gateway, interface, metrics, and protocol information, if --system-info flag was provided.
interfaces.txt: Anonymized network interface information, 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. 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. 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. config.txt: Anonymized configuration information of the NetBird client.
network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules. network_map.json: Anonymized network map containing peer configurations, routes, DNS settings, and firewall rules.
state.json: Anonymized client state dump containing netbird states. state.json: Anonymized client state dump containing netbird states.
@@ -105,7 +108,29 @@ go tool pprof -http=:8088 heap.prof
This will open a web browser tab with the profiling information. This will open a web browser tab with the profiling information.
Routes Routes
For anonymized routes, the IP addresses are replaced as described above. The prefix length remains unchanged. Note that for prefixes, the anonymized IP might not be a network address, but the prefix length is still correct. 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.
Network Interfaces Network Interfaces
The interfaces.txt file contains information about network interfaces, including: The interfaces.txt file contains information about network interfaces, including:
@@ -143,6 +168,22 @@ nftables.txt:
- Shows packet and byte counters for each rule - Shows packet and byte counters for each rule
- All IP addresses are anonymized - All IP addresses are anonymized
- Chain names, table names, and other non-sensitive information remain unchanged - 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 ( const (
@@ -158,15 +199,15 @@ type BundleGenerator struct {
anonymizer *anonymize.Anonymizer anonymizer *anonymize.Anonymizer
// deps // deps
internalConfig *internal.Config internalConfig *profilemanager.Config
statusRecorder *peer.Status statusRecorder *peer.Status
networkMap *mgmProto.NetworkMap networkMap *mgmProto.NetworkMap
logFile string logFile string
// config
anonymize bool anonymize bool
clientStatus string clientStatus string
includeSystemInfo bool includeSystemInfo bool
logFileCount uint32
archive *zip.Writer archive *zip.Writer
} }
@@ -175,16 +216,23 @@ type BundleConfig struct {
Anonymize bool Anonymize bool
ClientStatus string ClientStatus string
IncludeSystemInfo bool IncludeSystemInfo bool
LogFileCount uint32
} }
type GeneratorDependencies struct { type GeneratorDependencies struct {
InternalConfig *internal.Config InternalConfig *profilemanager.Config
StatusRecorder *peer.Status StatusRecorder *peer.Status
NetworkMap *mgmProto.NetworkMap NetworkMap *mgmProto.NetworkMap
LogFile string LogFile string
} }
func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator { func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGenerator {
// Default to 1 log file for backward compatibility when 0 is provided
logFileCount := cfg.LogFileCount
if logFileCount == 0 {
logFileCount = 1
}
return &BundleGenerator{ return &BundleGenerator{
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()), anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
@@ -196,6 +244,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
anonymize: cfg.Anonymize, anonymize: cfg.Anonymize,
clientStatus: cfg.ClientStatus, clientStatus: cfg.ClientStatus,
includeSystemInfo: cfg.IncludeSystemInfo, includeSystemInfo: cfg.IncludeSystemInfo,
logFileCount: logFileCount,
} }
} }
@@ -247,7 +296,11 @@ func (g *BundleGenerator) createArchive() error {
} }
if err := g.addConfig(); err != nil { if err := g.addConfig(); err != nil {
log.Errorf("Failed to add config to debug bundle: %v", err) 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)
} }
if g.includeSystemInfo { if g.includeSystemInfo {
@@ -255,7 +308,7 @@ func (g *BundleGenerator) createArchive() error {
} }
if err := g.addProf(); err != nil { 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 { if err := g.addNetworkMap(); err != nil {
@@ -263,26 +316,26 @@ func (g *BundleGenerator) createArchive() error {
} }
if err := g.addStateFile(); err != nil { 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 { 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 { 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 != "console" && g.logFile != "" { if g.logFile != "" && !slices.Contains(util.SpecialLogs, g.logFile) {
if err := g.addLogfile(); err != nil { 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 { 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 { } 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 return nil
@@ -290,15 +343,19 @@ func (g *BundleGenerator) createArchive() error {
func (g *BundleGenerator) addSystemInfo() { func (g *BundleGenerator) addSystemInfo() {
if err := g.addRoutes(); err != nil { 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 { if err := g.addInterfaces(); err != nil {
log.Errorf("Failed to add interfaces to debug bundle: %v", err) 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)
} }
if err := g.addFirewallRules(); err != nil { 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)
} }
} }
@@ -353,7 +410,6 @@ func (g *BundleGenerator) addConfig() error {
} }
} }
// Add config content to zip file
configReader := strings.NewReader(configContent.String()) configReader := strings.NewReader(configContent.String())
if err := g.addFileToZip(configReader, "config.txt"); err != nil { if err := g.addFileToZip(configReader, "config.txt"); err != nil {
return fmt.Errorf("add config file to zip: %w", err) return fmt.Errorf("add config file to zip: %w", err)
@@ -365,7 +421,6 @@ func (g *BundleGenerator) addConfig() error {
func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) { func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) {
configContent.WriteString("NetBird Client Configuration:\n\n") 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("WgIface: %s\n", g.internalConfig.WgIface))
configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort)) configContent.WriteString(fmt.Sprintf("WgPort: %d\n", g.internalConfig.WgPort))
if g.internalConfig.NetworkMonitor != nil { if g.internalConfig.NetworkMonitor != nil {
@@ -450,6 +505,27 @@ func (g *BundleGenerator) addInterfaces() error {
return nil 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 { func (g *BundleGenerator) addNetworkMap() error {
if g.networkMap == nil { if g.networkMap == nil {
log.Debugf("skipping empty network map in debug bundle") log.Debugf("skipping empty network map in debug bundle")
@@ -482,7 +558,8 @@ func (g *BundleGenerator) addNetworkMap() error {
} }
func (g *BundleGenerator) addStateFile() error { func (g *BundleGenerator) addStateFile() error {
path := statemanager.GetDefaultStatePath() sm := profilemanager.ServiceManager{}
path := sm.GetStatePath()
if path == "" { if path == "" {
return nil return nil
} }
@@ -520,7 +597,8 @@ func (g *BundleGenerator) addStateFile() error {
} }
func (g *BundleGenerator) addCorruptedStateFiles() error { func (g *BundleGenerator) addCorruptedStateFiles() error {
pattern := statemanager.GetDefaultStatePath() sm := profilemanager.ServiceManager{}
pattern := sm.GetStatePath()
if pattern == "" { if pattern == "" {
return nil return nil
} }
@@ -561,32 +639,7 @@ func (g *BundleGenerator) addLogfile() error {
return fmt.Errorf("add client log file to zip: %w", err) return fmt.Errorf("add client log file to zip: %w", err)
} }
// add latest rotated log file g.addRotatedLogFiles(logDir)
pattern := filepath.Join(logDir, "client-*.log.gz")
files, err := filepath.Glob(pattern)
if err != nil {
log.Warnf("failed to glob rotated logs: %v", err)
} else if len(files) > 0 {
// pick the file with the latest ModTime
sort.Slice(files, func(i, j int) bool {
fi, err := os.Stat(files[i])
if err != nil {
log.Warnf("failed to stat rotated log %s: %v", files[i], err)
return false
}
fj, err := os.Stat(files[j])
if err != nil {
log.Warnf("failed to stat rotated log %s: %v", files[j], err)
return false
}
return fi.ModTime().Before(fj.ModTime())
})
latest := files[len(files)-1]
name := filepath.Base(latest)
if err := g.addSingleLogFileGz(latest, name); err != nil {
log.Warnf("failed to add rotated log %s: %v", name, err)
}
}
stdErrLogPath := filepath.Join(logDir, errorLogFile) stdErrLogPath := filepath.Join(logDir, errorLogFile)
stdoutLogPath := filepath.Join(logDir, stdoutLogFile) stdoutLogPath := filepath.Join(logDir, stdoutLogFile)
@@ -614,7 +667,7 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
} }
defer func() { defer func() {
if err := logFile.Close(); err != nil { 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)
} }
}() }()
@@ -638,13 +691,21 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error {
if err != nil { if err != nil {
return fmt.Errorf("open gz log file %s: %w", targetName, err) return fmt.Errorf("open gz log file %s: %w", targetName, err)
} }
defer f.Close() defer func() {
if err := f.Close(); err != nil {
log.Errorf("failed to close gz file %s: %v", targetName, err)
}
}()
gzr, err := gzip.NewReader(f) gzr, err := gzip.NewReader(f)
if err != nil { if err != nil {
return fmt.Errorf("create gzip reader: %w", err) return fmt.Errorf("create gzip reader: %w", err)
} }
defer gzr.Close() defer func() {
if err := gzr.Close(); err != nil {
log.Errorf("failed to close gzip reader %s: %v", targetName, err)
}
}()
var logReader io.Reader = gzr var logReader io.Reader = gzr
if g.anonymize { if g.anonymize {
@@ -670,6 +731,51 @@ func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error {
return nil return nil
} }
// addRotatedLogFiles adds rotated log files to the bundle based on logFileCount
func (g *BundleGenerator) addRotatedLogFiles(logDir string) {
if g.logFileCount == 0 {
return
}
pattern := filepath.Join(logDir, "client-*.log.gz")
files, err := filepath.Glob(pattern)
if err != nil {
log.Warnf("failed to glob rotated logs: %v", err)
return
}
if len(files) == 0 {
return
}
// sort files by modification time (newest first)
sort.Slice(files, func(i, j int) bool {
fi, err := os.Stat(files[i])
if err != nil {
log.Warnf("failed to stat rotated log %s: %v", files[i], err)
return false
}
fj, err := os.Stat(files[j])
if err != nil {
log.Warnf("failed to stat rotated log %s: %v", files[j], err)
return false
}
return fi.ModTime().After(fj.ModTime())
})
maxFiles := int(g.logFileCount)
if maxFiles > len(files) {
maxFiles = len(files)
}
for i := 0; i < maxFiles; i++ {
name := filepath.Base(files[i])
if err := g.addSingleLogFileGz(files[i], name); err != nil {
log.Warnf("failed to add rotated log %s: %v", name, err)
}
}
}
func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error { func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error {
header := &zip.FileHeader{ header := &zip.FileHeader{
Name: filename, Name: filename,
@@ -684,7 +790,7 @@ func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error
// If the reader is a file, we can get more accurate information // If the reader is a file, we can get more accurate information
if f, ok := reader.(*os.File); ok { if f, ok := reader.(*os.File); ok {
if stat, err := f.Stat(); err != nil { 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 { } else {
header.Modified = stat.ModTime() header.Modified = stat.ModTime()
} }
@@ -732,89 +838,6 @@ 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) { func anonymizeLog(reader io.Reader, writer *io.PipeWriter, anonymizer *anonymize.Anonymizer) {
defer func() { defer func() {
// always nil // always nil
@@ -921,7 +944,6 @@ func anonymizeRemotePeer(peer *mgmProto.RemotePeerConfig, anonymizer *anonymize.
} }
for i, ip := range peer.AllowedIps { for i, ip := range peer.AllowedIps {
// Try to parse as prefix first (CIDR)
if prefix, err := netip.ParsePrefix(ip); err == nil { if prefix, err := netip.ParsePrefix(ip); err == nil {
anonIP := anonymizer.AnonymizeIP(prefix.Addr()) anonIP := anonymizer.AnonymizeIP(prefix.Addr())
peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits()) peer.AllowedIps[i] = fmt.Sprintf("%s/%d", anonIP, prefix.Bits())
@@ -1000,7 +1022,7 @@ func anonymizeRecords(records []*mgmProto.SimpleRecord, anonymizer *anonymize.An
func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) { func anonymizeRData(record *mgmProto.SimpleRecord, anonymizer *anonymize.Anonymizer) {
switch record.Type { switch record.Type {
case 1, 28: // A or AAAA record case 1, 28:
if addr, err := netip.ParseAddr(record.RData); err == nil { if addr, err := netip.ParseAddr(record.RData); err == nil {
record.RData = anonymizer.AnonymizeIP(addr).String() record.RData = anonymizer.AnonymizeIP(addr).String()
} }

View File

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

View File

@@ -12,3 +12,8 @@ func (g *BundleGenerator) trySystemdLogFallback() error {
// TODO: Add BSD support // TODO: Add BSD support
return nil 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 { func (g *BundleGenerator) addRoutes() error {
routes, err := systemops.GetRoutesFromTable() detailedRoutes, err := systemops.GetDetailedRoutesFromTable()
if err != nil { if err != nil {
return fmt.Errorf("get routes: %w", err) return fmt.Errorf("get detailed routes: %w", err)
} }
// TODO: get routes including nexthop routesContent := formatRoutesTable(detailedRoutes, g.anonymize, g.anonymizer)
routesContent := formatRoutes(routes, g.anonymize, g.anonymizer)
routesReader := strings.NewReader(routesContent) routesReader := strings.NewReader(routesContent)
if err := g.addFileToZip(routesReader, "routes.txt"); err != nil { if err := g.addFileToZip(routesReader, "routes.txt"); err != nil {
return fmt.Errorf("add routes file to zip: %w", err) return fmt.Errorf("add routes file to zip: %w", err)
} }
return nil return nil
} }

View File

@@ -0,0 +1,206 @@
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

@@ -0,0 +1,185 @@
//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

@@ -0,0 +1,27 @@
//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

@@ -0,0 +1,37 @@
//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 ( import (
"fmt" "fmt"
"net/netip"
"os" "os"
"regexp"
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -15,9 +15,6 @@ const (
defaultResolvConfPath = "/etc/resolv.conf" defaultResolvConfPath = "/etc/resolv.conf"
) )
var timeoutRegex = regexp.MustCompile(`timeout:\d+`)
var attemptsRegex = regexp.MustCompile(`attempts:\d+`)
type resolvConf struct { type resolvConf struct {
nameServers []string nameServers []string
searchDomains []string searchDomains []string
@@ -108,40 +105,9 @@ func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
return rconf, nil 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 // 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 // and writes the file back to the original location
func removeFirstNbNameserver(filename, nameserverIP string) error { func removeFirstNbNameserver(filename string, nameserverIP netip.Addr) error {
resolvConf, err := parseResolvConfFile(filename) resolvConf, err := parseResolvConfFile(filename)
if err != nil { if err != nil {
return fmt.Errorf("parse backup resolv.conf: %w", err) return fmt.Errorf("parse backup resolv.conf: %w", err)
@@ -151,7 +117,7 @@ func removeFirstNbNameserver(filename, nameserverIP string) error {
return fmt.Errorf("read %s: %w", filename, err) return fmt.Errorf("read %s: %w", filename, err)
} }
if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP { if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP.String() {
newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1) newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1)
stat, err := os.Stat(filename) stat, err := os.Stat(filename)

View File

@@ -3,11 +3,13 @@
package dns package dns
import ( import (
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func Test_parseResolvConf(t *testing.T) { func Test_parseResolvConf(t *testing.T) {
@@ -175,52 +177,6 @@ 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) { func TestRemoveFirstNbNameserver(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
@@ -292,7 +248,9 @@ search localdomain`,
err := os.WriteFile(tempFile, []byte(tc.content), 0644) err := os.WriteFile(tempFile, []byte(tc.content), 0644)
assert.NoError(t, err) assert.NoError(t, err)
err = removeFirstNbNameserver(tempFile, tc.ipToRemove) ip, err := netip.ParseAddr(tc.ipToRemove)
require.NoError(t, err, "Failed to parse IP address")
err = removeFirstNbNameserver(tempFile, ip)
assert.NoError(t, err) assert.NoError(t, err)
content, err := os.ReadFile(tempFile) content, err := os.ReadFile(tempFile)

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ const (
PriorityDNSRoute = 75 PriorityDNSRoute = 75
PriorityUpstream = 50 PriorityUpstream = 50
PriorityDefault = 1 PriorityDefault = 1
PriorityFallback = -100
) )
type SubdomainMatcher interface { type SubdomainMatcher interface {

View File

@@ -11,8 +11,6 @@ import (
nbdns "github.com/netbirdio/netbird/dns" 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 ( const (
ipv4ReverseZone = ".in-addr.arpa." ipv4ReverseZone = ".in-addr.arpa."
ipv6ReverseZone = ".ip6.arpa." ipv6ReverseZone = ".ip6.arpa."
@@ -27,14 +25,14 @@ type hostManager interface {
type SystemDNSSettings struct { type SystemDNSSettings struct {
Domains []string Domains []string
ServerIP string ServerIP netip.Addr
ServerPort int ServerPort int
} }
type HostDNSConfig struct { type HostDNSConfig struct {
Domains []DomainConfig `json:"domains"` Domains []DomainConfig `json:"domains"`
RouteAll bool `json:"routeAll"` RouteAll bool `json:"routeAll"`
ServerIP string `json:"serverIP"` ServerIP netip.Addr `json:"serverIP"`
ServerPort int `json:"serverPort"` ServerPort int `json:"serverPort"`
} }
@@ -89,7 +87,7 @@ func newNoopHostMocker() hostManager {
} }
} }
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostDNSConfig { func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip netip.Addr, port int) HostDNSConfig {
config := HostDNSConfig{ config := HostDNSConfig{
RouteAll: false, RouteAll: false,
ServerIP: ip, ServerIP: ip,

View File

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

View File

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

View File

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

View File

@@ -110,11 +110,7 @@ func (n *networkManagerDbusConfigurator) applyDNSConfig(config HostDNSConfig, st
connSettings.cleanDeprecatedSettings() connSettings.cleanDeprecatedSettings()
dnsIP, err := netip.ParseAddr(config.ServerIP) convDNSIP := binary.LittleEndian.Uint32(config.ServerIP.AsSlice())
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}) connSettings[networkManagerDbusIPv4Key][networkManagerDbusDNSKey] = dbus.MakeVariant([]uint32{convDNSIP})
var ( var (
searchDomains []string searchDomains []string

View File

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

View File

@@ -23,7 +23,6 @@ import (
"github.com/netbirdio/netbird/client/internal/listener" "github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer" "github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/statemanager" "github.com/netbirdio/netbird/client/internal/statemanager"
cProto "github.com/netbirdio/netbird/client/proto"
nbdns "github.com/netbirdio/netbird/dns" nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/management/domain" "github.com/netbirdio/netbird/management/domain"
) )
@@ -44,7 +43,7 @@ type Server interface {
DeregisterHandler(domains domain.List, priority int) DeregisterHandler(domains domain.List, priority int)
Initialize() error Initialize() error
Stop() Stop()
DnsIP() string DnsIP() netip.Addr
UpdateDNSServer(serial uint64, update nbdns.Config) error UpdateDNSServer(serial uint64, update nbdns.Config) error
OnUpdatedHostDNSServer(strings []string) OnUpdatedHostDNSServer(strings []string)
SearchDomains() []string SearchDomains() []string
@@ -58,10 +57,18 @@ type nsGroupsByDomain struct {
groups []*nbdns.NameServerGroup groups []*nbdns.NameServerGroup
} }
// hostManagerWithOriginalNS extends the basic hostManager interface
type hostManagerWithOriginalNS interface {
hostManager
getOriginalNameservers() []string
}
// DefaultServer dns server object // DefaultServer dns server object
type DefaultServer struct { type DefaultServer struct {
ctx context.Context ctx context.Context
ctxCancel context.CancelFunc 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.
disableSys bool disableSys bool
mux sync.Mutex mux sync.Mutex
service service service service
@@ -196,6 +203,7 @@ func newDefaultServer(
statusRecorder: statusRecorder, statusRecorder: statusRecorder,
stateManager: stateManager, stateManager: stateManager,
hostsDNSHolder: newHostsDNSHolder(), hostsDNSHolder: newHostsDNSHolder(),
hostManager: &noopHostConfigurator{},
mgmtCacheResolver: mgmtCacheResolver, mgmtCacheResolver: mgmtCacheResolver,
} }
@@ -229,6 +237,7 @@ func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, p
log.Warn("skipping empty domain") log.Warn("skipping empty domain")
continue continue
} }
s.handlerChain.AddHandler(domain, handler, priority) s.handlerChain.AddHandler(domain, handler, priority)
} }
} }
@@ -267,7 +276,8 @@ func (s *DefaultServer) Initialize() (err error) {
s.mux.Lock() s.mux.Lock()
defer s.mux.Unlock() defer s.mux.Unlock()
if s.hostManager != nil { if !s.isUsingNoopHostManager() {
// already initialized
return nil return nil
} }
@@ -280,19 +290,19 @@ func (s *DefaultServer) Initialize() (err error) {
s.stateManager.RegisterState(&ShutdownState{}) s.stateManager.RegisterState(&ShutdownState{})
// use noop host manager if requested or running in netstack mode. // Keep using noop host manager if dns off requested or running in netstack mode.
// Netstack mode currently doesn't have a way to receive DNS requests. // Netstack mode currently doesn't have a way to receive DNS requests.
// TODO: Use listener on localhost in netstack mode when running as root. // TODO: Use listener on localhost in netstack mode when running as root.
if s.disableSys || netstack.IsEnabled() { if s.disableSys || netstack.IsEnabled() {
log.Info("system DNS is disabled, not setting up host manager") log.Info("system DNS is disabled, not setting up host manager")
s.hostManager = &noopHostConfigurator{}
return nil return nil
} }
s.hostManager, err = s.initialize() hostManager, err := s.initialize()
if err != nil { if err != nil {
return fmt.Errorf("initialize: %w", err) return fmt.Errorf("initialize: %w", err)
} }
s.hostManager = hostManager
return nil return nil
} }
@@ -300,29 +310,48 @@ func (s *DefaultServer) Initialize() (err error) {
// //
// When kernel space interface used it return real DNS server listener IP address // 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) // For bind interface, fake DNS resolver address returned (second last IP address from Nebird network)
func (s *DefaultServer) DnsIP() string { func (s *DefaultServer) DnsIP() netip.Addr {
return s.service.RuntimeIP() return s.service.RuntimeIP()
} }
// Stop stops the server // Stop stops the server
func (s *DefaultServer) Stop() { func (s *DefaultServer) Stop() {
s.mux.Lock()
defer s.mux.Unlock()
s.ctxCancel() s.ctxCancel()
if s.hostManager != nil { s.mux.Lock()
if err := s.hostManager.restoreHostDNS(); err != nil { defer s.mux.Unlock()
log.Error("failed to restore host DNS settings: ", err)
} else if err := s.stateManager.DeleteState(&ShutdownState{}); err != nil { if err := s.disableDNS(); err != nil {
log.Errorf("failed to delete shutdown dns state: %v", err) log.Errorf("failed to disable DNS: %v", err)
}
} }
s.service.Stop()
maps.Clear(s.extraDomains) 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 // 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 // It will be applied if the mgm server do not enforce DNS settings for root zone
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) { func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) {
@@ -361,10 +390,6 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro
s.mux.Lock() s.mux.Lock()
defer s.mux.Unlock() 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{ hash, err := hashstructure.Hash(update, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true, ZeroNil: true,
IgnoreZeroValue: true, IgnoreZeroValue: true,
@@ -445,13 +470,14 @@ func (s *DefaultServer) UpdateServerConfig(domains dnsconfig.ServerDomains) erro
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error { func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
// is the service should be Disabled, we stop the listener or fake resolver // 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 update.ServiceEnable {
if err := s.service.Listen(); err != nil { if err := s.enableDNS(); err != nil {
log.Errorf("failed to start DNS service: %v", err) log.Errorf("failed to enable DNS: %v", err)
} }
} else if !s.permanent { } else if !s.permanent {
s.service.Stop() if err := s.disableDNS(); err != nil {
log.Errorf("failed to disable DNS: %v", err)
}
} }
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones) localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
@@ -496,11 +522,40 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
return nil return nil
} }
func (s *DefaultServer) applyHostConfig() { func (s *DefaultServer) isUsingNoopHostManager() bool {
if s.hostManager == nil { _, isNoop := s.hostManager.(*noopHostConfigurator)
return 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() {
// prevent reapplying config if we're shutting down // prevent reapplying config if we're shutting down
if s.ctx.Err() != nil { if s.ctx.Err() != nil {
return return
@@ -529,25 +584,53 @@ func (s *DefaultServer) applyHostConfig() {
if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil { if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil {
log.Errorf("failed to apply DNS host manager update: %v", err) log.Errorf("failed to apply DNS host manager update: %v", err)
s.handleErrNoGroupaAll(err)
} }
s.registerFallback(config)
} }
func (s *DefaultServer) handleErrNoGroupaAll(err error) { // registerFallback registers original nameservers as low-priority fallback handlers
if !errors.Is(ErrRouteAllWithoutNameserverGroup, err) { func (s *DefaultServer) registerFallback(config HostDNSConfig) {
hostMgrWithNS, ok := s.hostManager.(hostManagerWithOriginalNS)
if !ok {
return return
} }
if s.statusRecorder == nil { originalNameservers := hostMgrWithNS.getOriginalNameservers()
if len(originalNameservers) == 0 {
return return
} }
s.statusRecorder.PublishEvent( log.Infof("registering original nameservers %v as upstream handlers with priority %d", originalNameservers, PriorityFallback)
cProto.SystemEvent_WARNING, cProto.SystemEvent_DNS,
"The host dns manager does not support match domains", handler, err := newUpstreamResolver(
"The host dns manager does not support match domains without a catch-all nameserver group.", s.ctx,
map[string]string{"manager": s.hostManager.string()}, s.wgInterface.Name(),
s.wgInterface.Address().IP,
s.wgInterface.Address().Network,
s.statusRecorder,
s.hostsDNSHolder,
nbdns.RootZone,
) )
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 = formatAddr(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) { func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, []nbdns.SimpleRecord, error) {
@@ -624,14 +707,8 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
// Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts // Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts
priority := basePriority - i priority := basePriority - i
// Check if we're about to overlap with the next priority tier. // 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 if s.leaksPriority(domainGroup, basePriority, priority) {
// 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 break
} }
@@ -684,6 +761,21 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
return muxUpdates, nil 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) { func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
// this will introduce a short period of time when the server is not able to handle DNS requests // this will introduce a short period of time when the server is not able to handle DNS requests
for _, existing := range s.dnsMuxMap { for _, existing := range s.dnsMuxMap {
@@ -716,7 +808,15 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
} }
func getNSHostPort(ns nbdns.NameServer) string { func getNSHostPort(ns nbdns.NameServer) string {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port) return formatAddr(ns.IP.String(), ns.Port)
}
// formatAddr formats a nameserver address with port, handling IPv6 addresses properly
func formatAddr(address string, port int) string {
if ip, err := netip.ParseAddr(address); err == nil && ip.Is6() {
return fmt.Sprintf("[%s]:%d", address, port)
}
return fmt.Sprintf("%s:%d", address, port)
} }
// upstreamCallbacks returns two functions, the first one is used to deactivate // upstreamCallbacks returns two functions, the first one is used to deactivate
@@ -796,6 +896,12 @@ func (s *DefaultServer) upstreamCallbacks(
} }
func (s *DefaultServer) addHostRootZone() { 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( handler, err := newUpstreamResolver(
s.ctx, s.ctx,
s.wgInterface.Name(), s.wgInterface.Name(),
@@ -811,7 +917,7 @@ func (s *DefaultServer) addHostRootZone() {
} }
handler.upstreamServers = make([]string, 0) handler.upstreamServers = make([]string, 0)
for k := range s.hostsDNSHolder.get() { for k := range hostDNSServers {
handler.upstreamServers = append(handler.upstreamServers, k) handler.upstreamServers = append(handler.upstreamServers, k)
} }
handler.deactivate = func(error) {} handler.deactivate = func(error) {}

View File

@@ -956,7 +956,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
return wgIface, nil return wgIface, nil
} }
func newDnsResolver(ip string, port int) *net.Resolver { func newDnsResolver(ip netip.Addr, port int) *net.Resolver {
return &net.Resolver{ return &net.Resolver{
PreferGo: true, PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) { Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
@@ -1065,7 +1065,7 @@ type mockService struct{}
func (m *mockService) Listen() error { return nil } func (m *mockService) Listen() error { return nil }
func (m *mockService) Stop() {} func (m *mockService) Stop() {}
func (m *mockService) RuntimeIP() string { return "127.0.0.1" } func (m *mockService) RuntimeIP() netip.Addr { return netip.MustParseAddr("127.0.0.1") }
func (m *mockService) RuntimePort() int { return 53 } func (m *mockService) RuntimePort() int { return 53 }
func (m *mockService) RegisterMux(string, dns.Handler) {} func (m *mockService) RegisterMux(string, dns.Handler) {}
func (m *mockService) DeregisterMux(string) {} func (m *mockService) DeregisterMux(string) {}
@@ -2071,3 +2071,56 @@ func TestLocalResolverPriorityConstants(t *testing.T) {
assert.Equal(t, PriorityLocal, localMuxUpdates[0].priority, "Local handler should use PriorityLocal") assert.Equal(t, PriorityLocal, localMuxUpdates[0].priority, "Local handler should use PriorityLocal")
assert.Equal(t, "local.example.com", localMuxUpdates[0].domain) assert.Equal(t, "local.example.com", localMuxUpdates[0].domain)
} }
func TestFormatAddr(t *testing.T) {
tests := []struct {
name string
address string
port int
expected string
}{
{
name: "IPv4 address",
address: "8.8.8.8",
port: 53,
expected: "8.8.8.8:53",
},
{
name: "IPv4 address with custom port",
address: "1.1.1.1",
port: 5353,
expected: "1.1.1.1:5353",
},
{
name: "IPv6 address",
address: "fd78:94bf:7df8::1",
port: 53,
expected: "[fd78:94bf:7df8::1]:53",
},
{
name: "IPv6 address with custom port",
address: "2001:db8::1",
port: 5353,
expected: "[2001:db8::1]:5353",
},
{
name: "IPv6 localhost",
address: "::1",
port: 53,
expected: "[::1]:53",
},
{
name: "Invalid address treated as hostname",
address: "dns.example.com",
port: 53,
expected: "dns.example.com:53",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatAddr(tt.address, tt.port)
assert.Equal(t, tt.expected, result)
})
}
}

View File

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

View File

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

View File

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

View File

@@ -89,21 +89,16 @@ func (s *systemdDbusConfigurator) supportCustomPort() bool {
} }
func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error { 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{ defaultLinkInput := systemdDbusDNSInput{
Family: unix.AF_INET, Family: unix.AF_INET,
Address: ipAs4[:], Address: config.ServerIP.AsSlice(),
} }
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) 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 // 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) log.Warnf("failed to set DNSSEC to 'no': %v", err)
} }
@@ -129,8 +124,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
} }
if config.RouteAll { if config.RouteAll {
err = s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true) if err := s.callLinkMethod(systemdDbusSetDefaultRouteMethodSuffix, true); err != nil {
if err != nil {
return fmt.Errorf("set link as default dns router: %w", err) return fmt.Errorf("set link as default dns router: %w", err)
} }
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{ domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
@@ -139,7 +133,7 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
}) })
log.Infof("configured %s:%d as main DNS forwarder for this peer", config.ServerIP, config.ServerPort) log.Infof("configured %s:%d as main DNS forwarder for this peer", config.ServerIP, config.ServerPort)
} else { } 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) return fmt.Errorf("remove link as default dns router: %w", err)
} }
} }
@@ -153,9 +147,8 @@ 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) log.Infof("adding %d search domains and %d match domains. Search list: %s , Match list: %s", len(searchDomains), len(matchDomains), searchDomains, matchDomains)
err = s.setDomainsForInterface(domainsInput) if err := s.setDomainsForInterface(domainsInput); err != nil {
if err != nil { log.Error("failed to set domains for interface: ", err)
log.Error(err)
} }
if err := s.flushDNSCache(); err != nil { if err := s.flushDNSCache(); err != nil {

View File

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

View File

@@ -8,6 +8,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"net/url" "net/url"
"os"
"reflect" "reflect"
"runtime" "runtime"
"slices" "slices"
@@ -43,6 +44,7 @@ import (
"github.com/netbirdio/netbird/client/internal/peer/guard" "github.com/netbirdio/netbird/client/internal/peer/guard"
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice" icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
"github.com/netbirdio/netbird/client/internal/peerstore" "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/relay"
"github.com/netbirdio/netbird/client/internal/rosenpass" "github.com/netbirdio/netbird/client/internal/rosenpass"
"github.com/netbirdio/netbird/client/internal/routemanager" "github.com/netbirdio/netbird/client/internal/routemanager"
@@ -63,7 +65,6 @@ import (
signal "github.com/netbirdio/netbird/signal/client" signal "github.com/netbirdio/netbird/signal/client"
sProto "github.com/netbirdio/netbird/signal/proto" sProto "github.com/netbirdio/netbird/signal/proto"
"github.com/netbirdio/netbird/util" "github.com/netbirdio/netbird/util"
nbnet "github.com/netbirdio/netbird/util/net"
) )
// PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer. // PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer.
@@ -140,9 +141,6 @@ type Engine struct {
connMgr *ConnMgr connMgr *ConnMgr
beforePeerHook nbnet.AddHookFunc
afterPeerHook nbnet.RemoveHookFunc
// rpManager is a Rosenpass manager // rpManager is a Rosenpass manager
rpManager *rosenpass.Manager rpManager *rosenpass.Manager
@@ -242,7 +240,9 @@ func NewEngine(
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit), connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
} }
path := statemanager.GetDefaultStatePath() sm := profilemanager.ServiceManager{}
path := sm.GetStatePath()
if runtime.GOOS == "ios" { if runtime.GOOS == "ios" {
if !fileExists(mobileDep.StateFilePath) { if !fileExists(mobileDep.StateFilePath) {
err := createFile(mobileDep.StateFilePath) err := createFile(mobileDep.StateFilePath)
@@ -416,12 +416,8 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
DisableClientRoutes: e.config.DisableClientRoutes, DisableClientRoutes: e.config.DisableClientRoutes,
DisableServerRoutes: e.config.DisableServerRoutes, DisableServerRoutes: e.config.DisableServerRoutes,
}) })
beforePeerHook, afterPeerHook, err := e.routeManager.Init() if err := e.routeManager.Init(); err != nil {
if err != nil {
log.Errorf("Failed to initialize route manager: %s", err) log.Errorf("Failed to initialize route manager: %s", err)
} else {
e.beforePeerHook = beforePeerHook
e.afterPeerHook = afterPeerHook
} }
e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener) e.routeManager.SetRouteChangeListener(e.mobileDep.NetworkChangeListener)
@@ -1296,10 +1292,6 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
return fmt.Errorf("peer already exists: %s", peerKey) return fmt.Errorf("peer already exists: %s", peerKey)
} }
if e.beforePeerHook != nil && e.afterPeerHook != nil {
conn.AddBeforeAddPeerHook(e.beforePeerHook)
conn.AddAfterRemovePeerHook(e.afterPeerHook)
}
return nil return nil
} }
@@ -1597,7 +1589,7 @@ func (e *Engine) newWgIface() (*iface.WGIface, error) {
func (e *Engine) wgInterfaceCreate() (err error) { func (e *Engine) wgInterfaceCreate() (err error) {
switch runtime.GOOS { switch runtime.GOOS {
case "android": case "android":
err = e.wgInterface.CreateOnAndroid(e.routeManager.InitialRouteRange(), e.dnsServer.DnsIP(), e.dnsServer.SearchDomains()) err = e.wgInterface.CreateOnAndroid(e.routeManager.InitialRouteRange(), e.dnsServer.DnsIP().String(), e.dnsServer.SearchDomains())
case "ios": case "ios":
e.mobileDep.NetworkChangeListener.SetInterfaceIP(e.config.WgAddr) e.mobileDep.NetworkChangeListener.SetInterfaceIP(e.config.WgAddr)
err = e.wgInterface.Create() err = e.wgInterface.Create()
@@ -2022,21 +2014,24 @@ func (e *Engine) toExcludedLazyPeers(rules []firewallManager.ForwardRule, peers
} }
// isChecksEqual checks if two slices of checks are equal. // isChecksEqual checks if two slices of checks are equal.
func isChecksEqual(checks []*mgmProto.Checks, oChecks []*mgmProto.Checks) bool { func isChecksEqual(checks1, checks2 []*mgmProto.Checks) bool {
for _, check := range checks { normalize := func(checks []*mgmProto.Checks) []string {
sort.Slice(check.Files, func(i, j int) bool { normalized := make([]string, len(checks))
return check.Files[i] < check.Files[j]
}) for i, check := range checks {
} sortedFiles := slices.Clone(check.Files)
for _, oCheck := range oChecks { sort.Strings(sortedFiles)
sort.Slice(oCheck.Files, func(i, j int) bool { normalized[i] = strings.Join(sortedFiles, "|")
return oCheck.Files[i] < oCheck.Files[j] }
})
sort.Strings(normalized)
return normalized
} }
return slices.EqualFunc(checks, oChecks, func(checks, oChecks *mgmProto.Checks) bool { n1 := normalize(checks1)
return slices.Equal(checks.Files, oChecks.Files) n2 := normalize(checks2)
})
return slices.Equal(n1, n2)
} }
func getInterfacePrefixes() ([]netip.Prefix, error) { func getInterfacePrefixes() ([]netip.Prefix, error) {
@@ -2113,3 +2108,16 @@ func compareNetIPLists(list1 []netip.Prefix, list2 []string) bool {
} }
return true 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()
}

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