mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-05 09:03:54 -04:00
Compare commits
1 Commits
v0.60.4
...
snyk-fix-9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3fc6ab5e3 |
117
.github/workflows/check-license-dependencies.yml
vendored
117
.github/workflows/check-license-dependencies.yml
vendored
@@ -3,108 +3,39 @@ name: Check License Dependencies
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
paths:
|
|
||||||
- 'go.mod'
|
|
||||||
- 'go.sum'
|
|
||||||
- '.github/workflows/check-license-dependencies.yml'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- 'go.mod'
|
|
||||||
- 'go.sum'
|
|
||||||
- '.github/workflows/check-license-dependencies.yml'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-internal-dependencies:
|
check-dependencies:
|
||||||
name: Check Internal AGPL Dependencies
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Check for problematic license dependencies
|
|
||||||
run: |
|
|
||||||
echo "Checking for dependencies on management/, signal/, and relay/ packages..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Find all directories except the problematic ones and system dirs
|
|
||||||
FOUND_ISSUES=0
|
|
||||||
while IFS= read -r dir; do
|
|
||||||
echo "=== Checking $dir ==="
|
|
||||||
# Search for problematic imports, excluding test files
|
|
||||||
RESULTS=$(grep -r "github.com/netbirdio/netbird/\(management\|signal\|relay\)" "$dir" --include="*.go" 2>/dev/null | grep -v "_test.go" | grep -v "test_" | grep -v "/test/" || true)
|
|
||||||
if [ -n "$RESULTS" ]; then
|
|
||||||
echo "❌ Found problematic dependencies:"
|
|
||||||
echo "$RESULTS"
|
|
||||||
FOUND_ISSUES=1
|
|
||||||
else
|
|
||||||
echo "✓ No problematic dependencies found"
|
|
||||||
fi
|
|
||||||
done < <(find . -maxdepth 1 -type d -not -name "." -not -name "management" -not -name "signal" -not -name "relay" -not -name ".git*" | sort)
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
if [ $FOUND_ISSUES -eq 1 ]; then
|
|
||||||
echo "❌ Found dependencies on management/, signal/, or relay/ packages"
|
|
||||||
echo "These packages are licensed under AGPLv3 and must not be imported by BSD-licensed code"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo ""
|
|
||||||
echo "✅ All internal license dependencies are clean"
|
|
||||||
fi
|
|
||||||
|
|
||||||
check-external-licenses:
|
|
||||||
name: Check External GPL/AGPL Licenses
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Check for problematic license dependencies
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install go-licenses
|
|
||||||
run: go install github.com/google/go-licenses@v1.6.0
|
|
||||||
|
|
||||||
- name: Check for GPL/AGPL licensed dependencies
|
|
||||||
run: |
|
run: |
|
||||||
echo "Checking for GPL/AGPL/LGPL licensed dependencies..."
|
echo "Checking for dependencies on management/, signal/, and relay/ packages..."
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check all Go packages for copyleft licenses, excluding internal netbird packages
|
# Find all directories except the problematic ones and system dirs
|
||||||
COPYLEFT_DEPS=$(go-licenses report ./... 2>/dev/null | grep -E 'GPL|AGPL|LGPL' | grep -v 'github.com/netbirdio/netbird/' || true)
|
FOUND_ISSUES=0
|
||||||
|
find . -maxdepth 1 -type d -not -name "." -not -name "management" -not -name "signal" -not -name "relay" -not -name ".git*" | sort | while read dir; do
|
||||||
if [ -n "$COPYLEFT_DEPS" ]; then
|
echo "=== Checking $dir ==="
|
||||||
echo "Found copyleft licensed dependencies:"
|
# Search for problematic imports, excluding test files
|
||||||
echo "$COPYLEFT_DEPS"
|
RESULTS=$(grep -r "github.com/netbirdio/netbird/\(management\|signal\|relay\)" "$dir" --include="*.go" | grep -v "_test.go" | grep -v "test_" | grep -v "/test/" || true)
|
||||||
echo ""
|
if [ ! -z "$RESULTS" ]; then
|
||||||
|
echo "❌ Found problematic dependencies:"
|
||||||
# Filter out dependencies that are only pulled in by internal AGPL packages
|
echo "$RESULTS"
|
||||||
INCOMPATIBLE=""
|
FOUND_ISSUES=1
|
||||||
while IFS=',' read -r package url license; do
|
else
|
||||||
if echo "$license" | grep -qE 'GPL-[0-9]|AGPL-[0-9]|LGPL-[0-9]'; then
|
echo "✓ No problematic dependencies found"
|
||||||
# Find ALL packages that import this GPL package using go list
|
|
||||||
IMPORTERS=$(go list -json -deps ./... 2>/dev/null | jq -r "select(.Imports[]? == \"$package\") | .ImportPath")
|
|
||||||
|
|
||||||
# Check if any importer is NOT in management/signal/relay
|
|
||||||
BSD_IMPORTER=$(echo "$IMPORTERS" | grep -v "github.com/netbirdio/netbird/\(management\|signal\|relay\)" | head -1)
|
|
||||||
|
|
||||||
if [ -n "$BSD_IMPORTER" ]; then
|
|
||||||
echo "❌ $package ($license) is imported by BSD-licensed code: $BSD_IMPORTER"
|
|
||||||
INCOMPATIBLE="${INCOMPATIBLE}${package},${url},${license}\n"
|
|
||||||
else
|
|
||||||
echo "✓ $package ($license) is only used by internal AGPL packages - OK"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done <<< "$COPYLEFT_DEPS"
|
|
||||||
|
|
||||||
if [ -n "$INCOMPATIBLE" ]; then
|
|
||||||
echo ""
|
|
||||||
echo "❌ INCOMPATIBLE licenses found that are used by BSD-licensed code:"
|
|
||||||
echo -e "$INCOMPATIBLE"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
if [ $FOUND_ISSUES -eq 1 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "❌ Found dependencies on management/, signal/, or relay/ packages"
|
||||||
|
echo "These packages will change license and should not be imported by client or shared code"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "✅ All license dependencies are clean"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ All external license dependencies are compatible with BSD-3-Clause"
|
|
||||||
|
|||||||
7
.github/workflows/golang-test-darwin.yml
vendored
7
.github/workflows/golang-test-darwin.yml
vendored
@@ -15,14 +15,13 @@ jobs:
|
|||||||
name: "Client / Unit"
|
name: "Client / Unit"
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
2
.github/workflows/golang-test-freebsd.yml
vendored
2
.github/workflows/golang-test-freebsd.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
|||||||
release: "14.2"
|
release: "14.2"
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y curl pkgconf xorg
|
pkg install -y curl pkgconf xorg
|
||||||
GO_TARBALL="go1.24.10.freebsd-amd64.tar.gz"
|
GO_TARBALL="go1.23.12.freebsd-amd64.tar.gz"
|
||||||
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
GO_URL="https://go.dev/dl/$GO_TARBALL"
|
||||||
curl -vLO "$GO_URL"
|
curl -vLO "$GO_URL"
|
||||||
tar -C /usr/local -vxzf "$GO_TARBALL"
|
tar -C /usr/local -vxzf "$GO_TARBALL"
|
||||||
|
|||||||
69
.github/workflows/golang-test-linux.yml
vendored
69
.github/workflows/golang-test-linux.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
@@ -106,15 +106,15 @@ jobs:
|
|||||||
arch: [ '386','amd64' ]
|
arch: [ '386','amd64' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
@@ -151,15 +151,15 @@ jobs:
|
|||||||
needs: [ build-cache ]
|
needs: [ build-cache ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
id: go-env
|
id: go-env
|
||||||
run: |
|
run: |
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
-e GOCACHE=${CONTAINER_GOCACHE} \
|
-e GOCACHE=${CONTAINER_GOCACHE} \
|
||||||
-e GOMODCACHE=${CONTAINER_GOMODCACHE} \
|
-e GOMODCACHE=${CONTAINER_GOMODCACHE} \
|
||||||
-e CONTAINER=${CONTAINER} \
|
-e CONTAINER=${CONTAINER} \
|
||||||
golang:1.24-alpine \
|
golang:1.23-alpine \
|
||||||
sh -c ' \
|
sh -c ' \
|
||||||
apk update; apk add --no-cache \
|
apk update; apk add --no-cache \
|
||||||
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
||||||
@@ -220,15 +220,15 @@ jobs:
|
|||||||
raceFlag: "-race"
|
raceFlag: "-race"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
|
run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
|
||||||
@@ -270,15 +270,15 @@ jobs:
|
|||||||
arch: [ '386','amd64' ]
|
arch: [ '386','amd64' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
|
run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
|
||||||
@@ -321,15 +321,15 @@ jobs:
|
|||||||
store: [ 'sqlite', 'postgres', 'mysql' ]
|
store: [ 'sqlite', 'postgres', 'mysql' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
@@ -408,16 +408,15 @@ jobs:
|
|||||||
-v $PWD/prometheus.yml:/etc/prometheus/prometheus.yml \
|
-v $PWD/prometheus.yml:/etc/prometheus/prometheus.yml \
|
||||||
-p 9090:9090 \
|
-p 9090:9090 \
|
||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
@@ -498,15 +497,15 @@ jobs:
|
|||||||
-p 9090:9090 \
|
-p 9090:9090 \
|
||||||
prom/prometheus
|
prom/prometheus
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
@@ -562,15 +561,15 @@ jobs:
|
|||||||
store: [ 'sqlite', 'postgres']
|
store: [ 'sqlite', 'postgres']
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
|
|||||||
2
.github/workflows/golang-test-windows.yml
vendored
2
.github/workflows/golang-test-windows.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
id: go
|
id: go
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
|
|||||||
4
.github/workflows/golangci-lint.yml
vendored
4
.github/workflows/golangci-lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- name: codespell
|
- name: codespell
|
||||||
uses: codespell-project/actions-codespell@v2
|
uses: codespell-project/actions-codespell@v2
|
||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe
|
||||||
skip: go.mod,go.sum
|
skip: go.mod,go.sum
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
with:
|
with:
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- name: Setup NDK
|
- name: Setup NDK
|
||||||
run: /usr/local/lib/android/sdk/cmdline-tools/7.0/bin/sdkmanager --install "ndk;23.1.7779620"
|
run: /usr/local/lib/android/sdk/cmdline-tools/7.0/bin/sdkmanager --install "ndk;23.1.7779620"
|
||||||
- name: install gomobile
|
- name: install gomobile
|
||||||
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20251113184115-a159579294ab
|
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240404231514-09dbf07665ed
|
||||||
- name: gomobile init
|
- name: gomobile init
|
||||||
run: gomobile init
|
run: gomobile init
|
||||||
- name: build android netbird lib
|
- name: build android netbird lib
|
||||||
@@ -56,9 +56,9 @@ jobs:
|
|||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
- name: install gomobile
|
- name: install gomobile
|
||||||
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20251113184115-a159579294ab
|
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240404231514-09dbf07665ed
|
||||||
- name: gomobile init
|
- name: gomobile init
|
||||||
run: gomobile init
|
run: gomobile init
|
||||||
- name: build iOS netbird lib
|
- name: build iOS netbird lib
|
||||||
|
|||||||
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.23"
|
SIGN_PIPE_VER: "v0.0.22"
|
||||||
GORELEASER_VER: "v2.3.2"
|
GORELEASER_VER: "v2.3.2"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
@@ -20,7 +20,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest-m
|
runs-on: ubuntu-22.04
|
||||||
env:
|
env:
|
||||||
flags: ""
|
flags: ""
|
||||||
steps:
|
steps:
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -136,7 +136,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -200,7 +200,7 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23"
|
||||||
cache: false
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
@@ -67,13 +67,10 @@ jobs:
|
|||||||
- name: Install curl
|
- name: Install curl
|
||||||
run: sudo apt-get install -y curl
|
run: sudo apt-get install -y curl
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: "go.mod"
|
go-version: "1.23.x"
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -83,6 +80,9 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup MySQL privileges
|
- name: Setup MySQL privileges
|
||||||
if: matrix.store == 'mysql'
|
if: matrix.store == 'mysql'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
67
.github/workflows/wasm-build-validation.yml
vendored
67
.github/workflows/wasm-build-validation.yml
vendored
@@ -1,67 +0,0 @@
|
|||||||
name: Wasm
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
js_lint:
|
|
||||||
name: "JS / Lint"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: "go.mod"
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
|
||||||
- name: Install golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@d6238b002a20823d52840fda27e2d4891c5952dc
|
|
||||||
with:
|
|
||||||
version: latest
|
|
||||||
install-mode: binary
|
|
||||||
skip-cache: true
|
|
||||||
skip-pkg-cache: true
|
|
||||||
skip-build-cache: true
|
|
||||||
- name: Run golangci-lint for WASM
|
|
||||||
run: |
|
|
||||||
GOOS=js GOARCH=wasm golangci-lint run --timeout=12m --out-format colored-line-number ./client/...
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
js_build:
|
|
||||||
name: "JS / Build"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version-file: "go.mod"
|
|
||||||
- name: Build Wasm client
|
|
||||||
run: GOOS=js GOARCH=wasm go build -o netbird.wasm ./client/wasm/cmd
|
|
||||||
env:
|
|
||||||
CGO_ENABLED: 0
|
|
||||||
- name: Check Wasm build size
|
|
||||||
run: |
|
|
||||||
echo "Wasm build size:"
|
|
||||||
ls -lh netbird.wasm
|
|
||||||
|
|
||||||
SIZE=$(stat -c%s netbird.wasm)
|
|
||||||
SIZE_MB=$((SIZE / 1024 / 1024))
|
|
||||||
|
|
||||||
echo "Size: ${SIZE} bytes (${SIZE_MB} MB)"
|
|
||||||
|
|
||||||
if [ ${SIZE} -gt 57671680 ]; then
|
|
||||||
echo "Wasm binary size (${SIZE_MB}MB) exceeds 55MB limit!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
0
.gitmodules
vendored
0
.gitmodules
vendored
@@ -2,18 +2,6 @@ version: 2
|
|||||||
|
|
||||||
project_name: netbird
|
project_name: netbird
|
||||||
builds:
|
builds:
|
||||||
- id: netbird-wasm
|
|
||||||
dir: client/wasm/cmd
|
|
||||||
binary: netbird
|
|
||||||
env: [GOOS=js, GOARCH=wasm, CGO_ENABLED=0]
|
|
||||||
goos:
|
|
||||||
- js
|
|
||||||
goarch:
|
|
||||||
- wasm
|
|
||||||
ldflags:
|
|
||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
|
||||||
mod_timestamp: "{{ .CommitTimestamp }}"
|
|
||||||
|
|
||||||
- id: netbird
|
- id: netbird
|
||||||
dir: client
|
dir: client
|
||||||
binary: netbird
|
binary: netbird
|
||||||
@@ -127,11 +115,6 @@ archives:
|
|||||||
- builds:
|
- builds:
|
||||||
- netbird
|
- netbird
|
||||||
- netbird-static
|
- netbird-static
|
||||||
- id: netbird-wasm
|
|
||||||
builds:
|
|
||||||
- netbird-wasm
|
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}"
|
|
||||||
format: binary
|
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
- maintainer: Netbird <dev@netbird.io>
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -53,7 +52,7 @@
|
|||||||
|
|
||||||
### Open Source Network Security in a Single Platform
|
### Open Source Network Security in a Single Platform
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
<img width="1188" alt="centralized-network-management 1" src="https://github.com/user-attachments/assets/c28cc8e4-15d2-4d2f-bb97-a6433db39d56" />
|
||||||
|
|
||||||
### NetBird on Lawrence Systems (Video)
|
### NetBird on Lawrence Systems (Video)
|
||||||
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
|
[](https://www.youtube.com/watch?v=Kwrff6h0rEw)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# sudo podman build -t localhost/netbird:latest -f client/Dockerfile --ignorefile .dockerignore-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
|
# sudo podman run --rm -it --cap-add={BPF,NET_ADMIN,NET_RAW} localhost/netbird:latest
|
||||||
|
|
||||||
FROM alpine:3.22.2
|
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 \
|
RUN apk add --no-cache \
|
||||||
bash \
|
bash \
|
||||||
@@ -18,7 +18,7 @@ ENV \
|
|||||||
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
||||||
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
||||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
|
NB_ENTRYPOINT_SERVICE_TIMEOUT="5" \
|
||||||
NB_ENTRYPOINT_LOGIN_TIMEOUT="5"
|
NB_ENTRYPOINT_LOGIN_TIMEOUT="1"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,10 @@ package android
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
@@ -19,13 +16,10 @@ 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/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/client/net"
|
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/formatter"
|
"github.com/netbirdio/netbird/formatter"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/util/net"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionListener export internal Listener for mobile
|
// ConnectionListener export internal Listener for mobile
|
||||||
@@ -68,18 +62,17 @@ type Client struct {
|
|||||||
deviceName string
|
deviceName string
|
||||||
uiVersion string
|
uiVersion string
|
||||||
networkChangeListener listener.NetworkChangeListener
|
networkChangeListener listener.NetworkChangeListener
|
||||||
stateFile string
|
|
||||||
|
|
||||||
connectClient *internal.ConnectClient
|
connectClient *internal.ConnectClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient instantiate a new Client
|
// NewClient instantiate a new Client
|
||||||
func NewClient(platformFiles PlatformFiles, androidSDKVersion int, 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)
|
execWorkaround(androidSDKVersion)
|
||||||
|
|
||||||
net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
|
net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
|
||||||
return &Client{
|
return &Client{
|
||||||
cfgFile: platformFiles.ConfigurationFilePath(),
|
cfgFile: cfgFile,
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
uiVersion: uiVersion,
|
uiVersion: uiVersion,
|
||||||
tunAdapter: tunAdapter,
|
tunAdapter: tunAdapter,
|
||||||
@@ -87,12 +80,11 @@ func NewClient(platformFiles PlatformFiles, androidSDKVersion int, deviceName st
|
|||||||
recorder: peer.NewRecorder(""),
|
recorder: peer.NewRecorder(""),
|
||||||
ctxCancelLock: &sync.Mutex{},
|
ctxCancelLock: &sync.Mutex{},
|
||||||
networkChangeListener: networkChangeListener,
|
networkChangeListener: networkChangeListener,
|
||||||
stateFile: platformFiles.StateFilePath(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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, isAndroidTV bool, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
||||||
exportEnvList(envList)
|
exportEnvList(envList)
|
||||||
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
||||||
ConfigPath: c.cfgFile,
|
ConfigPath: c.cfgFile,
|
||||||
@@ -115,7 +107,7 @@ func (c *Client) Run(urlOpener URLOpener, isAndroidTV bool, dns *DNSList, dnsRea
|
|||||||
c.ctxCancelLock.Unlock()
|
c.ctxCancelLock.Unlock()
|
||||||
|
|
||||||
auth := NewAuthWithConfig(ctx, cfg)
|
auth := NewAuthWithConfig(ctx, cfg)
|
||||||
err = auth.login(urlOpener, isAndroidTV)
|
err = auth.login(urlOpener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -123,7 +115,7 @@ func (c *Client) Run(urlOpener URLOpener, isAndroidTV bool, dns *DNSList, dnsRea
|
|||||||
// todo do not throw error in case of cancelled context
|
// todo do not throw error in case of cancelled context
|
||||||
ctx = internal.CtxInitState(ctx)
|
ctx = internal.CtxInitState(ctx)
|
||||||
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, c.stateFile)
|
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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).
|
||||||
@@ -150,7 +142,7 @@ func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener
|
|||||||
// todo do not throw error in case of cancelled context
|
// todo do not throw error in case of cancelled context
|
||||||
ctx = internal.CtxInitState(ctx)
|
ctx = internal.CtxInitState(ctx)
|
||||||
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener, c.stateFile)
|
return c.connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, slices.Clone(dns.items), dnsReadyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the internal client and free the resources
|
// Stop the internal client and free the resources
|
||||||
@@ -164,19 +156,6 @@ func (c *Client) Stop() {
|
|||||||
c.ctxCancel()
|
c.ctxCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RenewTun(fd int) error {
|
|
||||||
if c.connectClient == nil {
|
|
||||||
return fmt.Errorf("engine not running")
|
|
||||||
}
|
|
||||||
|
|
||||||
e := c.connectClient.Engine()
|
|
||||||
if e == nil {
|
|
||||||
return fmt.Errorf("engine not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.RenewTun(fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTraceLogLevel configure the logger to trace level
|
// SetTraceLogLevel configure the logger to trace level
|
||||||
func (c *Client) SetTraceLogLevel() {
|
func (c *Client) SetTraceLogLevel() {
|
||||||
log.SetLevel(log.TraceLevel)
|
log.SetLevel(log.TraceLevel)
|
||||||
@@ -198,7 +177,6 @@ func (c *Client) PeersList() *PeerInfoArray {
|
|||||||
p.IP,
|
p.IP,
|
||||||
p.FQDN,
|
p.FQDN,
|
||||||
p.ConnStatus.String(),
|
p.ConnStatus.String(),
|
||||||
PeerRoutes{routes: maps.Keys(p.GetRoutes())},
|
|
||||||
}
|
}
|
||||||
peerInfos[n] = pi
|
peerInfos[n] = pi
|
||||||
}
|
}
|
||||||
@@ -223,43 +201,31 @@ func (c *Client) Networks() *NetworkArray {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
routeSelector := routeManager.GetRouteSelector()
|
|
||||||
if routeSelector == nil {
|
|
||||||
log.Error("could not get route selector")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
networkArray := &NetworkArray{
|
networkArray := &NetworkArray{
|
||||||
items: make([]Network, 0),
|
items: make([]Network, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvedDomains := c.recorder.GetResolvedDomainsStates()
|
|
||||||
|
|
||||||
for id, routes := range routeManager.GetClientRoutesWithNetID() {
|
for id, routes := range routeManager.GetClientRoutesWithNetID() {
|
||||||
if len(routes) == 0 {
|
if len(routes) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
r := routes[0]
|
r := routes[0]
|
||||||
domains := c.getNetworkDomainsFromRoute(r, resolvedDomains)
|
|
||||||
netStr := r.Network.String()
|
netStr := r.Network.String()
|
||||||
|
|
||||||
if r.IsDynamic() {
|
if r.IsDynamic() {
|
||||||
netStr = r.Domains.SafeString()
|
netStr = r.Domains.SafeString()
|
||||||
}
|
}
|
||||||
|
|
||||||
routePeer, err := c.recorder.GetPeer(routes[0].Peer)
|
peer, err := c.recorder.GetPeer(routes[0].Peer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("could not get peer info for %s: %v", routes[0].Peer, err)
|
log.Errorf("could not get peer info for %s: %v", routes[0].Peer, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
network := Network{
|
network := Network{
|
||||||
Name: string(id),
|
Name: string(id),
|
||||||
Network: netStr,
|
Network: netStr,
|
||||||
Peer: routePeer.FQDN,
|
Peer: peer.FQDN,
|
||||||
Status: routePeer.ConnStatus.String(),
|
Status: peer.ConnStatus.String(),
|
||||||
IsSelected: routeSelector.IsSelected(id),
|
|
||||||
Domains: domains,
|
|
||||||
}
|
}
|
||||||
networkArray.Add(network)
|
networkArray.Add(network)
|
||||||
}
|
}
|
||||||
@@ -287,69 +253,6 @@ func (c *Client) RemoveConnectionListener() {
|
|||||||
c.recorder.RemoveConnectionListener()
|
c.recorder.RemoveConnectionListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) toggleRoute(command routeCommand) error {
|
|
||||||
return command.toggleRoute()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getRouteManager() (routemanager.Manager, error) {
|
|
||||||
client := c.connectClient
|
|
||||||
if client == nil {
|
|
||||||
return nil, fmt.Errorf("not connected")
|
|
||||||
}
|
|
||||||
|
|
||||||
engine := client.Engine()
|
|
||||||
if engine == nil {
|
|
||||||
return nil, fmt.Errorf("engine is not running")
|
|
||||||
}
|
|
||||||
|
|
||||||
manager := engine.GetRouteManager()
|
|
||||||
if manager == nil {
|
|
||||||
return nil, fmt.Errorf("could not get route manager")
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) SelectRoute(route string) error {
|
|
||||||
manager, err := c.getRouteManager()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.toggleRoute(selectRouteCommand{route: route, manager: manager})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) DeselectRoute(route string) error {
|
|
||||||
manager, err := c.getRouteManager()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.toggleRoute(deselectRouteCommand{route: route, manager: manager})
|
|
||||||
}
|
|
||||||
|
|
||||||
// getNetworkDomainsFromRoute extracts domains from a route and enriches each domain
|
|
||||||
// with its resolved IP addresses from the provided resolvedDomains map.
|
|
||||||
func (c *Client) getNetworkDomainsFromRoute(route *route.Route, resolvedDomains map[domain.Domain]peer.ResolvedDomainInfo) NetworkDomains {
|
|
||||||
domains := NetworkDomains{}
|
|
||||||
|
|
||||||
for _, d := range route.Domains {
|
|
||||||
networkDomain := NetworkDomain{
|
|
||||||
Address: d.SafeString(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if info, exists := resolvedDomains[d]; exists {
|
|
||||||
for _, prefix := range info.Prefixes {
|
|
||||||
networkDomain.addResolvedIP(prefix.Addr().String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
domains.Add(&networkDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
return domains
|
|
||||||
}
|
|
||||||
|
|
||||||
func exportEnvList(list *EnvList) {
|
func exportEnvList(list *EnvList) {
|
||||||
if list == nil {
|
if list == nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ type ErrListener interface {
|
|||||||
// URLOpener it is a callback interface. The Open function will be triggered if
|
// URLOpener it is a callback interface. The Open function will be triggered if
|
||||||
// the backend want to show an url for the user
|
// the backend want to show an url for the user
|
||||||
type URLOpener interface {
|
type URLOpener interface {
|
||||||
Open(url string, userCode string)
|
Open(string)
|
||||||
OnLoginSuccess()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth can register or login new client
|
// Auth can register or login new client
|
||||||
@@ -148,9 +147,9 @@ func (a *Auth) loginWithSetupKeyAndSaveConfig(setupKey string, deviceName string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Login try register the client on the server
|
// Login try register the client on the server
|
||||||
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener, isAndroidTV bool) {
|
func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener) {
|
||||||
go func() {
|
go func() {
|
||||||
err := a.login(urlOpener, isAndroidTV)
|
err := a.login(urlOpener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resultListener.OnError(err)
|
resultListener.OnError(err)
|
||||||
} else {
|
} else {
|
||||||
@@ -159,7 +158,7 @@ func (a *Auth) Login(resultListener ErrListener, urlOpener URLOpener, isAndroidT
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) login(urlOpener URLOpener, isAndroidTV bool) error {
|
func (a *Auth) login(urlOpener URLOpener) error {
|
||||||
var needsLogin bool
|
var needsLogin bool
|
||||||
|
|
||||||
// check if we need to generate JWT token
|
// check if we need to generate JWT token
|
||||||
@@ -173,7 +172,7 @@ func (a *Auth) login(urlOpener URLOpener, isAndroidTV bool) error {
|
|||||||
|
|
||||||
jwtToken := ""
|
jwtToken := ""
|
||||||
if needsLogin {
|
if needsLogin {
|
||||||
tokenInfo, err := a.foregroundGetTokenInfo(urlOpener, isAndroidTV)
|
tokenInfo, err := a.foregroundGetTokenInfo(urlOpener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("interactive sso login failed: %v", err)
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -182,11 +181,6 @@ func (a *Auth) login(urlOpener URLOpener, isAndroidTV bool) error {
|
|||||||
|
|
||||||
err = a.withBackOff(a.ctx, func() error {
|
err = a.withBackOff(a.ctx, func() error {
|
||||||
err := internal.Login(a.ctx, a.config, "", jwtToken)
|
err := internal.Login(a.ctx, a.config, "", jwtToken)
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
go urlOpener.OnLoginSuccess()
|
|
||||||
}
|
|
||||||
|
|
||||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -199,8 +193,8 @@ func (a *Auth) login(urlOpener URLOpener, isAndroidTV bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener, isAndroidTV bool) (*auth.TokenInfo, error) {
|
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) {
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false, isAndroidTV, "")
|
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -210,7 +204,7 @@ func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener, isAndroidTV bool) (*a
|
|||||||
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
|
return nil, fmt.Errorf("getting a request OAuth flow info failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go urlOpener.Open(flowInfo.VerificationURIComplete, flowInfo.UserCode)
|
go urlOpener.Open(flowInfo.VerificationURIComplete)
|
||||||
|
|
||||||
waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second
|
waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second
|
||||||
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout)
|
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout)
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package android
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type ResolvedIPs struct {
|
|
||||||
resolvedIPs []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ResolvedIPs) Add(ipAddress string) {
|
|
||||||
r.resolvedIPs = append(r.resolvedIPs, ipAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ResolvedIPs) Get(i int) (string, error) {
|
|
||||||
if i < 0 || i >= len(r.resolvedIPs) {
|
|
||||||
return "", fmt.Errorf("%d is out of range", i)
|
|
||||||
}
|
|
||||||
return r.resolvedIPs[i], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ResolvedIPs) Size() int {
|
|
||||||
return len(r.resolvedIPs)
|
|
||||||
}
|
|
||||||
|
|
||||||
type NetworkDomain struct {
|
|
||||||
Address string
|
|
||||||
resolvedIPs ResolvedIPs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetworkDomain) addResolvedIP(resolvedIP string) {
|
|
||||||
d.resolvedIPs.Add(resolvedIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NetworkDomain) GetResolvedIPs() *ResolvedIPs {
|
|
||||||
return &d.resolvedIPs
|
|
||||||
}
|
|
||||||
|
|
||||||
type NetworkDomains struct {
|
|
||||||
domains []*NetworkDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NetworkDomains) Add(domain *NetworkDomain) {
|
|
||||||
n.domains = append(n.domains, domain)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NetworkDomains) Get(i int) (*NetworkDomain, error) {
|
|
||||||
if i < 0 || i >= len(n.domains) {
|
|
||||||
return nil, fmt.Errorf("%d is out of range", i)
|
|
||||||
}
|
|
||||||
return n.domains[i], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *NetworkDomains) Size() int {
|
|
||||||
return len(n.domains)
|
|
||||||
}
|
|
||||||
@@ -3,16 +3,10 @@
|
|||||||
package android
|
package android
|
||||||
|
|
||||||
type Network struct {
|
type Network struct {
|
||||||
Name string
|
Name string
|
||||||
Network string
|
Network string
|
||||||
Peer string
|
Peer string
|
||||||
Status string
|
Status string
|
||||||
IsSelected bool
|
|
||||||
Domains NetworkDomains
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n Network) GetNetworkDomains() *NetworkDomains {
|
|
||||||
return &n.Domains
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetworkArray struct {
|
type NetworkArray struct {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package android
|
package android
|
||||||
|
|
||||||
// PeerInfo describe information about the peers. It designed for the UI usage
|
// PeerInfo describe information about the peers. It designed for the UI usage
|
||||||
@@ -7,11 +5,6 @@ type PeerInfo struct {
|
|||||||
IP string
|
IP string
|
||||||
FQDN string
|
FQDN string
|
||||||
ConnStatus string // Todo replace to enum
|
ConnStatus string // Todo replace to enum
|
||||||
Routes PeerRoutes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PeerInfo) GetPeerRoutes() *PeerRoutes {
|
|
||||||
return &p.Routes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PeerInfoArray is a wrapper of []PeerInfo
|
// PeerInfoArray is a wrapper of []PeerInfo
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package android
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type PeerRoutes struct {
|
|
||||||
routes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PeerRoutes) Get(i int) (string, error) {
|
|
||||||
if i < 0 || i >= len(p.routes) {
|
|
||||||
return "", fmt.Errorf("%d is out of range", i)
|
|
||||||
}
|
|
||||||
return p.routes[i], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PeerRoutes) Size() int {
|
|
||||||
return len(p.routes)
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package android
|
|
||||||
|
|
||||||
// PlatformFiles groups paths to files used internally by the engine that can't be created/modified
|
|
||||||
// at their default locations due to android OS restrictions.
|
|
||||||
type PlatformFiles interface {
|
|
||||||
ConfigurationFilePath() string
|
|
||||||
StateFilePath() string
|
|
||||||
}
|
|
||||||
@@ -201,94 +201,6 @@ func (p *Preferences) SetServerSSHAllowed(allowed bool) {
|
|||||||
p.configInput.ServerSSHAllowed = &allowed
|
p.configInput.ServerSSHAllowed = &allowed
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEnableSSHRoot reads SSH root login setting from config file
|
|
||||||
func (p *Preferences) GetEnableSSHRoot() (bool, error) {
|
|
||||||
if p.configInput.EnableSSHRoot != nil {
|
|
||||||
return *p.configInput.EnableSSHRoot, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if cfg.EnableSSHRoot == nil {
|
|
||||||
// Default to false for security on Android
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return *cfg.EnableSSHRoot, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEnableSSHRoot stores the given value and waits for commit
|
|
||||||
func (p *Preferences) SetEnableSSHRoot(enabled bool) {
|
|
||||||
p.configInput.EnableSSHRoot = &enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnableSSHSFTP reads SSH SFTP setting from config file
|
|
||||||
func (p *Preferences) GetEnableSSHSFTP() (bool, error) {
|
|
||||||
if p.configInput.EnableSSHSFTP != nil {
|
|
||||||
return *p.configInput.EnableSSHSFTP, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if cfg.EnableSSHSFTP == nil {
|
|
||||||
// Default to false for security on Android
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return *cfg.EnableSSHSFTP, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEnableSSHSFTP stores the given value and waits for commit
|
|
||||||
func (p *Preferences) SetEnableSSHSFTP(enabled bool) {
|
|
||||||
p.configInput.EnableSSHSFTP = &enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnableSSHLocalPortForwarding reads SSH local port forwarding setting from config file
|
|
||||||
func (p *Preferences) GetEnableSSHLocalPortForwarding() (bool, error) {
|
|
||||||
if p.configInput.EnableSSHLocalPortForwarding != nil {
|
|
||||||
return *p.configInput.EnableSSHLocalPortForwarding, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if cfg.EnableSSHLocalPortForwarding == nil {
|
|
||||||
// Default to false for security on Android
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return *cfg.EnableSSHLocalPortForwarding, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEnableSSHLocalPortForwarding stores the given value and waits for commit
|
|
||||||
func (p *Preferences) SetEnableSSHLocalPortForwarding(enabled bool) {
|
|
||||||
p.configInput.EnableSSHLocalPortForwarding = &enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEnableSSHRemotePortForwarding reads SSH remote port forwarding setting from config file
|
|
||||||
func (p *Preferences) GetEnableSSHRemotePortForwarding() (bool, error) {
|
|
||||||
if p.configInput.EnableSSHRemotePortForwarding != nil {
|
|
||||||
return *p.configInput.EnableSSHRemotePortForwarding, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if cfg.EnableSSHRemotePortForwarding == nil {
|
|
||||||
// Default to false for security on Android
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return *cfg.EnableSSHRemotePortForwarding, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEnableSSHRemotePortForwarding stores the given value and waits for commit
|
|
||||||
func (p *Preferences) SetEnableSSHRemotePortForwarding(enabled bool) {
|
|
||||||
p.configInput.EnableSSHRemotePortForwarding = &enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockInbound reads block inbound setting from config file
|
// GetBlockInbound reads block inbound setting from config file
|
||||||
func (p *Preferences) GetBlockInbound() (bool, error) {
|
func (p *Preferences) GetBlockInbound() (bool, error) {
|
||||||
if p.configInput.BlockInbound != nil {
|
if p.configInput.BlockInbound != nil {
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package android
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/exp/maps"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
|
||||||
"github.com/netbirdio/netbird/route"
|
|
||||||
)
|
|
||||||
|
|
||||||
func executeRouteToggle(id string, manager routemanager.Manager,
|
|
||||||
operationName string,
|
|
||||||
routeOperation func(routes []route.NetID, allRoutes []route.NetID) error) error {
|
|
||||||
netID := route.NetID(id)
|
|
||||||
routes := []route.NetID{netID}
|
|
||||||
|
|
||||||
log.Debugf("%s with id: %s", operationName, id)
|
|
||||||
|
|
||||||
if err := routeOperation(routes, maps.Keys(manager.GetClientRoutesWithNetID())); err != nil {
|
|
||||||
log.Debugf("error when %s: %s", operationName, err)
|
|
||||||
return fmt.Errorf("error %s: %w", operationName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.TriggerSelection(manager.GetClientRoutes())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type routeCommand interface {
|
|
||||||
toggleRoute() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type selectRouteCommand struct {
|
|
||||||
route string
|
|
||||||
manager routemanager.Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s selectRouteCommand) toggleRoute() error {
|
|
||||||
routeSelector := s.manager.GetRouteSelector()
|
|
||||||
if routeSelector == nil {
|
|
||||||
return fmt.Errorf("no route selector available")
|
|
||||||
}
|
|
||||||
|
|
||||||
routeOperation := func(routes []route.NetID, allRoutes []route.NetID) error {
|
|
||||||
return routeSelector.SelectRoutes(routes, true, allRoutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
return executeRouteToggle(s.route, s.manager, "selecting route", routeOperation)
|
|
||||||
}
|
|
||||||
|
|
||||||
type deselectRouteCommand struct {
|
|
||||||
route string
|
|
||||||
manager routemanager.Manager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d deselectRouteCommand) toggleRoute() error {
|
|
||||||
routeSelector := d.manager.GetRouteSelector()
|
|
||||||
if routeSelector == nil {
|
|
||||||
return fmt.Errorf("no route selector available")
|
|
||||||
}
|
|
||||||
|
|
||||||
return executeRouteToggle(d.route, d.manager, "deselecting route", routeSelector.DeselectRoutes)
|
|
||||||
}
|
|
||||||
@@ -168,7 +168,7 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
stat, err := client.Status(cmd.Context(), &proto.StatusRequest{ShouldRunProbes: true})
|
stat, err := client.Status(cmd.Context(), &proto.StatusRequest{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get status: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to get status: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
@@ -303,18 +303,12 @@ func setSyncResponsePersistence(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
func getStatusOutput(cmd *cobra.Command, anon bool) string {
|
func getStatusOutput(cmd *cobra.Command, anon bool) string {
|
||||||
var statusOutputString string
|
var statusOutputString string
|
||||||
statusResp, err := getStatus(cmd.Context(), true)
|
statusResp, err := getStatus(cmd.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.PrintErrf("Failed to get status: %v\n", err)
|
cmd.PrintErrf("Failed to get status: %v\n", err)
|
||||||
} else {
|
} else {
|
||||||
pm := profilemanager.NewProfileManager()
|
|
||||||
var profName string
|
|
||||||
if activeProf, err := pm.GetActiveProfile(); err == nil {
|
|
||||||
profName = activeProf.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
statusOutputString = nbstatus.ParseToFullDetailSummary(
|
statusOutputString = nbstatus.ParseToFullDetailSummary(
|
||||||
nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, "", profName),
|
nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, "", ""),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return statusOutputString
|
return statusOutputString
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
// SetupDebugHandler is a no-op for WASM
|
|
||||||
func SetupDebugHandler(context.Context, interface{}, interface{}, interface{}, string) {
|
|
||||||
// Debug handler not needed for WASM
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ var downCmd = &cobra.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*7)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
"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"
|
||||||
@@ -104,13 +105,6 @@ func doDaemonLogin(ctx context.Context, cmd *cobra.Command, providedSetupKey str
|
|||||||
Username: &username,
|
Username: &username,
|
||||||
}
|
}
|
||||||
|
|
||||||
profileState, err := pm.GetProfileState(activeProf.Name)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
|
||||||
} else if profileState.Email != "" {
|
|
||||||
loginRequest.Hint = &profileState.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
loginRequest.OptionalPreSharedKey = &preSharedKey
|
loginRequest.OptionalPreSharedKey = &preSharedKey
|
||||||
}
|
}
|
||||||
@@ -246,7 +240,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
|
|||||||
return fmt.Errorf("read config file %s: %v", configFilePath, err)
|
return fmt.Errorf("read config file %s: %v", configFilePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, setupKey, activeProf.Name)
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -274,7 +268,7 @@ func handleSSOLogin(ctx context.Context, cmd *cobra.Command, loginResp *proto.Lo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, setupKey, profileName string) error {
|
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 {
|
||||||
@@ -291,7 +285,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profileman
|
|||||||
|
|
||||||
jwtToken := ""
|
jwtToken := ""
|
||||||
if setupKey == "" && needsLogin {
|
if setupKey == "" && needsLogin {
|
||||||
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config, profileName)
|
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("interactive sso login failed: %v", err)
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -320,17 +314,8 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *profileman
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config, profileName string) (*auth.TokenInfo, error) {
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *profilemanager.Config) (*auth.TokenInfo, error) {
|
||||||
hint := ""
|
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop())
|
||||||
pm := profilemanager.NewProfileManager()
|
|
||||||
profileState, err := pm.GetProfileState(profileName)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
|
||||||
} else if profileState.Email != "" {
|
|
||||||
hint = profileState.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop(), false, hint)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -371,7 +356,7 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
|
|||||||
cmd.Println("")
|
cmd.Println("")
|
||||||
|
|
||||||
if !noBrowser {
|
if !noBrowser {
|
||||||
if err := util.OpenBrowser(verificationURIComplete); err != nil {
|
if err := open.Run(verificationURIComplete); err != nil {
|
||||||
cmd.Println("\nAlternatively, you may want to use a setup key, see:\n\n" +
|
cmd.Println("\nAlternatively, you may want to use a setup key, see:\n\n" +
|
||||||
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const (
|
|||||||
wireguardPortFlag = "wireguard-port"
|
wireguardPortFlag = "wireguard-port"
|
||||||
networkMonitorFlag = "network-monitor"
|
networkMonitorFlag = "network-monitor"
|
||||||
disableAutoConnectFlag = "disable-auto-connect"
|
disableAutoConnectFlag = "disable-auto-connect"
|
||||||
|
serverSSHAllowedFlag = "allow-server-ssh"
|
||||||
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
||||||
dnsRouteIntervalFlag = "dns-router-interval"
|
dnsRouteIntervalFlag = "dns-router-interval"
|
||||||
enableLazyConnectionFlag = "enable-lazy-connection"
|
enableLazyConnectionFlag = "enable-lazy-connection"
|
||||||
@@ -63,6 +64,7 @@ var (
|
|||||||
customDNSAddress string
|
customDNSAddress string
|
||||||
rosenpassEnabled bool
|
rosenpassEnabled bool
|
||||||
rosenpassPermissive bool
|
rosenpassPermissive bool
|
||||||
|
serverSSHAllowed bool
|
||||||
interfaceName string
|
interfaceName string
|
||||||
wireguardPort uint16
|
wireguardPort uint16
|
||||||
networkMonitor bool
|
networkMonitor bool
|
||||||
@@ -174,6 +176,7 @@ func init() {
|
|||||||
)
|
)
|
||||||
upCmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "[Experimental] Enable Rosenpass feature. If enabled, the connection will be post-quantum secured via Rosenpass.")
|
upCmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "[Experimental] Enable Rosenpass feature. If enabled, the connection will be post-quantum secured via Rosenpass.")
|
||||||
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(&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. Note: this setting may be overridden by management configuration.")
|
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.")
|
||||||
|
|
||||||
@@ -228,7 +231,7 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
|
|||||||
|
|
||||||
// DialClientGRPCServer returns client connection to the daemon server.
|
// DialClientGRPCServer returns client connection to the daemon server.
|
||||||
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return grpc.DialContext(
|
return grpc.DialContext(
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -83,10 +81,6 @@ func configurePlatformSpecificSettings(svcConfig *service.Config) error {
|
|||||||
svcConfig.Option["LogDirectory"] = dir
|
svcConfig.Option["LogDirectory"] = dir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := configureSystemdNetworkd(); err != nil {
|
|
||||||
log.Warnf("failed to configure systemd-networkd: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
@@ -166,12 +160,6 @@ var uninstallCmd = &cobra.Command{
|
|||||||
return fmt.Errorf("uninstall service: %w", err)
|
return fmt.Errorf("uninstall service: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "linux" {
|
|
||||||
if err := cleanupSystemdNetworkd(); err != nil {
|
|
||||||
log.Warnf("failed to cleanup systemd-networkd configuration: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("NetBird service has been uninstalled")
|
cmd.Println("NetBird service has been uninstalled")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -257,50 +245,3 @@ func isServiceRunning() (bool, error) {
|
|||||||
|
|
||||||
return status == service.StatusRunning, nil
|
return status == service.StatusRunning, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
networkdConf = "/etc/systemd/networkd.conf"
|
|
||||||
networkdConfDir = "/etc/systemd/networkd.conf.d"
|
|
||||||
networkdConfFile = "/etc/systemd/networkd.conf.d/99-netbird.conf"
|
|
||||||
networkdConfContent = `# Created by NetBird to prevent systemd-networkd from removing
|
|
||||||
# routes and policy rules managed by NetBird.
|
|
||||||
|
|
||||||
[Network]
|
|
||||||
ManageForeignRoutes=no
|
|
||||||
ManageForeignRoutingPolicyRules=no
|
|
||||||
`
|
|
||||||
)
|
|
||||||
|
|
||||||
// configureSystemdNetworkd creates a drop-in configuration file to prevent
|
|
||||||
// systemd-networkd from removing NetBird's routes and policy rules.
|
|
||||||
func configureSystemdNetworkd() error {
|
|
||||||
if _, err := os.Stat(networkdConf); os.IsNotExist(err) {
|
|
||||||
log.Debug("systemd-networkd not in use, skipping configuration")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint:gosec // standard networkd permissions
|
|
||||||
if err := os.MkdirAll(networkdConfDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("create networkd.conf.d directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// nolint:gosec // standard networkd permissions
|
|
||||||
if err := os.WriteFile(networkdConfFile, []byte(networkdConfContent), 0644); err != nil {
|
|
||||||
return fmt.Errorf("write networkd configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanupSystemdNetworkd removes the NetBird systemd-networkd configuration file.
|
|
||||||
func cleanupSystemdNetworkd() error {
|
|
||||||
if _, err := os.Stat(networkdConfFile); os.IsNotExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Remove(networkdConfFile); err != nil {
|
|
||||||
return fmt.Errorf("remove networkd configuration: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,849 +3,125 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"os/user"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
sshclient "github.com/netbirdio/netbird/client/ssh/client"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/ssh/detection"
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
sshproxy "github.com/netbirdio/netbird/client/ssh/proxy"
|
|
||||||
sshserver "github.com/netbirdio/netbird/client/ssh/server"
|
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
sshUsernameDesc = "SSH username"
|
|
||||||
hostArgumentRequired = "host argument required"
|
|
||||||
|
|
||||||
serverSSHAllowedFlag = "allow-server-ssh"
|
|
||||||
enableSSHRootFlag = "enable-ssh-root"
|
|
||||||
enableSSHSFTPFlag = "enable-ssh-sftp"
|
|
||||||
enableSSHLocalPortForwardFlag = "enable-ssh-local-port-forwarding"
|
|
||||||
enableSSHRemotePortForwardFlag = "enable-ssh-remote-port-forwarding"
|
|
||||||
disableSSHAuthFlag = "disable-ssh-auth"
|
|
||||||
sshJWTCacheTTLFlag = "ssh-jwt-cache-ttl"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
port int
|
port int
|
||||||
username string
|
userName = "root"
|
||||||
host string
|
host string
|
||||||
command string
|
|
||||||
localForwards []string
|
|
||||||
remoteForwards []string
|
|
||||||
strictHostKeyChecking bool
|
|
||||||
knownHostsFile string
|
|
||||||
identityFile string
|
|
||||||
skipCachedToken bool
|
|
||||||
requestPTY bool
|
|
||||||
sshNoBrowser bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
serverSSHAllowed bool
|
|
||||||
enableSSHRoot bool
|
|
||||||
enableSSHSFTP bool
|
|
||||||
enableSSHLocalPortForward bool
|
|
||||||
enableSSHRemotePortForward bool
|
|
||||||
disableSSHAuth bool
|
|
||||||
sshJWTCacheTTL int
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer")
|
|
||||||
upCmd.PersistentFlags().BoolVar(&enableSSHRoot, enableSSHRootFlag, false, "Enable root login for SSH server")
|
|
||||||
upCmd.PersistentFlags().BoolVar(&enableSSHSFTP, enableSSHSFTPFlag, false, "Enable SFTP subsystem for SSH server")
|
|
||||||
upCmd.PersistentFlags().BoolVar(&enableSSHLocalPortForward, enableSSHLocalPortForwardFlag, false, "Enable local port forwarding for SSH server")
|
|
||||||
upCmd.PersistentFlags().BoolVar(&enableSSHRemotePortForward, enableSSHRemotePortForwardFlag, false, "Enable remote port forwarding for SSH server")
|
|
||||||
upCmd.PersistentFlags().BoolVar(&disableSSHAuth, disableSSHAuthFlag, false, "Disable SSH authentication")
|
|
||||||
upCmd.PersistentFlags().IntVar(&sshJWTCacheTTL, sshJWTCacheTTLFlag, 0, "SSH JWT token cache TTL in seconds (0=disabled)")
|
|
||||||
|
|
||||||
sshCmd.PersistentFlags().IntVarP(&port, "port", "p", sshserver.DefaultSSHPort, "Remote SSH port")
|
|
||||||
sshCmd.PersistentFlags().StringVarP(&username, "user", "u", "", sshUsernameDesc)
|
|
||||||
sshCmd.PersistentFlags().StringVar(&username, "login", "", sshUsernameDesc+" (alias for --user)")
|
|
||||||
sshCmd.PersistentFlags().BoolVarP(&requestPTY, "tty", "t", false, "Force pseudo-terminal allocation")
|
|
||||||
sshCmd.PersistentFlags().BoolVar(&strictHostKeyChecking, "strict-host-key-checking", true, "Enable strict host key checking (default: true)")
|
|
||||||
sshCmd.PersistentFlags().StringVarP(&knownHostsFile, "known-hosts", "o", "", "Path to known_hosts file (default: ~/.ssh/known_hosts)")
|
|
||||||
sshCmd.PersistentFlags().StringVarP(&identityFile, "identity", "i", "", "Path to SSH private key file (deprecated)")
|
|
||||||
_ = sshCmd.PersistentFlags().MarkDeprecated("identity", "this flag is no longer used")
|
|
||||||
sshCmd.PersistentFlags().BoolVar(&skipCachedToken, "no-cache", false, "Skip cached JWT token and force fresh authentication")
|
|
||||||
sshCmd.PersistentFlags().BoolVar(&sshNoBrowser, noBrowserFlag, false, noBrowserDesc)
|
|
||||||
|
|
||||||
sshCmd.PersistentFlags().StringArrayP("L", "L", []string{}, "Local port forwarding [bind_address:]port:host:hostport")
|
|
||||||
sshCmd.PersistentFlags().StringArrayP("R", "R", []string{}, "Remote port forwarding [bind_address:]port:host:hostport")
|
|
||||||
|
|
||||||
sshCmd.AddCommand(sshSftpCmd)
|
|
||||||
sshCmd.AddCommand(sshProxyCmd)
|
|
||||||
sshCmd.AddCommand(sshDetectCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sshCmd = &cobra.Command{
|
var sshCmd = &cobra.Command{
|
||||||
Use: "ssh [flags] [user@]host [command]",
|
Use: "ssh [user@]host",
|
||||||
Short: "Connect to a NetBird peer via SSH",
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
Long: `Connect to a NetBird peer using SSH with support for port forwarding.
|
if len(args) < 1 {
|
||||||
|
return errors.New("requires a host argument")
|
||||||
Port Forwarding:
|
|
||||||
-L [bind_address:]port:host:hostport Local port forwarding
|
|
||||||
-L [bind_address:]port:/path/to/socket Local port forwarding to Unix socket
|
|
||||||
-R [bind_address:]port:host:hostport Remote port forwarding
|
|
||||||
-R [bind_address:]port:/path/to/socket Remote port forwarding to Unix socket
|
|
||||||
|
|
||||||
SSH Options:
|
|
||||||
-p, --port int Remote SSH port (default 22)
|
|
||||||
-u, --user string SSH username
|
|
||||||
--login string SSH username (alias for --user)
|
|
||||||
-t, --tty Force pseudo-terminal allocation
|
|
||||||
--strict-host-key-checking Enable strict host key checking (default: true)
|
|
||||||
-o, --known-hosts string Path to known_hosts file
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
netbird ssh peer-hostname
|
|
||||||
netbird ssh root@peer-hostname
|
|
||||||
netbird ssh --login root peer-hostname
|
|
||||||
netbird ssh peer-hostname ls -la
|
|
||||||
netbird ssh peer-hostname whoami
|
|
||||||
netbird ssh -t peer-hostname tmux # Force PTY for tmux/screen
|
|
||||||
netbird ssh -t peer-hostname sudo -i # Force PTY for interactive sudo
|
|
||||||
netbird ssh -L 8080:localhost:80 peer-hostname # Local port forwarding
|
|
||||||
netbird ssh -R 9090:localhost:3000 peer-hostname # Remote port forwarding
|
|
||||||
netbird ssh -L "*:8080:localhost:80" peer-hostname # Bind to all interfaces
|
|
||||||
netbird ssh -L 8080:/tmp/socket peer-hostname # Unix socket forwarding`,
|
|
||||||
DisableFlagParsing: true,
|
|
||||||
Args: validateSSHArgsWithoutFlagParsing,
|
|
||||||
RunE: sshFn,
|
|
||||||
Aliases: []string{"ssh"},
|
|
||||||
}
|
|
||||||
|
|
||||||
func sshFn(cmd *cobra.Command, args []string) error {
|
|
||||||
for _, arg := range args {
|
|
||||||
if arg == "-h" || arg == "--help" {
|
|
||||||
return cmd.Help()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
split := strings.Split(args[0], "@")
|
||||||
SetFlagsFromEnvVars(cmd)
|
if len(split) == 2 {
|
||||||
|
userName = split[0]
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
host = split[1]
|
||||||
|
|
||||||
logOutput := "console"
|
|
||||||
if firstLogFile := util.FindFirstLogPath(logFiles); firstLogFile != "" && firstLogFile != defaultLogFile {
|
|
||||||
logOutput = firstLogFile
|
|
||||||
}
|
|
||||||
if err := util.InitLog(logLevel, logOutput); err != nil {
|
|
||||||
return fmt.Errorf("init log: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := internal.CtxInitState(cmd.Context())
|
|
||||||
|
|
||||||
sig := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
|
|
||||||
sshctx, cancel := context.WithCancel(ctx)
|
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
if err := runSSH(sshctx, host, cmd); err != nil {
|
|
||||||
errCh <- err
|
|
||||||
}
|
|
||||||
cancel()
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-sig:
|
|
||||||
cancel()
|
|
||||||
<-sshctx.Done()
|
|
||||||
return nil
|
|
||||||
case err := <-errCh:
|
|
||||||
return err
|
|
||||||
case <-sshctx.Done():
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getEnvOrDefault checks for environment variables with WT_ and NB_ prefixes
|
|
||||||
func getEnvOrDefault(flagName, defaultValue string) string {
|
|
||||||
if envValue := os.Getenv("WT_" + flagName); envValue != "" {
|
|
||||||
return envValue
|
|
||||||
}
|
|
||||||
if envValue := os.Getenv("NB_" + flagName); envValue != "" {
|
|
||||||
return envValue
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// getBoolEnvOrDefault checks for boolean environment variables with WT_ and NB_ prefixes
|
|
||||||
func getBoolEnvOrDefault(flagName string, defaultValue bool) bool {
|
|
||||||
if envValue := os.Getenv("WT_" + flagName); envValue != "" {
|
|
||||||
if parsed, err := strconv.ParseBool(envValue); err == nil {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if envValue := os.Getenv("NB_" + flagName); envValue != "" {
|
|
||||||
if parsed, err := strconv.ParseBool(envValue); err == nil {
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// resetSSHGlobals sets SSH globals to their default values
|
|
||||||
func resetSSHGlobals() {
|
|
||||||
port = sshserver.DefaultSSHPort
|
|
||||||
username = ""
|
|
||||||
host = ""
|
|
||||||
command = ""
|
|
||||||
localForwards = nil
|
|
||||||
remoteForwards = nil
|
|
||||||
strictHostKeyChecking = true
|
|
||||||
knownHostsFile = ""
|
|
||||||
identityFile = ""
|
|
||||||
sshNoBrowser = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseCustomSSHFlags extracts -L, -R flags and returns filtered args
|
|
||||||
func parseCustomSSHFlags(args []string) ([]string, []string, []string) {
|
|
||||||
var localForwardFlags []string
|
|
||||||
var remoteForwardFlags []string
|
|
||||||
var filteredArgs []string
|
|
||||||
|
|
||||||
for i := 0; i < len(args); i++ {
|
|
||||||
arg := args[i]
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(arg, "-L"):
|
|
||||||
localForwardFlags, i = parseForwardFlag(arg, args, i, localForwardFlags)
|
|
||||||
case strings.HasPrefix(arg, "-R"):
|
|
||||||
remoteForwardFlags, i = parseForwardFlag(arg, args, i, remoteForwardFlags)
|
|
||||||
default:
|
|
||||||
filteredArgs = append(filteredArgs, arg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredArgs, localForwardFlags, remoteForwardFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseForwardFlag(arg string, args []string, i int, flags []string) ([]string, int) {
|
|
||||||
if arg == "-L" || arg == "-R" {
|
|
||||||
if i+1 < len(args) {
|
|
||||||
flags = append(flags, args[i+1])
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
} else if len(arg) > 2 {
|
|
||||||
flags = append(flags, arg[2:])
|
|
||||||
}
|
|
||||||
return flags, i
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractGlobalFlags parses global flags that were passed before 'ssh' command
|
|
||||||
func extractGlobalFlags(args []string) {
|
|
||||||
sshPos := findSSHCommandPosition(args)
|
|
||||||
if sshPos == -1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
globalArgs := args[:sshPos]
|
|
||||||
parseGlobalArgs(globalArgs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// findSSHCommandPosition locates the 'ssh' command in the argument list
|
|
||||||
func findSSHCommandPosition(args []string) int {
|
|
||||||
for i, arg := range args {
|
|
||||||
if arg == "ssh" {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
configFlag = "config"
|
|
||||||
logLevelFlag = "log-level"
|
|
||||||
logFileFlag = "log-file"
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseGlobalArgs processes the global arguments and sets the corresponding variables
|
|
||||||
func parseGlobalArgs(globalArgs []string) {
|
|
||||||
flagHandlers := map[string]func(string){
|
|
||||||
configFlag: func(value string) { configPath = value },
|
|
||||||
logLevelFlag: func(value string) { logLevel = value },
|
|
||||||
logFileFlag: func(value string) {
|
|
||||||
if !slices.Contains(logFiles, value) {
|
|
||||||
logFiles = append(logFiles, value)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
shortFlags := map[string]string{
|
|
||||||
"c": configFlag,
|
|
||||||
"l": logLevelFlag,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(globalArgs); i++ {
|
|
||||||
arg := globalArgs[i]
|
|
||||||
|
|
||||||
if handled, nextIndex := parseFlag(arg, globalArgs, i, flagHandlers, shortFlags); handled {
|
|
||||||
i = nextIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseFlag handles generic flag parsing for both long and short forms
|
|
||||||
func parseFlag(arg string, args []string, currentIndex int, flagHandlers map[string]func(string), shortFlags map[string]string) (bool, int) {
|
|
||||||
if parsedValue, found := parseEqualsFormat(arg, flagHandlers, shortFlags); found {
|
|
||||||
flagHandlers[parsedValue.flagName](parsedValue.value)
|
|
||||||
return true, currentIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
if parsedValue, found := parseSpacedFormat(arg, args, currentIndex, flagHandlers, shortFlags); found {
|
|
||||||
flagHandlers[parsedValue.flagName](parsedValue.value)
|
|
||||||
return true, currentIndex + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, currentIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
type parsedFlag struct {
|
|
||||||
flagName string
|
|
||||||
value string
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseEqualsFormat handles --flag=value and -f=value formats
|
|
||||||
func parseEqualsFormat(arg string, flagHandlers map[string]func(string), shortFlags map[string]string) (parsedFlag, bool) {
|
|
||||||
if !strings.Contains(arg, "=") {
|
|
||||||
return parsedFlag{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.SplitN(arg, "=", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return parsedFlag{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(parts[0], "--") {
|
|
||||||
flagName := strings.TrimPrefix(parts[0], "--")
|
|
||||||
if _, exists := flagHandlers[flagName]; exists {
|
|
||||||
return parsedFlag{flagName: flagName, value: parts[1]}, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(parts[0], "-") && len(parts[0]) == 2 {
|
|
||||||
shortFlag := strings.TrimPrefix(parts[0], "-")
|
|
||||||
if longFlag, exists := shortFlags[shortFlag]; exists {
|
|
||||||
if _, exists := flagHandlers[longFlag]; exists {
|
|
||||||
return parsedFlag{flagName: longFlag, value: parts[1]}, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedFlag{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseSpacedFormat handles --flag value and -f value formats
|
|
||||||
func parseSpacedFormat(arg string, args []string, currentIndex int, flagHandlers map[string]func(string), shortFlags map[string]string) (parsedFlag, bool) {
|
|
||||||
if currentIndex+1 >= len(args) {
|
|
||||||
return parsedFlag{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(arg, "--") {
|
|
||||||
flagName := strings.TrimPrefix(arg, "--")
|
|
||||||
if _, exists := flagHandlers[flagName]; exists {
|
|
||||||
return parsedFlag{flagName: flagName, value: args[currentIndex+1]}, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(arg, "-") && len(arg) == 2 {
|
|
||||||
shortFlag := strings.TrimPrefix(arg, "-")
|
|
||||||
if longFlag, exists := shortFlags[shortFlag]; exists {
|
|
||||||
if _, exists := flagHandlers[longFlag]; exists {
|
|
||||||
return parsedFlag{flagName: longFlag, value: args[currentIndex+1]}, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedFlag{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// createSSHFlagSet creates and configures the flag set for SSH command parsing
|
|
||||||
// sshFlags contains all SSH-related flags and parameters
|
|
||||||
type sshFlags struct {
|
|
||||||
Port int
|
|
||||||
Username string
|
|
||||||
Login string
|
|
||||||
RequestPTY bool
|
|
||||||
StrictHostKeyChecking bool
|
|
||||||
KnownHostsFile string
|
|
||||||
IdentityFile string
|
|
||||||
SkipCachedToken bool
|
|
||||||
NoBrowser bool
|
|
||||||
ConfigPath string
|
|
||||||
LogLevel string
|
|
||||||
LocalForwards []string
|
|
||||||
RemoteForwards []string
|
|
||||||
Host string
|
|
||||||
Command string
|
|
||||||
}
|
|
||||||
|
|
||||||
func createSSHFlagSet() (*flag.FlagSet, *sshFlags) {
|
|
||||||
defaultConfigPath := getEnvOrDefault("CONFIG", configPath)
|
|
||||||
defaultLogLevel := getEnvOrDefault("LOG_LEVEL", logLevel)
|
|
||||||
defaultNoBrowser := getBoolEnvOrDefault("NO_BROWSER", false)
|
|
||||||
|
|
||||||
fs := flag.NewFlagSet("ssh-flags", flag.ContinueOnError)
|
|
||||||
fs.SetOutput(nil)
|
|
||||||
|
|
||||||
flags := &sshFlags{}
|
|
||||||
|
|
||||||
fs.IntVar(&flags.Port, "p", sshserver.DefaultSSHPort, "SSH port")
|
|
||||||
fs.IntVar(&flags.Port, "port", sshserver.DefaultSSHPort, "SSH port")
|
|
||||||
fs.StringVar(&flags.Username, "u", "", sshUsernameDesc)
|
|
||||||
fs.StringVar(&flags.Username, "user", "", sshUsernameDesc)
|
|
||||||
fs.StringVar(&flags.Login, "login", "", sshUsernameDesc+" (alias for --user)")
|
|
||||||
fs.BoolVar(&flags.RequestPTY, "t", false, "Force pseudo-terminal allocation")
|
|
||||||
fs.BoolVar(&flags.RequestPTY, "tty", false, "Force pseudo-terminal allocation")
|
|
||||||
|
|
||||||
fs.BoolVar(&flags.StrictHostKeyChecking, "strict-host-key-checking", true, "Enable strict host key checking")
|
|
||||||
fs.StringVar(&flags.KnownHostsFile, "o", "", "Path to known_hosts file")
|
|
||||||
fs.StringVar(&flags.KnownHostsFile, "known-hosts", "", "Path to known_hosts file")
|
|
||||||
fs.StringVar(&flags.IdentityFile, "i", "", "Path to SSH private key file")
|
|
||||||
fs.StringVar(&flags.IdentityFile, "identity", "", "Path to SSH private key file")
|
|
||||||
fs.BoolVar(&flags.SkipCachedToken, "no-cache", false, "Skip cached JWT token and force fresh authentication")
|
|
||||||
fs.BoolVar(&flags.NoBrowser, "no-browser", defaultNoBrowser, noBrowserDesc)
|
|
||||||
|
|
||||||
fs.StringVar(&flags.ConfigPath, "c", defaultConfigPath, "Netbird config file location")
|
|
||||||
fs.StringVar(&flags.ConfigPath, "config", defaultConfigPath, "Netbird config file location")
|
|
||||||
fs.StringVar(&flags.LogLevel, "l", defaultLogLevel, "sets Netbird log level")
|
|
||||||
fs.StringVar(&flags.LogLevel, "log-level", defaultLogLevel, "sets Netbird log level")
|
|
||||||
|
|
||||||
return fs, flags
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateSSHArgsWithoutFlagParsing(_ *cobra.Command, args []string) error {
|
|
||||||
if len(args) < 1 {
|
|
||||||
return errors.New(hostArgumentRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
resetSSHGlobals()
|
|
||||||
|
|
||||||
if len(os.Args) > 2 {
|
|
||||||
extractGlobalFlags(os.Args[1:])
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredArgs, localForwardFlags, remoteForwardFlags := parseCustomSSHFlags(args)
|
|
||||||
|
|
||||||
fs, flags := createSSHFlagSet()
|
|
||||||
|
|
||||||
if err := fs.Parse(filteredArgs); err != nil {
|
|
||||||
if errors.Is(err, flag.ErrHelp) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
remaining := fs.Args()
|
|
||||||
if len(remaining) < 1 {
|
|
||||||
return errors.New(hostArgumentRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
port = flags.Port
|
|
||||||
if flags.Username != "" {
|
|
||||||
username = flags.Username
|
|
||||||
} else if flags.Login != "" {
|
|
||||||
username = flags.Login
|
|
||||||
}
|
|
||||||
|
|
||||||
requestPTY = flags.RequestPTY
|
|
||||||
strictHostKeyChecking = flags.StrictHostKeyChecking
|
|
||||||
knownHostsFile = flags.KnownHostsFile
|
|
||||||
identityFile = flags.IdentityFile
|
|
||||||
skipCachedToken = flags.SkipCachedToken
|
|
||||||
sshNoBrowser = flags.NoBrowser
|
|
||||||
|
|
||||||
if flags.ConfigPath != getEnvOrDefault("CONFIG", configPath) {
|
|
||||||
configPath = flags.ConfigPath
|
|
||||||
}
|
|
||||||
if flags.LogLevel != getEnvOrDefault("LOG_LEVEL", logLevel) {
|
|
||||||
logLevel = flags.LogLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
localForwards = localForwardFlags
|
|
||||||
remoteForwards = remoteForwardFlags
|
|
||||||
|
|
||||||
return parseHostnameAndCommand(remaining)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseHostnameAndCommand(args []string) error {
|
|
||||||
if len(args) < 1 {
|
|
||||||
return errors.New(hostArgumentRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
arg := args[0]
|
|
||||||
if strings.Contains(arg, "@") {
|
|
||||||
parts := strings.SplitN(arg, "@", 2)
|
|
||||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
||||||
return errors.New("invalid user@host format")
|
|
||||||
}
|
|
||||||
if username == "" {
|
|
||||||
username = parts[0]
|
|
||||||
}
|
|
||||||
host = parts[1]
|
|
||||||
} else {
|
|
||||||
host = arg
|
|
||||||
}
|
|
||||||
|
|
||||||
if username == "" {
|
|
||||||
if sudoUser := os.Getenv("SUDO_USER"); sudoUser != "" {
|
|
||||||
username = sudoUser
|
|
||||||
} else if currentUser, err := user.Current(); err == nil {
|
|
||||||
username = currentUser.Username
|
|
||||||
} else {
|
} else {
|
||||||
username = "root"
|
host = args[0]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Everything after hostname becomes the command
|
return nil
|
||||||
if len(args) > 1 {
|
},
|
||||||
command = strings.Join(args[1:], " ")
|
Short: "Connect to a remote SSH server",
|
||||||
}
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
SetFlagsFromEnvVars(cmd)
|
||||||
|
|
||||||
return nil
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := util.InitLog(logLevel, util.LogConsole)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !util.IsAdmin() {
|
||||||
|
cmd.Printf("error: you must have Administrator privileges to run this command\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(cmd.Context())
|
||||||
|
|
||||||
|
sm := profilemanager.NewServiceManager(configPath)
|
||||||
|
activeProf, err := sm.GetActiveProfileState()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get active profile: %v", err)
|
||||||
|
}
|
||||||
|
profPath, err := activeProf.FilePath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get active profile path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := profilemanager.ReadConfig(profPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read profile config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
sshctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// blocking
|
||||||
|
if err := runSSH(sshctx, host, []byte(config.SSHKey), cmd); err != nil {
|
||||||
|
cmd.Printf("Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sig:
|
||||||
|
cancel()
|
||||||
|
case <-sshctx.Done():
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSSH(ctx context.Context, addr string, cmd *cobra.Command) error {
|
func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command) error {
|
||||||
target := fmt.Sprintf("%s:%d", addr, port)
|
c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), userName, pemKey)
|
||||||
c, err := sshclient.Dial(ctx, target, username, sshclient.DialOptions{
|
|
||||||
KnownHostsFile: knownHostsFile,
|
|
||||||
IdentityFile: identityFile,
|
|
||||||
DaemonAddr: daemonAddr,
|
|
||||||
SkipCachedToken: skipCachedToken,
|
|
||||||
InsecureSkipVerify: !strictHostKeyChecking,
|
|
||||||
NoBrowser: sshNoBrowser,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.Printf("Failed to connect to %s@%s\n", username, target)
|
cmd.Printf("Error: %v\n", err)
|
||||||
cmd.Printf("\nTroubleshooting steps:\n")
|
cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" +
|
||||||
cmd.Printf(" 1. Check peer connectivity: netbird status -d\n")
|
"\nYou can verify the connection by running:\n\n" +
|
||||||
cmd.Printf(" 2. Verify SSH server is enabled on the peer\n")
|
" netbird status\n\n")
|
||||||
cmd.Printf(" 3. Ensure correct hostname/IP is used\n")
|
return err
|
||||||
return fmt.Errorf("dial %s: %w", target, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sshCtx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-sshCtx.Done()
|
<-ctx.Done()
|
||||||
if err := c.Close(); err != nil {
|
err = c.Close()
|
||||||
cmd.Printf("Error closing SSH connection: %v\n", err)
|
if err != nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := startPortForwarding(sshCtx, c, cmd); err != nil {
|
err = c.OpenTerminal()
|
||||||
return fmt.Errorf("start port forwarding: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if command != "" {
|
|
||||||
return executeSSHCommand(sshCtx, c, command)
|
|
||||||
}
|
|
||||||
return openSSHTerminal(sshCtx, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// executeSSHCommand executes a command over SSH.
|
|
||||||
func executeSSHCommand(ctx context.Context, c *sshclient.Client, command string) error {
|
|
||||||
var err error
|
|
||||||
if requestPTY {
|
|
||||||
err = c.ExecuteCommandWithPTY(ctx, command)
|
|
||||||
} else {
|
|
||||||
err = c.ExecuteCommandWithIO(ctx, command)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitErr *ssh.ExitError
|
|
||||||
if errors.As(err, &exitErr) {
|
|
||||||
os.Exit(exitErr.ExitStatus())
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitMissingErr *ssh.ExitMissingError
|
|
||||||
if errors.As(err, &exitMissingErr) {
|
|
||||||
log.Debugf("Remote command exited without exit status: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("execute command: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// openSSHTerminal opens an interactive SSH terminal.
|
|
||||||
func openSSHTerminal(ctx context.Context, c *sshclient.Client) error {
|
|
||||||
if err := c.OpenTerminal(ctx); err != nil {
|
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var exitMissingErr *ssh.ExitMissingError
|
|
||||||
if errors.As(err, &exitMissingErr) {
|
|
||||||
log.Debugf("Remote terminal exited without exit status: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("open terminal: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// startPortForwarding starts local and remote port forwarding based on command line flags
|
|
||||||
func startPortForwarding(ctx context.Context, c *sshclient.Client, cmd *cobra.Command) error {
|
|
||||||
for _, forward := range localForwards {
|
|
||||||
if err := parseAndStartLocalForward(ctx, c, forward, cmd); err != nil {
|
|
||||||
return fmt.Errorf("local port forward %s: %w", forward, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, forward := range remoteForwards {
|
|
||||||
if err := parseAndStartRemoteForward(ctx, c, forward, cmd); err != nil {
|
|
||||||
return fmt.Errorf("remote port forward %s: %w", forward, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseAndStartLocalForward parses and starts a local port forward (-L)
|
|
||||||
func parseAndStartLocalForward(ctx context.Context, c *sshclient.Client, forward string, cmd *cobra.Command) error {
|
|
||||||
localAddr, remoteAddr, err := parsePortForwardSpec(forward)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Printf("Local port forwarding: %s -> %s\n", localAddr, remoteAddr)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := c.LocalPortForward(ctx, localAddr, remoteAddr); err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
cmd.Printf("Local port forward error: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseAndStartRemoteForward parses and starts a remote port forward (-R)
|
func init() {
|
||||||
func parseAndStartRemoteForward(ctx context.Context, c *sshclient.Client, forward string, cmd *cobra.Command) error {
|
sshCmd.PersistentFlags().IntVarP(&port, "port", "p", nbssh.DefaultSSHPort, "Sets remote SSH port. Defaults to "+fmt.Sprint(nbssh.DefaultSSHPort))
|
||||||
remoteAddr, localAddr, err := parsePortForwardSpec(forward)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("Remote port forwarding: %s -> %s\n", remoteAddr, localAddr)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := c.RemotePortForward(ctx, remoteAddr, localAddr); err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
cmd.Printf("Remote port forward error: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parsePortForwardSpec parses port forward specifications like "8080:localhost:80" or "[::1]:8080:localhost:80".
|
|
||||||
// Also supports Unix sockets like "8080:/tmp/socket" or "127.0.0.1:8080:/tmp/socket".
|
|
||||||
func parsePortForwardSpec(spec string) (string, string, error) {
|
|
||||||
// Support formats:
|
|
||||||
// port:host:hostport -> localhost:port -> host:hostport
|
|
||||||
// host:port:host:hostport -> host:port -> host:hostport
|
|
||||||
// [host]:port:host:hostport -> [host]:port -> host:hostport
|
|
||||||
// port:unix_socket_path -> localhost:port -> unix_socket_path
|
|
||||||
// host:port:unix_socket_path -> host:port -> unix_socket_path
|
|
||||||
|
|
||||||
if strings.HasPrefix(spec, "[") && strings.Contains(spec, "]:") {
|
|
||||||
return parseIPv6ForwardSpec(spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(spec, ":")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return "", "", fmt.Errorf("invalid port forward specification: %s (expected format: [local_host:]local_port:remote_target)", spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch len(parts) {
|
|
||||||
case 2:
|
|
||||||
return parseTwoPartForwardSpec(parts, spec)
|
|
||||||
case 3:
|
|
||||||
return parseThreePartForwardSpec(parts)
|
|
||||||
case 4:
|
|
||||||
return parseFourPartForwardSpec(parts)
|
|
||||||
default:
|
|
||||||
return "", "", fmt.Errorf("invalid port forward specification: %s", spec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseTwoPartForwardSpec handles "port:unix_socket" format.
|
|
||||||
func parseTwoPartForwardSpec(parts []string, spec string) (string, string, error) {
|
|
||||||
if isUnixSocket(parts[1]) {
|
|
||||||
localAddr := "localhost:" + parts[0]
|
|
||||||
remoteAddr := parts[1]
|
|
||||||
return localAddr, remoteAddr, nil
|
|
||||||
}
|
|
||||||
return "", "", fmt.Errorf("invalid port forward specification: %s (expected format: [local_host:]local_port:remote_host:remote_port or [local_host:]local_port:unix_socket)", spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseThreePartForwardSpec handles "port:host:hostport" or "host:port:unix_socket" formats.
|
|
||||||
func parseThreePartForwardSpec(parts []string) (string, string, error) {
|
|
||||||
if isUnixSocket(parts[2]) {
|
|
||||||
localHost := normalizeLocalHost(parts[0])
|
|
||||||
localAddr := localHost + ":" + parts[1]
|
|
||||||
remoteAddr := parts[2]
|
|
||||||
return localAddr, remoteAddr, nil
|
|
||||||
}
|
|
||||||
localAddr := "localhost:" + parts[0]
|
|
||||||
remoteAddr := parts[1] + ":" + parts[2]
|
|
||||||
return localAddr, remoteAddr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseFourPartForwardSpec handles "host:port:host:hostport" format.
|
|
||||||
func parseFourPartForwardSpec(parts []string) (string, string, error) {
|
|
||||||
localHost := normalizeLocalHost(parts[0])
|
|
||||||
localAddr := localHost + ":" + parts[1]
|
|
||||||
remoteAddr := parts[2] + ":" + parts[3]
|
|
||||||
return localAddr, remoteAddr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseIPv6ForwardSpec handles "[host]:port:host:hostport" format.
|
|
||||||
func parseIPv6ForwardSpec(spec string) (string, string, error) {
|
|
||||||
idx := strings.Index(spec, "]:")
|
|
||||||
if idx == -1 {
|
|
||||||
return "", "", fmt.Errorf("invalid IPv6 port forward specification: %s", spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipv6Host := spec[:idx+1]
|
|
||||||
remaining := spec[idx+2:]
|
|
||||||
|
|
||||||
parts := strings.Split(remaining, ":")
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return "", "", fmt.Errorf("invalid IPv6 port forward specification: %s (expected [ipv6]:port:host:hostport)", spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
localAddr := ipv6Host + ":" + parts[0]
|
|
||||||
remoteAddr := parts[1] + ":" + parts[2]
|
|
||||||
return localAddr, remoteAddr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isUnixSocket checks if a path is a Unix socket path.
|
|
||||||
func isUnixSocket(path string) bool {
|
|
||||||
return strings.HasPrefix(path, "/") || strings.HasPrefix(path, "./")
|
|
||||||
}
|
|
||||||
|
|
||||||
// normalizeLocalHost converts "*" to "0.0.0.0" for binding to all interfaces.
|
|
||||||
func normalizeLocalHost(host string) string {
|
|
||||||
if host == "*" {
|
|
||||||
return "0.0.0.0"
|
|
||||||
}
|
|
||||||
return host
|
|
||||||
}
|
|
||||||
|
|
||||||
var sshProxyCmd = &cobra.Command{
|
|
||||||
Use: "proxy <host> <port>",
|
|
||||||
Short: "Internal SSH proxy for native SSH client integration",
|
|
||||||
Long: "Internal command used by SSH ProxyCommand to handle JWT authentication",
|
|
||||||
Hidden: true,
|
|
||||||
Args: cobra.ExactArgs(2),
|
|
||||||
RunE: sshProxyFn,
|
|
||||||
}
|
|
||||||
|
|
||||||
func sshProxyFn(cmd *cobra.Command, args []string) error {
|
|
||||||
logOutput := "console"
|
|
||||||
if firstLogFile := util.FindFirstLogPath(logFiles); firstLogFile != "" && firstLogFile != defaultLogFile {
|
|
||||||
logOutput = firstLogFile
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyLogLevel := getEnvOrDefault("LOG_LEVEL", logLevel)
|
|
||||||
if err := util.InitLog(proxyLogLevel, logOutput); err != nil {
|
|
||||||
return fmt.Errorf("init log: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
host := args[0]
|
|
||||||
portStr := args[1]
|
|
||||||
|
|
||||||
port, err := strconv.Atoi(portStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid port: %s", portStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check env var for browser setting since this command is invoked via SSH ProxyCommand
|
|
||||||
// where command-line flags cannot be passed. Default is to open browser.
|
|
||||||
noBrowser := getBoolEnvOrDefault("NO_BROWSER", false)
|
|
||||||
var browserOpener func(string) error
|
|
||||||
if !noBrowser {
|
|
||||||
browserOpener = util.OpenBrowser
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy, err := sshproxy.New(daemonAddr, host, port, cmd.ErrOrStderr(), browserOpener)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create SSH proxy: %w", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := proxy.Close(); err != nil {
|
|
||||||
log.Debugf("close SSH proxy: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := proxy.Connect(cmd.Context()); err != nil {
|
|
||||||
return fmt.Errorf("SSH proxy: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var sshDetectCmd = &cobra.Command{
|
|
||||||
Use: "detect <host> <port>",
|
|
||||||
Short: "Detect if a host is running NetBird SSH",
|
|
||||||
Long: "Internal command used by SSH Match exec to detect NetBird SSH servers. Exit codes: 0=JWT, 1=no-JWT, 2=regular SSH",
|
|
||||||
Hidden: true,
|
|
||||||
Args: cobra.ExactArgs(2),
|
|
||||||
RunE: sshDetectFn,
|
|
||||||
}
|
|
||||||
|
|
||||||
func sshDetectFn(cmd *cobra.Command, args []string) error {
|
|
||||||
detectLogLevel := getEnvOrDefault("LOG_LEVEL", logLevel)
|
|
||||||
if err := util.InitLog(detectLogLevel, "console"); err != nil {
|
|
||||||
os.Exit(detection.ServerTypeRegular.ExitCode())
|
|
||||||
}
|
|
||||||
|
|
||||||
host := args[0]
|
|
||||||
portStr := args[1]
|
|
||||||
|
|
||||||
port, err := strconv.Atoi(portStr)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("invalid port %q: %v", portStr, err)
|
|
||||||
os.Exit(detection.ServerTypeRegular.ExitCode())
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(cmd.Context(), detection.DefaultTimeout)
|
|
||||||
|
|
||||||
dialer := &net.Dialer{}
|
|
||||||
serverType, err := detection.DetectSSHServerType(ctx, dialer, host, port)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("SSH server detection failed: %v", err)
|
|
||||||
cancel()
|
|
||||||
os.Exit(detection.ServerTypeRegular.ExitCode())
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
os.Exit(serverType.ExitCode())
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
//go:build unix
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
sshserver "github.com/netbirdio/netbird/client/ssh/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
sshExecUID uint32
|
|
||||||
sshExecGID uint32
|
|
||||||
sshExecGroups []uint
|
|
||||||
sshExecWorkingDir string
|
|
||||||
sshExecShell string
|
|
||||||
sshExecCommand string
|
|
||||||
sshExecPTY bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// sshExecCmd represents the hidden ssh exec subcommand for privilege dropping
|
|
||||||
var sshExecCmd = &cobra.Command{
|
|
||||||
Use: "exec",
|
|
||||||
Short: "Internal SSH execution with privilege dropping (hidden)",
|
|
||||||
Hidden: true,
|
|
||||||
RunE: runSSHExec,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
sshExecCmd.Flags().Uint32Var(&sshExecUID, "uid", 0, "Target user ID")
|
|
||||||
sshExecCmd.Flags().Uint32Var(&sshExecGID, "gid", 0, "Target group ID")
|
|
||||||
sshExecCmd.Flags().UintSliceVar(&sshExecGroups, "groups", nil, "Supplementary group IDs (can be repeated)")
|
|
||||||
sshExecCmd.Flags().StringVar(&sshExecWorkingDir, "working-dir", "", "Working directory")
|
|
||||||
sshExecCmd.Flags().StringVar(&sshExecShell, "shell", "/bin/sh", "Shell to execute")
|
|
||||||
sshExecCmd.Flags().BoolVar(&sshExecPTY, "pty", false, "Request PTY (will fail as executor doesn't support PTY)")
|
|
||||||
sshExecCmd.Flags().StringVar(&sshExecCommand, "cmd", "", "Command to execute")
|
|
||||||
|
|
||||||
if err := sshExecCmd.MarkFlagRequired("uid"); err != nil {
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "failed to mark uid flag as required: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if err := sshExecCmd.MarkFlagRequired("gid"); err != nil {
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "failed to mark gid flag as required: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
sshCmd.AddCommand(sshExecCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// runSSHExec handles the SSH exec subcommand execution.
|
|
||||||
func runSSHExec(cmd *cobra.Command, _ []string) error {
|
|
||||||
privilegeDropper := sshserver.NewPrivilegeDropper()
|
|
||||||
|
|
||||||
var groups []uint32
|
|
||||||
for _, groupInt := range sshExecGroups {
|
|
||||||
groups = append(groups, uint32(groupInt))
|
|
||||||
}
|
|
||||||
|
|
||||||
config := sshserver.ExecutorConfig{
|
|
||||||
UID: sshExecUID,
|
|
||||||
GID: sshExecGID,
|
|
||||||
Groups: groups,
|
|
||||||
WorkingDir: sshExecWorkingDir,
|
|
||||||
Shell: sshExecShell,
|
|
||||||
Command: sshExecCommand,
|
|
||||||
PTY: sshExecPTY,
|
|
||||||
}
|
|
||||||
|
|
||||||
privilegeDropper.ExecuteWithPrivilegeDrop(cmd.Context(), config)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
//go:build unix
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/sftp"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
sshserver "github.com/netbirdio/netbird/client/ssh/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
sftpUID uint32
|
|
||||||
sftpGID uint32
|
|
||||||
sftpGroupsInt []uint
|
|
||||||
sftpWorkingDir string
|
|
||||||
)
|
|
||||||
|
|
||||||
var sshSftpCmd = &cobra.Command{
|
|
||||||
Use: "sftp",
|
|
||||||
Short: "SFTP server with privilege dropping (internal use)",
|
|
||||||
Hidden: true,
|
|
||||||
RunE: sftpMain,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
sshSftpCmd.Flags().Uint32Var(&sftpUID, "uid", 0, "Target user ID")
|
|
||||||
sshSftpCmd.Flags().Uint32Var(&sftpGID, "gid", 0, "Target group ID")
|
|
||||||
sshSftpCmd.Flags().UintSliceVar(&sftpGroupsInt, "groups", nil, "Supplementary group IDs (can be repeated)")
|
|
||||||
sshSftpCmd.Flags().StringVar(&sftpWorkingDir, "working-dir", "", "Working directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sftpMain(cmd *cobra.Command, _ []string) error {
|
|
||||||
privilegeDropper := sshserver.NewPrivilegeDropper()
|
|
||||||
|
|
||||||
var groups []uint32
|
|
||||||
for _, groupInt := range sftpGroupsInt {
|
|
||||||
groups = append(groups, uint32(groupInt))
|
|
||||||
}
|
|
||||||
|
|
||||||
config := sshserver.ExecutorConfig{
|
|
||||||
UID: sftpUID,
|
|
||||||
GID: sftpGID,
|
|
||||||
Groups: groups,
|
|
||||||
WorkingDir: sftpWorkingDir,
|
|
||||||
Shell: "",
|
|
||||||
Command: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("dropping privileges for SFTP to UID=%d, GID=%d, groups=%v", config.UID, config.GID, config.Groups)
|
|
||||||
|
|
||||||
if err := privilegeDropper.DropPrivileges(config.UID, config.GID, config.Groups); err != nil {
|
|
||||||
cmd.PrintErrf("privilege drop failed: %v\n", err)
|
|
||||||
os.Exit(sshserver.ExitCodePrivilegeDropFail)
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.WorkingDir != "" {
|
|
||||||
if err := os.Chdir(config.WorkingDir); err != nil {
|
|
||||||
cmd.PrintErrf("failed to change to working directory %s: %v\n", config.WorkingDir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sftpServer, err := sftp.NewServer(struct {
|
|
||||||
io.Reader
|
|
||||||
io.WriteCloser
|
|
||||||
}{
|
|
||||||
Reader: os.Stdin,
|
|
||||||
WriteCloser: os.Stdout,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrf("SFTP server creation failed: %v\n", err)
|
|
||||||
os.Exit(sshserver.ExitCodeShellExecFail)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("starting SFTP server with dropped privileges")
|
|
||||||
if err := sftpServer.Serve(); err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
cmd.PrintErrf("SFTP server error: %v\n", err)
|
|
||||||
if closeErr := sftpServer.Close(); closeErr != nil {
|
|
||||||
cmd.PrintErrf("SFTP server close error: %v\n", closeErr)
|
|
||||||
}
|
|
||||||
os.Exit(sshserver.ExitCodeShellExecFail)
|
|
||||||
}
|
|
||||||
|
|
||||||
if closeErr := sftpServer.Close(); closeErr != nil {
|
|
||||||
cmd.PrintErrf("SFTP server close error: %v\n", closeErr)
|
|
||||||
}
|
|
||||||
os.Exit(sshserver.ExitCodeSuccess)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
//go:build windows
|
|
||||||
|
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/user"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/sftp"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
sshserver "github.com/netbirdio/netbird/client/ssh/server"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
sftpWorkingDir string
|
|
||||||
windowsUsername string
|
|
||||||
windowsDomain string
|
|
||||||
)
|
|
||||||
|
|
||||||
var sshSftpCmd = &cobra.Command{
|
|
||||||
Use: "sftp",
|
|
||||||
Short: "SFTP server with user switching for Windows (internal use)",
|
|
||||||
Hidden: true,
|
|
||||||
RunE: sftpMain,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
sshSftpCmd.Flags().StringVar(&sftpWorkingDir, "working-dir", "", "Working directory")
|
|
||||||
sshSftpCmd.Flags().StringVar(&windowsUsername, "windows-username", "", "Windows username for user switching")
|
|
||||||
sshSftpCmd.Flags().StringVar(&windowsDomain, "windows-domain", "", "Windows domain for user switching")
|
|
||||||
}
|
|
||||||
|
|
||||||
func sftpMain(cmd *cobra.Command, _ []string) error {
|
|
||||||
return sftpMainDirect(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func sftpMainDirect(cmd *cobra.Command) error {
|
|
||||||
currentUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrf("failed to get current user: %v\n", err)
|
|
||||||
os.Exit(sshserver.ExitCodeValidationFail)
|
|
||||||
}
|
|
||||||
|
|
||||||
if windowsUsername != "" {
|
|
||||||
expectedUsername := windowsUsername
|
|
||||||
if windowsDomain != "" {
|
|
||||||
expectedUsername = fmt.Sprintf(`%s\%s`, windowsDomain, windowsUsername)
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(currentUser.Username, expectedUsername) && !strings.EqualFold(currentUser.Username, windowsUsername) {
|
|
||||||
cmd.PrintErrf("user switching failed\n")
|
|
||||||
os.Exit(sshserver.ExitCodeValidationFail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("SFTP process running as: %s (UID: %s, Name: %s)", currentUser.Username, currentUser.Uid, currentUser.Name)
|
|
||||||
|
|
||||||
if sftpWorkingDir != "" {
|
|
||||||
if err := os.Chdir(sftpWorkingDir); err != nil {
|
|
||||||
cmd.PrintErrf("failed to change to working directory %s: %v\n", sftpWorkingDir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sftpServer, err := sftp.NewServer(struct {
|
|
||||||
io.Reader
|
|
||||||
io.WriteCloser
|
|
||||||
}{
|
|
||||||
Reader: os.Stdin,
|
|
||||||
WriteCloser: os.Stdout,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
cmd.PrintErrf("SFTP server creation failed: %v\n", err)
|
|
||||||
os.Exit(sshserver.ExitCodeShellExecFail)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("starting SFTP server")
|
|
||||||
exitCode := sshserver.ExitCodeSuccess
|
|
||||||
if err := sftpServer.Serve(); err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
cmd.PrintErrf("SFTP server error: %v\n", err)
|
|
||||||
exitCode = sshserver.ExitCodeShellExecFail
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sftpServer.Close(); err != nil {
|
|
||||||
log.Debugf("SFTP server close error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
os.Exit(exitCode)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,717 +0,0 @@
|
|||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSSHCommand_FlagParsing(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedHost string
|
|
||||||
expectedUser string
|
|
||||||
expectedPort int
|
|
||||||
expectedCmd string
|
|
||||||
expectError bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "basic host",
|
|
||||||
args: []string{"hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedUser: "",
|
|
||||||
expectedPort: 22,
|
|
||||||
expectedCmd: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "user@host format",
|
|
||||||
args: []string{"user@hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedUser: "user",
|
|
||||||
expectedPort: 22,
|
|
||||||
expectedCmd: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "host with command",
|
|
||||||
args: []string{"hostname", "echo", "hello"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedUser: "",
|
|
||||||
expectedPort: 22,
|
|
||||||
expectedCmd: "echo hello",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "command with flags should be preserved",
|
|
||||||
args: []string{"hostname", "ls", "-la", "/tmp"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedUser: "",
|
|
||||||
expectedPort: 22,
|
|
||||||
expectedCmd: "ls -la /tmp",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "double dash separator",
|
|
||||||
args: []string{"hostname", "--", "ls", "-la"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedUser: "",
|
|
||||||
expectedPort: 22,
|
|
||||||
expectedCmd: "-- ls -la",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
|
|
||||||
// Mock command for testing
|
|
||||||
cmd := sshCmd
|
|
||||||
cmd.SetArgs(tt.args)
|
|
||||||
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(cmd, tt.args)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err, "SSH args validation should succeed for valid input")
|
|
||||||
assert.Equal(t, tt.expectedHost, host, "host mismatch")
|
|
||||||
if tt.expectedUser != "" {
|
|
||||||
assert.Equal(t, tt.expectedUser, username, "username mismatch")
|
|
||||||
}
|
|
||||||
assert.Equal(t, tt.expectedPort, port, "port mismatch")
|
|
||||||
assert.Equal(t, tt.expectedCmd, command, "command mismatch")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_FlagConflictPrevention(t *testing.T) {
|
|
||||||
// Test that SSH flags don't conflict with command flags
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedCmd string
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "ls with -la flags",
|
|
||||||
args: []string{"hostname", "ls", "-la"},
|
|
||||||
expectedCmd: "ls -la",
|
|
||||||
description: "ls flags should be passed to remote command",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "grep with -r flag",
|
|
||||||
args: []string{"hostname", "grep", "-r", "pattern", "/path"},
|
|
||||||
expectedCmd: "grep -r pattern /path",
|
|
||||||
description: "grep flags should be passed to remote command",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ps with aux flags",
|
|
||||||
args: []string{"hostname", "ps", "aux"},
|
|
||||||
expectedCmd: "ps aux",
|
|
||||||
description: "ps flags should be passed to remote command",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "command with double dash",
|
|
||||||
args: []string{"hostname", "--", "ls", "-la"},
|
|
||||||
expectedCmd: "-- ls -la",
|
|
||||||
description: "double dash should be preserved in command",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
|
|
||||||
cmd := sshCmd
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(cmd, tt.args)
|
|
||||||
require.NoError(t, err, "SSH args validation should succeed for valid input")
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedCmd, command, tt.description)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_NonInteractiveExecution(t *testing.T) {
|
|
||||||
// Test that commands with arguments should execute the command and exit,
|
|
||||||
// not drop to an interactive shell
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedCmd string
|
|
||||||
shouldExit bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "ls command should execute and exit",
|
|
||||||
args: []string{"hostname", "ls"},
|
|
||||||
expectedCmd: "ls",
|
|
||||||
shouldExit: true,
|
|
||||||
description: "ls command should execute and exit, not drop to shell",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ls with flags should execute and exit",
|
|
||||||
args: []string{"hostname", "ls", "-la"},
|
|
||||||
expectedCmd: "ls -la",
|
|
||||||
shouldExit: true,
|
|
||||||
description: "ls with flags should execute and exit, not drop to shell",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "pwd command should execute and exit",
|
|
||||||
args: []string{"hostname", "pwd"},
|
|
||||||
expectedCmd: "pwd",
|
|
||||||
shouldExit: true,
|
|
||||||
description: "pwd command should execute and exit, not drop to shell",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "echo command should execute and exit",
|
|
||||||
args: []string{"hostname", "echo", "hello"},
|
|
||||||
expectedCmd: "echo hello",
|
|
||||||
shouldExit: true,
|
|
||||||
description: "echo command should execute and exit, not drop to shell",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no command should open shell",
|
|
||||||
args: []string{"hostname"},
|
|
||||||
expectedCmd: "",
|
|
||||||
shouldExit: false,
|
|
||||||
description: "no command should open interactive shell",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
|
|
||||||
cmd := sshCmd
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(cmd, tt.args)
|
|
||||||
require.NoError(t, err, "SSH args validation should succeed for valid input")
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedCmd, command, tt.description)
|
|
||||||
|
|
||||||
// When command is present, it should execute the command and exit
|
|
||||||
// When command is empty, it should open interactive shell
|
|
||||||
hasCommand := command != ""
|
|
||||||
assert.Equal(t, tt.shouldExit, hasCommand, "Command presence should match expected behavior")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_FlagHandling(t *testing.T) {
|
|
||||||
// Test that flags after hostname are not parsed by netbird but passed to SSH command
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedHost string
|
|
||||||
expectedCmd string
|
|
||||||
expectError bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "ls with -la flag should not be parsed by netbird",
|
|
||||||
args: []string{"debian2", "ls", "-la"},
|
|
||||||
expectedHost: "debian2",
|
|
||||||
expectedCmd: "ls -la",
|
|
||||||
expectError: false,
|
|
||||||
description: "ls -la should be passed as SSH command, not parsed as netbird flags",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "command with netbird-like flags should be passed through",
|
|
||||||
args: []string{"hostname", "echo", "--help"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedCmd: "echo --help",
|
|
||||||
expectError: false,
|
|
||||||
description: "--help should be passed to echo, not parsed by netbird",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "command with -p flag should not conflict with SSH port flag",
|
|
||||||
args: []string{"hostname", "ps", "-p", "1234"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedCmd: "ps -p 1234",
|
|
||||||
expectError: false,
|
|
||||||
description: "ps -p should be passed to ps command, not parsed as port",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tar with flags should be passed through",
|
|
||||||
args: []string{"hostname", "tar", "-czf", "backup.tar.gz", "/home"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedCmd: "tar -czf backup.tar.gz /home",
|
|
||||||
expectError: false,
|
|
||||||
description: "tar flags should be passed to tar command",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
|
|
||||||
cmd := sshCmd
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(cmd, tt.args)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err, "SSH args validation should succeed for valid input")
|
|
||||||
assert.Equal(t, tt.expectedHost, host, "host mismatch")
|
|
||||||
assert.Equal(t, tt.expectedCmd, command, tt.description)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_RegressionFlagParsing(t *testing.T) {
|
|
||||||
// Regression test for the specific issue: "sudo ./netbird ssh debian2 ls -la"
|
|
||||||
// should not parse -la as netbird flags but pass them to the SSH command
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedHost string
|
|
||||||
expectedCmd string
|
|
||||||
expectError bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "original issue: ls -la should be preserved",
|
|
||||||
args: []string{"debian2", "ls", "-la"},
|
|
||||||
expectedHost: "debian2",
|
|
||||||
expectedCmd: "ls -la",
|
|
||||||
expectError: false,
|
|
||||||
description: "The original failing case should now work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "ls -l should be preserved",
|
|
||||||
args: []string{"hostname", "ls", "-l"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedCmd: "ls -l",
|
|
||||||
expectError: false,
|
|
||||||
description: "Single letter flags should be preserved",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "SSH port flag should work",
|
|
||||||
args: []string{"-p", "2222", "hostname", "ls", "-la"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedCmd: "ls -la",
|
|
||||||
expectError: false,
|
|
||||||
description: "SSH -p flag should be parsed, command flags preserved",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
|
|
||||||
cmd := sshCmd
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(cmd, tt.args)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err, "SSH args validation should succeed for valid input")
|
|
||||||
assert.Equal(t, tt.expectedHost, host, "host mismatch")
|
|
||||||
assert.Equal(t, tt.expectedCmd, command, tt.description)
|
|
||||||
|
|
||||||
// Check port for the test case with -p flag
|
|
||||||
if len(tt.args) > 0 && tt.args[0] == "-p" {
|
|
||||||
assert.Equal(t, 2222, port, "port should be parsed from -p flag")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_PortForwardingFlagParsing(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedHost string
|
|
||||||
expectedLocal []string
|
|
||||||
expectedRemote []string
|
|
||||||
expectError bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "local port forwarding -L",
|
|
||||||
args: []string{"-L", "8080:localhost:80", "hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{"8080:localhost:80"},
|
|
||||||
expectedRemote: []string{},
|
|
||||||
expectError: false,
|
|
||||||
description: "Single -L flag should be parsed correctly",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remote port forwarding -R",
|
|
||||||
args: []string{"-R", "8080:localhost:80", "hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{},
|
|
||||||
expectedRemote: []string{"8080:localhost:80"},
|
|
||||||
expectError: false,
|
|
||||||
description: "Single -R flag should be parsed correctly",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple local port forwards",
|
|
||||||
args: []string{"-L", "8080:localhost:80", "-L", "9090:localhost:443", "hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{"8080:localhost:80", "9090:localhost:443"},
|
|
||||||
expectedRemote: []string{},
|
|
||||||
expectError: false,
|
|
||||||
description: "Multiple -L flags should be parsed correctly",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple remote port forwards",
|
|
||||||
args: []string{"-R", "8080:localhost:80", "-R", "9090:localhost:443", "hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{},
|
|
||||||
expectedRemote: []string{"8080:localhost:80", "9090:localhost:443"},
|
|
||||||
expectError: false,
|
|
||||||
description: "Multiple -R flags should be parsed correctly",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed local and remote forwards",
|
|
||||||
args: []string{"-L", "8080:localhost:80", "-R", "9090:localhost:443", "hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{"8080:localhost:80"},
|
|
||||||
expectedRemote: []string{"9090:localhost:443"},
|
|
||||||
expectError: false,
|
|
||||||
description: "Mixed -L and -R flags should be parsed correctly",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port forwarding with bind address",
|
|
||||||
args: []string{"-L", "127.0.0.1:8080:localhost:80", "hostname"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{"127.0.0.1:8080:localhost:80"},
|
|
||||||
expectedRemote: []string{},
|
|
||||||
expectError: false,
|
|
||||||
description: "Port forwarding with bind address should work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port forwarding with command",
|
|
||||||
args: []string{"-L", "8080:localhost:80", "hostname", "ls", "-la"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{"8080:localhost:80"},
|
|
||||||
expectedRemote: []string{},
|
|
||||||
expectError: false,
|
|
||||||
description: "Port forwarding with command should work",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
localForwards = nil
|
|
||||||
remoteForwards = nil
|
|
||||||
|
|
||||||
cmd := sshCmd
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(cmd, tt.args)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
assert.Error(t, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err, "SSH args validation should succeed for valid input")
|
|
||||||
assert.Equal(t, tt.expectedHost, host, "host mismatch")
|
|
||||||
// Handle nil vs empty slice comparison
|
|
||||||
if len(tt.expectedLocal) == 0 {
|
|
||||||
assert.True(t, len(localForwards) == 0, tt.description+" - local forwards should be empty")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, tt.expectedLocal, localForwards, tt.description+" - local forwards")
|
|
||||||
}
|
|
||||||
if len(tt.expectedRemote) == 0 {
|
|
||||||
assert.True(t, len(remoteForwards) == 0, tt.description+" - remote forwards should be empty")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, tt.expectedRemote, remoteForwards, tt.description+" - remote forwards")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParsePortForward(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
spec string
|
|
||||||
expectedLocal string
|
|
||||||
expectedRemote string
|
|
||||||
expectError bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "simple port forward",
|
|
||||||
spec: "8080:localhost:80",
|
|
||||||
expectedLocal: "localhost:8080",
|
|
||||||
expectedRemote: "localhost:80",
|
|
||||||
expectError: false,
|
|
||||||
description: "Simple port:host:port format should work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port forward with bind address",
|
|
||||||
spec: "127.0.0.1:8080:localhost:80",
|
|
||||||
expectedLocal: "127.0.0.1:8080",
|
|
||||||
expectedRemote: "localhost:80",
|
|
||||||
expectError: false,
|
|
||||||
description: "bind_address:port:host:port format should work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port forward to different host",
|
|
||||||
spec: "8080:example.com:443",
|
|
||||||
expectedLocal: "localhost:8080",
|
|
||||||
expectedRemote: "example.com:443",
|
|
||||||
expectError: false,
|
|
||||||
description: "Forwarding to different host should work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port forward with IPv6 (needs bracket support)",
|
|
||||||
spec: "::1:8080:localhost:80",
|
|
||||||
expectError: true,
|
|
||||||
description: "IPv6 without brackets fails as expected (feature to implement)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid format - too few parts",
|
|
||||||
spec: "8080:localhost",
|
|
||||||
expectError: true,
|
|
||||||
description: "Invalid format with too few parts should fail",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid format - too many parts",
|
|
||||||
spec: "127.0.0.1:8080:localhost:80:extra",
|
|
||||||
expectError: true,
|
|
||||||
description: "Invalid format with too many parts should fail",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty spec",
|
|
||||||
spec: "",
|
|
||||||
expectError: true,
|
|
||||||
description: "Empty spec should fail",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unix socket local forward",
|
|
||||||
spec: "8080:/tmp/socket",
|
|
||||||
expectedLocal: "localhost:8080",
|
|
||||||
expectedRemote: "/tmp/socket",
|
|
||||||
expectError: false,
|
|
||||||
description: "Unix socket forwarding should work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unix socket with bind address",
|
|
||||||
spec: "127.0.0.1:8080:/tmp/socket",
|
|
||||||
expectedLocal: "127.0.0.1:8080",
|
|
||||||
expectedRemote: "/tmp/socket",
|
|
||||||
expectError: false,
|
|
||||||
description: "Unix socket with bind address should work",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard bind all interfaces",
|
|
||||||
spec: "*:8080:localhost:80",
|
|
||||||
expectedLocal: "0.0.0.0:8080",
|
|
||||||
expectedRemote: "localhost:80",
|
|
||||||
expectError: false,
|
|
||||||
description: "Wildcard * should bind to all interfaces (0.0.0.0)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wildcard for port only",
|
|
||||||
spec: "8080:*:80",
|
|
||||||
expectedLocal: "localhost:8080",
|
|
||||||
expectedRemote: "*:80",
|
|
||||||
expectError: false,
|
|
||||||
description: "Wildcard in remote host should be preserved",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
localAddr, remoteAddr, err := parsePortForwardSpec(tt.spec)
|
|
||||||
|
|
||||||
if tt.expectError {
|
|
||||||
assert.Error(t, err, tt.description)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, err, tt.description)
|
|
||||||
assert.Equal(t, tt.expectedLocal, localAddr, tt.description+" - local address")
|
|
||||||
assert.Equal(t, tt.expectedRemote, remoteAddr, tt.description+" - remote address")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_IntegrationPortForwarding(t *testing.T) {
|
|
||||||
// Integration test for port forwarding with the actual SSH command implementation
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedHost string
|
|
||||||
expectedLocal []string
|
|
||||||
expectedRemote []string
|
|
||||||
expectedCmd string
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "local forward with command",
|
|
||||||
args: []string{"-L", "8080:localhost:80", "hostname", "echo", "test"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{"8080:localhost:80"},
|
|
||||||
expectedRemote: []string{},
|
|
||||||
expectedCmd: "echo test",
|
|
||||||
description: "Local forwarding should work with commands",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "remote forward with command",
|
|
||||||
args: []string{"-R", "8080:localhost:80", "hostname", "ls", "-la"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{},
|
|
||||||
expectedRemote: []string{"8080:localhost:80"},
|
|
||||||
expectedCmd: "ls -la",
|
|
||||||
description: "Remote forwarding should work with commands",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple forwards with user and command",
|
|
||||||
args: []string{"-L", "8080:localhost:80", "-R", "9090:localhost:443", "user@hostname", "ps", "aux"},
|
|
||||||
expectedHost: "hostname",
|
|
||||||
expectedLocal: []string{"8080:localhost:80"},
|
|
||||||
expectedRemote: []string{"9090:localhost:443"},
|
|
||||||
expectedCmd: "ps aux",
|
|
||||||
description: "Complex case with multiple forwards, user, and command",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
localForwards = nil
|
|
||||||
remoteForwards = nil
|
|
||||||
|
|
||||||
cmd := sshCmd
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(cmd, tt.args)
|
|
||||||
require.NoError(t, err, "SSH args validation should succeed for valid input")
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedHost, host, "host mismatch")
|
|
||||||
// Handle nil vs empty slice comparison
|
|
||||||
if len(tt.expectedLocal) == 0 {
|
|
||||||
assert.True(t, len(localForwards) == 0, tt.description+" - local forwards should be empty")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, tt.expectedLocal, localForwards, tt.description+" - local forwards")
|
|
||||||
}
|
|
||||||
if len(tt.expectedRemote) == 0 {
|
|
||||||
assert.True(t, len(remoteForwards) == 0, tt.description+" - remote forwards should be empty")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, tt.expectedRemote, remoteForwards, tt.description+" - remote forwards")
|
|
||||||
}
|
|
||||||
assert.Equal(t, tt.expectedCmd, command, tt.description+" - command")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_ParameterIsolation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
expectedCmd string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "cmd flag passed as command",
|
|
||||||
args: []string{"hostname", "--cmd", "echo test"},
|
|
||||||
expectedCmd: "--cmd echo test",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "uid flag passed as command",
|
|
||||||
args: []string{"hostname", "--uid", "1000"},
|
|
||||||
expectedCmd: "--uid 1000",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "shell flag passed as command",
|
|
||||||
args: []string{"hostname", "--shell", "/bin/bash"},
|
|
||||||
expectedCmd: "--shell /bin/bash",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(sshCmd, tt.args)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
assert.Equal(t, "hostname", host)
|
|
||||||
assert.Equal(t, tt.expectedCmd, command)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSSHCommand_InvalidFlagRejection(t *testing.T) {
|
|
||||||
// Test that invalid flags are properly rejected and not misinterpreted as hostnames
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args []string
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "invalid long flag before hostname",
|
|
||||||
args: []string{"--invalid-flag", "hostname"},
|
|
||||||
description: "Invalid flag should return parse error, not treat flag as hostname",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid short flag before hostname",
|
|
||||||
args: []string{"-x", "hostname"},
|
|
||||||
description: "Invalid short flag should return parse error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid flag with value before hostname",
|
|
||||||
args: []string{"--invalid-option=value", "hostname"},
|
|
||||||
description: "Invalid flag with value should return parse error",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "typo in known flag",
|
|
||||||
args: []string{"--por", "2222", "hostname"},
|
|
||||||
description: "Typo in flag name should return parse error (not silently ignored)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Reset global variables
|
|
||||||
host = ""
|
|
||||||
username = ""
|
|
||||||
port = 22
|
|
||||||
command = ""
|
|
||||||
|
|
||||||
err := validateSSHArgsWithoutFlagParsing(sshCmd, tt.args)
|
|
||||||
|
|
||||||
// Should return an error for invalid flags
|
|
||||||
assert.Error(t, err, tt.description)
|
|
||||||
|
|
||||||
// Should not have set host to the invalid flag
|
|
||||||
assert.NotEqual(t, tt.args[0], host, "Invalid flag should not be interpreted as hostname")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -68,7 +68,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
ctx := internal.CtxInitState(cmd.Context())
|
ctx := internal.CtxInitState(cmd.Context())
|
||||||
|
|
||||||
resp, err := getStatus(ctx, false)
|
resp, err := getStatus(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
case yamlFlag:
|
case yamlFlag:
|
||||||
statusOutputString, err = nbstatus.ParseToYAML(outputInformationHolder)
|
statusOutputString, err = nbstatus.ParseToYAML(outputInformationHolder)
|
||||||
default:
|
default:
|
||||||
statusOutputString = nbstatus.ParseGeneralSummary(outputInformationHolder, false, false, false, false)
|
statusOutputString = nbstatus.ParseGeneralSummary(outputInformationHolder, false, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,7 +121,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatus(ctx context.Context, shouldRunProbes bool) (*proto.StatusResponse, error) {
|
func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
@@ -130,7 +130,7 @@ func getStatus(ctx context.Context, shouldRunProbes bool) (*proto.StatusResponse
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true, ShouldRunProbes: shouldRunProbes})
|
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true, ShouldRunProbes: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/controller"
|
|
||||||
"github.com/netbirdio/netbird/management/internals/controllers/network_map/update_channel"
|
|
||||||
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
|
|
||||||
|
|
||||||
clientProto "github.com/netbirdio/netbird/client/proto"
|
clientProto "github.com/netbirdio/netbird/client/proto"
|
||||||
client "github.com/netbirdio/netbird/client/server"
|
client "github.com/netbirdio/netbird/client/server"
|
||||||
"github.com/netbirdio/netbird/management/internals/server/config"
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
@@ -25,7 +20,6 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/groups"
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
"github.com/netbirdio/netbird/management/server/peers"
|
"github.com/netbirdio/netbird/management/server/peers"
|
||||||
"github.com/netbirdio/netbird/management/server/peers/ephemeral/manager"
|
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
"github.com/netbirdio/netbird/management/server/permissions"
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
"github.com/netbirdio/netbird/management/server/settings"
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
@@ -88,6 +82,7 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
}
|
}
|
||||||
t.Cleanup(cleanUp)
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
|
peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -113,18 +108,13 @@ func startManagement(t *testing.T, config *config.Config, testFile string) (*grp
|
|||||||
Return(&types.Settings{}, nil).
|
Return(&types.Settings{}, nil).
|
||||||
AnyTimes()
|
AnyTimes()
|
||||||
|
|
||||||
ctx := context.Background()
|
accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false)
|
||||||
updateManager := update_channel.NewPeersUpdateManager(metrics)
|
|
||||||
requestBuffer := mgmt.NewAccountRequestBuffer(ctx, store)
|
|
||||||
networkMapController := controller.NewController(ctx, store, metrics, updateManager, requestBuffer, mgmt.MockIntegratedValidator{}, settingsMockManager, "netbird.cloud", port_forwarding.NewControllerMock(), config)
|
|
||||||
|
|
||||||
accountManager, err := mgmt.BuildManager(context.Background(), config, store, networkMapController, nil, "", eventStore, nil, false, iv, metrics, port_forwarding.NewControllerMock(), settingsMockManager, permissionsManagerMock, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsManager := nbgrpc.NewTimeBasedAuthSecretsManager(updateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
|
secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
|
||||||
mgmtServer, err := nbgrpc.NewServer(config, accountManager, settingsMockManager, updateManager, secretsManager, nil, &manager.EphemeralManager{}, nil, &mgmt.MockIntegratedValidator{}, networkMapController)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command, activeProf *pr
|
|||||||
|
|
||||||
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
|
_, _ = profilemanager.UpdateOldManagementURL(ctx, config, configFilePath)
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, providedSetupKey, activeProf.Name)
|
err = foregroundLogin(ctx, cmd, config, providedSetupKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("foreground login failed: %v", err)
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -230,9 +230,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
|
|||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
status, err := client.Status(ctx, &proto.StatusRequest{
|
status, err := client.Status(ctx, &proto.StatusRequest{})
|
||||||
WaitForReady: func() *bool { b := true; return &b }(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get daemon status: %v", err)
|
return fmt.Errorf("unable to get daemon status: %v", err)
|
||||||
}
|
}
|
||||||
@@ -286,13 +284,6 @@ func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServ
|
|||||||
loginRequest.ProfileName = &activeProf.Name
|
loginRequest.ProfileName = &activeProf.Name
|
||||||
loginRequest.Username = &username
|
loginRequest.Username = &username
|
||||||
|
|
||||||
profileState, err := pm.GetProfileState(activeProf.Name)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("failed to get profile state for login hint: %v", err)
|
|
||||||
} else if profileState.Email != "" {
|
|
||||||
loginRequest.Hint = &profileState.Email
|
|
||||||
}
|
|
||||||
|
|
||||||
var loginErr error
|
var loginErr error
|
||||||
var loginResp *proto.LoginResponse
|
var loginResp *proto.LoginResponse
|
||||||
|
|
||||||
@@ -355,25 +346,6 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro
|
|||||||
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
||||||
req.ServerSSHAllowed = &serverSSHAllowed
|
req.ServerSSHAllowed = &serverSSHAllowed
|
||||||
}
|
}
|
||||||
if cmd.Flag(enableSSHRootFlag).Changed {
|
|
||||||
req.EnableSSHRoot = &enableSSHRoot
|
|
||||||
}
|
|
||||||
if cmd.Flag(enableSSHSFTPFlag).Changed {
|
|
||||||
req.EnableSSHSFTP = &enableSSHSFTP
|
|
||||||
}
|
|
||||||
if cmd.Flag(enableSSHLocalPortForwardFlag).Changed {
|
|
||||||
req.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward
|
|
||||||
}
|
|
||||||
if cmd.Flag(enableSSHRemotePortForwardFlag).Changed {
|
|
||||||
req.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward
|
|
||||||
}
|
|
||||||
if cmd.Flag(disableSSHAuthFlag).Changed {
|
|
||||||
req.DisableSSHAuth = &disableSSHAuth
|
|
||||||
}
|
|
||||||
if cmd.Flag(sshJWTCacheTTLFlag).Changed {
|
|
||||||
sshJWTCacheTTL32 := int32(sshJWTCacheTTL)
|
|
||||||
req.SshJWTCacheTTL = &sshJWTCacheTTL32
|
|
||||||
}
|
|
||||||
if cmd.Flag(interfaceNameFlag).Changed {
|
if cmd.Flag(interfaceNameFlag).Changed {
|
||||||
if err := parseInterfaceName(interfaceName); err != nil {
|
if err := parseInterfaceName(interfaceName); err != nil {
|
||||||
log.Errorf("parse interface name: %v", err)
|
log.Errorf("parse interface name: %v", err)
|
||||||
@@ -458,30 +430,6 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil
|
|||||||
ic.ServerSSHAllowed = &serverSSHAllowed
|
ic.ServerSSHAllowed = &serverSSHAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Flag(enableSSHRootFlag).Changed {
|
|
||||||
ic.EnableSSHRoot = &enableSSHRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(enableSSHSFTPFlag).Changed {
|
|
||||||
ic.EnableSSHSFTP = &enableSSHSFTP
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(enableSSHLocalPortForwardFlag).Changed {
|
|
||||||
ic.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(enableSSHRemotePortForwardFlag).Changed {
|
|
||||||
ic.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(disableSSHAuthFlag).Changed {
|
|
||||||
ic.DisableSSHAuth = &disableSSHAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(sshJWTCacheTTLFlag).Changed {
|
|
||||||
ic.SSHJWTCacheTTL = &sshJWTCacheTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(interfaceNameFlag).Changed {
|
if cmd.Flag(interfaceNameFlag).Changed {
|
||||||
if err := parseInterfaceName(interfaceName); err != nil {
|
if err := parseInterfaceName(interfaceName); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -582,31 +530,6 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
|
|||||||
loginRequest.ServerSSHAllowed = &serverSSHAllowed
|
loginRequest.ServerSSHAllowed = &serverSSHAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.Flag(enableSSHRootFlag).Changed {
|
|
||||||
loginRequest.EnableSSHRoot = &enableSSHRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(enableSSHSFTPFlag).Changed {
|
|
||||||
loginRequest.EnableSSHSFTP = &enableSSHSFTP
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(enableSSHLocalPortForwardFlag).Changed {
|
|
||||||
loginRequest.EnableSSHLocalPortForwarding = &enableSSHLocalPortForward
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(enableSSHRemotePortForwardFlag).Changed {
|
|
||||||
loginRequest.EnableSSHRemotePortForwarding = &enableSSHRemotePortForward
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(disableSSHAuthFlag).Changed {
|
|
||||||
loginRequest.DisableSSHAuth = &disableSSHAuth
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(sshJWTCacheTTLFlag).Changed {
|
|
||||||
sshJWTCacheTTL32 := int32(sshJWTCacheTTL)
|
|
||||||
loginRequest.SshJWTCacheTTL = &sshJWTCacheTTL32
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(disableAutoConnectFlag).Changed {
|
if cmd.Flag(disableAutoConnectFlag).Changed {
|
||||||
loginRequest.DisableAutoConnect = &autoConnectDisabled
|
loginRequest.DisableAutoConnect = &autoConnectDisabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,38 +18,28 @@ 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"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
sshcommon "github.com/netbirdio/netbird/client/ssh"
|
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var ErrClientAlreadyStarted = errors.New("client already started")
|
||||||
ErrClientAlreadyStarted = errors.New("client already started")
|
var ErrClientNotStarted = errors.New("client not started")
|
||||||
ErrClientNotStarted = errors.New("client not started")
|
|
||||||
ErrEngineNotStarted = errors.New("engine not started")
|
|
||||||
ErrConfigNotInitialized = errors.New("config not initialized")
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 *profilemanager.Config
|
config *profilemanager.Config
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
setupKey string
|
setupKey string
|
||||||
jwtToken string
|
|
||||||
connect *internal.ConnectClient
|
connect *internal.ConnectClient
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options configures a new Client.
|
// Options configures a new Client
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// DeviceName is this peer's name in the network
|
// DeviceName is this peer's name in the network
|
||||||
DeviceName string
|
DeviceName string
|
||||||
// SetupKey is used for authentication
|
// SetupKey is used for authentication
|
||||||
SetupKey string
|
SetupKey string
|
||||||
// JWTToken is used for JWT-based authentication
|
|
||||||
JWTToken string
|
|
||||||
// PrivateKey is used for direct private key authentication
|
|
||||||
PrivateKey string
|
|
||||||
// ManagementURL overrides the default management server URL
|
// ManagementURL overrides the default management server URL
|
||||||
ManagementURL string
|
ManagementURL string
|
||||||
// PreSharedKey is the pre-shared key for the WireGuard interface
|
// PreSharedKey is the pre-shared key for the WireGuard interface
|
||||||
@@ -68,35 +58,8 @@ type Options struct {
|
|||||||
DisableClientRoutes bool
|
DisableClientRoutes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateCredentials checks that exactly one credential type is provided
|
// New creates a new netbird embedded client
|
||||||
func (opts *Options) validateCredentials() error {
|
|
||||||
credentialsProvided := 0
|
|
||||||
if opts.SetupKey != "" {
|
|
||||||
credentialsProvided++
|
|
||||||
}
|
|
||||||
if opts.JWTToken != "" {
|
|
||||||
credentialsProvided++
|
|
||||||
}
|
|
||||||
if opts.PrivateKey != "" {
|
|
||||||
credentialsProvided++
|
|
||||||
}
|
|
||||||
|
|
||||||
if credentialsProvided == 0 {
|
|
||||||
return fmt.Errorf("one of SetupKey, JWTToken, or PrivateKey must be provided")
|
|
||||||
}
|
|
||||||
if credentialsProvided > 1 {
|
|
||||||
return fmt.Errorf("only one of SetupKey, JWTToken, or PrivateKey can be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new netbird embedded client.
|
|
||||||
func New(opts Options) (*Client, error) {
|
func New(opts Options) (*Client, error) {
|
||||||
if err := opts.validateCredentials(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.LogOutput != nil {
|
if opts.LogOutput != nil {
|
||||||
logrus.SetOutput(opts.LogOutput)
|
logrus.SetOutput(opts.LogOutput)
|
||||||
}
|
}
|
||||||
@@ -144,14 +107,9 @@ func New(opts Options) (*Client, error) {
|
|||||||
return nil, fmt.Errorf("create config: %w", err)
|
return nil, fmt.Errorf("create config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.PrivateKey != "" {
|
|
||||||
config.PrivateKey = opts.PrivateKey
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Client{
|
return &Client{
|
||||||
deviceName: opts.DeviceName,
|
deviceName: opts.DeviceName,
|
||||||
setupKey: opts.SetupKey,
|
setupKey: opts.SetupKey,
|
||||||
jwtToken: opts.JWTToken,
|
|
||||||
config: config,
|
config: config,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -168,7 +126,7 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
ctx := internal.CtxInitState(context.Background())
|
ctx := internal.CtxInitState(context.Background())
|
||||||
// nolint:staticcheck
|
// nolint:staticcheck
|
||||||
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
|
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, c.deviceName)
|
||||||
if err := internal.Login(ctx, c.config, c.setupKey, c.jwtToken); err != nil {
|
if err := internal.Login(ctx, c.config, c.setupKey, ""); err != nil {
|
||||||
return fmt.Errorf("login: %w", err)
|
return fmt.Errorf("login: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +135,7 @@ func (c *Client) Start(startCtx context.Context) error {
|
|||||||
|
|
||||||
// either startup error (permanent backoff err) or nil err (successful engine up)
|
// either startup error (permanent backoff err) or nil err (successful engine up)
|
||||||
// TODO: make after-startup backoff err available
|
// TODO: make after-startup backoff err available
|
||||||
run := make(chan struct{})
|
run := make(chan struct{}, 1)
|
||||||
clientErr := make(chan error, 1)
|
clientErr := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
if err := client.Run(run); err != nil {
|
if err := client.Run(run); err != nil {
|
||||||
@@ -229,22 +187,20 @@ func (c *Client) Stop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfig returns a copy of the internal client config.
|
|
||||||
func (c *Client) GetConfig() (profilemanager.Config, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if c.config == nil {
|
|
||||||
return profilemanager.Config{}, ErrConfigNotInitialized
|
|
||||||
}
|
|
||||||
return *c.config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial dials a network address in the netbird network.
|
// Dial dials a network address in the netbird network.
|
||||||
// Not applicable if the userspace networking mode is disabled.
|
// Not applicable if the userspace networking mode is disabled.
|
||||||
func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
|
func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
engine, err := c.getEngine()
|
c.mu.Lock()
|
||||||
if err != nil {
|
connect := c.connect
|
||||||
return nil, err
|
if connect == nil {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return nil, ErrClientNotStarted
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
engine := connect.Engine()
|
||||||
|
if engine == nil {
|
||||||
|
return nil, errors.New("engine not started")
|
||||||
}
|
}
|
||||||
|
|
||||||
nsnet, err := engine.GetNet()
|
nsnet, err := engine.GetNet()
|
||||||
@@ -255,12 +211,7 @@ func (c *Client) Dial(ctx context.Context, network, address string) (net.Conn, e
|
|||||||
return nsnet.DialContext(ctx, network, address)
|
return nsnet.DialContext(ctx, network, address)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext dials a network address in the netbird network with context
|
// ListenTCP listens on the given address in the netbird network
|
||||||
func (c *Client) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
return c.Dial(ctx, network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListenTCP listens on the given address in the netbird network.
|
|
||||||
// Not applicable if the userspace networking mode is disabled.
|
// Not applicable if the userspace networking mode is disabled.
|
||||||
func (c *Client) ListenTCP(address string) (net.Listener, error) {
|
func (c *Client) ListenTCP(address string) (net.Listener, error) {
|
||||||
nsnet, addr, err := c.getNet()
|
nsnet, addr, err := c.getNet()
|
||||||
@@ -281,7 +232,7 @@ func (c *Client) ListenTCP(address string) (net.Listener, error) {
|
|||||||
return nsnet.ListenTCP(tcpAddr)
|
return nsnet.ListenTCP(tcpAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenUDP listens on the given address in the netbird network.
|
// ListenUDP listens on the given address in the netbird network
|
||||||
// Not applicable if the userspace networking mode is disabled.
|
// Not applicable if the userspace networking mode is disabled.
|
||||||
func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
|
func (c *Client) ListenUDP(address string) (net.PacketConn, error) {
|
||||||
nsnet, addr, err := c.getNet()
|
nsnet, addr, err := c.getNet()
|
||||||
@@ -315,47 +266,18 @@ func (c *Client) NewHTTPClient() *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifySSHHostKey verifies an SSH host key against stored peer keys.
|
func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) {
|
||||||
// Returns nil if the key matches, ErrPeerNotFound if peer is not in network,
|
|
||||||
// ErrNoStoredKey if peer has no stored key, or an error for verification failures.
|
|
||||||
func (c *Client) VerifySSHHostKey(peerAddress string, key []byte) error {
|
|
||||||
engine, err := c.getEngine()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
storedKey, found := engine.GetPeerSSHKey(peerAddress)
|
|
||||||
if !found {
|
|
||||||
return sshcommon.ErrPeerNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return sshcommon.VerifyHostKey(storedKey, key, peerAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getEngine safely retrieves the engine from the client with proper locking.
|
|
||||||
// Returns ErrClientNotStarted if the client is not started.
|
|
||||||
// Returns ErrEngineNotStarted if the engine is not available.
|
|
||||||
func (c *Client) getEngine() (*internal.Engine, error) {
|
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
connect := c.connect
|
connect := c.connect
|
||||||
c.mu.Unlock()
|
|
||||||
|
|
||||||
if connect == nil {
|
if connect == nil {
|
||||||
return nil, ErrClientNotStarted
|
c.mu.Unlock()
|
||||||
|
return nil, netip.Addr{}, errors.New("client not started")
|
||||||
}
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
engine := connect.Engine()
|
engine := connect.Engine()
|
||||||
if engine == nil {
|
if engine == nil {
|
||||||
return nil, ErrEngineNotStarted
|
return nil, netip.Addr{}, errors.New("engine not started")
|
||||||
}
|
|
||||||
|
|
||||||
return engine, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) getNet() (*wgnetstack.Net, netip.Addr, error) {
|
|
||||||
engine, err := c.getEngine()
|
|
||||||
if err != nil {
|
|
||||||
return nil, netip.Addr{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := engine.Address()
|
addr, err := engine.Address()
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewFirewall creates a firewall manager instance
|
// NewFirewall creates a firewall manager instance
|
||||||
func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool, mtu uint16) (firewall.Manager, error) {
|
func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool) (firewall.Manager, error) {
|
||||||
if !iface.IsUserspaceBind() {
|
if !iface.IsUserspaceBind() {
|
||||||
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
|
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use userspace packet filtering firewall
|
// use userspace packet filtering firewall
|
||||||
fm, err := uspfilter.Create(iface, disableServerRoutes, flowLogger, mtu)
|
fm, err := uspfilter.Create(iface, disableServerRoutes, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK"
|
|||||||
// FWType is the type for the firewall type
|
// FWType is the type for the firewall type
|
||||||
type FWType int
|
type FWType int
|
||||||
|
|
||||||
func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool, mtu uint16) (firewall.Manager, error) {
|
func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool) (firewall.Manager, error) {
|
||||||
// on the linux system we try to user nftables or iptables
|
// on the linux system we try to user nftables or iptables
|
||||||
// in any case, because we need to allow netbird interface traffic
|
// in any case, because we need to allow netbird interface traffic
|
||||||
// so we use AllowNetbird traffic from these firewall managers
|
// so we use AllowNetbird traffic from these firewall managers
|
||||||
// for the userspace packet filtering firewall
|
// for the userspace packet filtering firewall
|
||||||
fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes, mtu)
|
fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes)
|
||||||
|
|
||||||
if !iface.IsUserspaceBind() {
|
if !iface.IsUserspaceBind() {
|
||||||
return fm, err
|
return fm, err
|
||||||
@@ -48,11 +48,11 @@ func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, flowLogg
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err)
|
log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err)
|
||||||
}
|
}
|
||||||
return createUserspaceFirewall(iface, fm, disableServerRoutes, flowLogger, mtu)
|
return createUserspaceFirewall(iface, fm, disableServerRoutes, flowLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool, mtu uint16) (firewall.Manager, error) {
|
func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool) (firewall.Manager, error) {
|
||||||
fm, err := createFW(iface, mtu)
|
fm, err := createFW(iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create firewall: %s", err)
|
return nil, fmt.Errorf("create firewall: %s", err)
|
||||||
}
|
}
|
||||||
@@ -64,26 +64,26 @@ func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager,
|
|||||||
return fm, nil
|
return fm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFW(iface IFaceMapper, mtu uint16) (firewall.Manager, error) {
|
func createFW(iface IFaceMapper) (firewall.Manager, error) {
|
||||||
switch check() {
|
switch check() {
|
||||||
case IPTABLES:
|
case IPTABLES:
|
||||||
log.Info("creating an iptables firewall manager")
|
log.Info("creating an iptables firewall manager")
|
||||||
return nbiptables.Create(iface, mtu)
|
return nbiptables.Create(iface)
|
||||||
case NFTABLES:
|
case NFTABLES:
|
||||||
log.Info("creating an nftables firewall manager")
|
log.Info("creating an nftables firewall manager")
|
||||||
return nbnftables.Create(iface, mtu)
|
return nbnftables.Create(iface)
|
||||||
default:
|
default:
|
||||||
log.Info("no firewall manager found, trying to use userspace packet filtering firewall")
|
log.Info("no firewall manager found, trying to use userspace packet filtering firewall")
|
||||||
return nil, errors.New("no firewall manager found")
|
return nil, errors.New("no firewall manager found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger, mtu uint16) (firewall.Manager, error) {
|
func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (firewall.Manager, error) {
|
||||||
var errUsp error
|
var errUsp error
|
||||||
if fm != nil {
|
if fm != nil {
|
||||||
fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes, flowLogger, mtu)
|
fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes, flowLogger)
|
||||||
} else {
|
} else {
|
||||||
fm, errUsp = uspfilter.Create(iface, disableServerRoutes, flowLogger, mtu)
|
fm, errUsp = uspfilter.Create(iface, disableServerRoutes, flowLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errUsp != nil {
|
if errUsp != nil {
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
ipset "github.com/lrh3321/ipset-go"
|
"github.com/nadoo/ipset"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -41,13 +40,19 @@ type aclManager struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newAclManager(iptablesClient *iptables.IPTables, wgIface iFaceMapper) (*aclManager, error) {
|
func newAclManager(iptablesClient *iptables.IPTables, wgIface iFaceMapper) (*aclManager, error) {
|
||||||
return &aclManager{
|
m := &aclManager{
|
||||||
iptablesClient: iptablesClient,
|
iptablesClient: iptablesClient,
|
||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
entries: make(map[string][][]string),
|
entries: make(map[string][][]string),
|
||||||
optionalEntries: make(map[string][]entry),
|
optionalEntries: make(map[string][]entry),
|
||||||
ipsetStore: newIpsetStore(),
|
ipsetStore: newIpsetStore(),
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
if err := ipset.Init(); err != nil {
|
||||||
|
return nil, fmt.Errorf("init ipset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *aclManager) init(stateManager *statemanager.Manager) error {
|
func (m *aclManager) init(stateManager *statemanager.Manager) error {
|
||||||
@@ -93,8 +98,8 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
specs = append(specs, "-j", actionToStr(action))
|
specs = append(specs, "-j", actionToStr(action))
|
||||||
if ipsetName != "" {
|
if ipsetName != "" {
|
||||||
if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists {
|
if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists {
|
||||||
if err := m.addToIPSet(ipsetName, ip); err != nil {
|
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
||||||
return nil, fmt.Errorf("add IP to ipset: %w", err)
|
return nil, fmt.Errorf("failed to add IP to ipset: %w", err)
|
||||||
}
|
}
|
||||||
// if ruleset already exists it means we already have the firewall rule
|
// if ruleset already exists it means we already have the firewall rule
|
||||||
// so we need to update IPs in the ruleset and return new fw.Rule object for ACL manager.
|
// so we need to update IPs in the ruleset and return new fw.Rule object for ACL manager.
|
||||||
@@ -108,18 +113,14 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
}}, nil
|
}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.flushIPSet(ipsetName); err != nil {
|
if err := ipset.Flush(ipsetName); err != nil {
|
||||||
if errors.Is(err, ipset.ErrSetNotExist) {
|
log.Errorf("flush ipset %s before use it: %s", ipsetName, err)
|
||||||
log.Debugf("flush ipset %s before use: %v", ipsetName, err)
|
|
||||||
} else {
|
|
||||||
log.Errorf("flush ipset %s before use: %v", ipsetName, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err := m.createIPSet(ipsetName); err != nil {
|
if err := ipset.Create(ipsetName); err != nil {
|
||||||
return nil, fmt.Errorf("create ipset: %w", err)
|
return nil, fmt.Errorf("failed to create ipset: %w", err)
|
||||||
}
|
}
|
||||||
if err := m.addToIPSet(ipsetName, ip); err != nil {
|
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
||||||
return nil, fmt.Errorf("add IP to ipset: %w", err)
|
return nil, fmt.Errorf("failed to add IP to ipset: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ipList := newIpList(ip.String())
|
ipList := newIpList(ip.String())
|
||||||
@@ -171,16 +172,11 @@ func (m *aclManager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
return fmt.Errorf("invalid rule type")
|
return fmt.Errorf("invalid rule type")
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldDestroyIpset := false
|
|
||||||
if ipsetList, ok := m.ipsetStore.ipset(r.ipsetName); ok {
|
if ipsetList, ok := m.ipsetStore.ipset(r.ipsetName); ok {
|
||||||
// delete IP from ruleset IPs list and ipset
|
// delete IP from ruleset IPs list and ipset
|
||||||
if _, ok := ipsetList.ips[r.ip]; ok {
|
if _, ok := ipsetList.ips[r.ip]; ok {
|
||||||
ip := net.ParseIP(r.ip)
|
if err := ipset.Del(r.ipsetName, r.ip); err != nil {
|
||||||
if ip == nil {
|
return fmt.Errorf("failed to delete ip from ipset: %w", err)
|
||||||
return fmt.Errorf("parse IP %s", r.ip)
|
|
||||||
}
|
|
||||||
if err := m.delFromIPSet(r.ipsetName, ip); err != nil {
|
|
||||||
return fmt.Errorf("delete ip from ipset: %w", err)
|
|
||||||
}
|
}
|
||||||
delete(ipsetList.ips, r.ip)
|
delete(ipsetList.ips, r.ip)
|
||||||
}
|
}
|
||||||
@@ -194,7 +190,10 @@ func (m *aclManager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
// we delete last IP from the set, that means we need to delete
|
// we delete last IP from the set, that means we need to delete
|
||||||
// set itself and associated firewall rule too
|
// set itself and associated firewall rule too
|
||||||
m.ipsetStore.deleteIpset(r.ipsetName)
|
m.ipsetStore.deleteIpset(r.ipsetName)
|
||||||
shouldDestroyIpset = true
|
|
||||||
|
if err := ipset.Destroy(r.ipsetName); err != nil {
|
||||||
|
log.Errorf("delete empty ipset: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.iptablesClient.Delete(tableName, r.chain, r.specs...); err != nil {
|
if err := m.iptablesClient.Delete(tableName, r.chain, r.specs...); err != nil {
|
||||||
@@ -207,16 +206,6 @@ func (m *aclManager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if shouldDestroyIpset {
|
|
||||||
if err := m.destroyIPSet(r.ipsetName); err != nil {
|
|
||||||
if errors.Is(err, ipset.ErrBusy) || errors.Is(err, ipset.ErrSetNotExist) {
|
|
||||||
log.Debugf("destroy empty ipset: %v", err)
|
|
||||||
} else {
|
|
||||||
log.Errorf("destroy empty ipset: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -275,19 +264,11 @@ func (m *aclManager) cleanChains() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, ipsetName := range m.ipsetStore.ipsetNames() {
|
for _, ipsetName := range m.ipsetStore.ipsetNames() {
|
||||||
if err := m.flushIPSet(ipsetName); err != nil {
|
if err := ipset.Flush(ipsetName); err != nil {
|
||||||
if errors.Is(err, ipset.ErrSetNotExist) {
|
log.Errorf("flush ipset %q during reset: %v", ipsetName, err)
|
||||||
log.Debugf("flush ipset %q during reset: %v", ipsetName, err)
|
|
||||||
} else {
|
|
||||||
log.Errorf("flush ipset %q during reset: %v", ipsetName, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if err := m.destroyIPSet(ipsetName); err != nil {
|
if err := ipset.Destroy(ipsetName); err != nil {
|
||||||
if errors.Is(err, ipset.ErrBusy) || errors.Is(err, ipset.ErrSetNotExist) {
|
log.Errorf("delete ipset %q during reset: %v", ipsetName, err)
|
||||||
log.Debugf("destroy ipset %q during reset: %v", ipsetName, err)
|
|
||||||
} else {
|
|
||||||
log.Errorf("destroy ipset %q during reset: %v", ipsetName, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
m.ipsetStore.deleteIpset(ipsetName)
|
m.ipsetStore.deleteIpset(ipsetName)
|
||||||
}
|
}
|
||||||
@@ -387,8 +368,8 @@ func (m *aclManager) updateState() {
|
|||||||
// filterRuleSpecs returns the specs of a filtering rule
|
// filterRuleSpecs returns the specs of a filtering rule
|
||||||
func filterRuleSpecs(ip net.IP, protocol string, sPort, dPort *firewall.Port, action firewall.Action, ipsetName string) (specs []string) {
|
func filterRuleSpecs(ip net.IP, protocol string, sPort, dPort *firewall.Port, action firewall.Action, ipsetName string) (specs []string) {
|
||||||
matchByIP := true
|
matchByIP := true
|
||||||
// don't use IP matching if IP is 0.0.0.0
|
// don't use IP matching if IP is ip 0.0.0.0
|
||||||
if ip.IsUnspecified() {
|
if ip.String() == "0.0.0.0" {
|
||||||
matchByIP = false
|
matchByIP = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +400,7 @@ func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port, action fi
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include action in the ipset name to prevent squashing rules with different actions
|
||||||
actionSuffix := ""
|
actionSuffix := ""
|
||||||
if action == firewall.ActionDrop {
|
if action == firewall.ActionDrop {
|
||||||
actionSuffix = "-drop"
|
actionSuffix = "-drop"
|
||||||
@@ -435,61 +417,3 @@ func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port, action fi
|
|||||||
return ipsetName + actionSuffix
|
return ipsetName + actionSuffix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *aclManager) createIPSet(name string) error {
|
|
||||||
opts := ipset.CreateOptions{
|
|
||||||
Replace: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ipset.Create(name, ipset.TypeHashNet, opts); err != nil {
|
|
||||||
return fmt.Errorf("create ipset %s: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("created ipset %s with type hash:net", name)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *aclManager) addToIPSet(name string, ip net.IP) error {
|
|
||||||
cidr := uint8(32)
|
|
||||||
if ip.To4() == nil {
|
|
||||||
cidr = 128
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := &ipset.Entry{
|
|
||||||
IP: ip,
|
|
||||||
CIDR: cidr,
|
|
||||||
Replace: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ipset.Add(name, entry); err != nil {
|
|
||||||
return fmt.Errorf("add IP to ipset %s: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *aclManager) delFromIPSet(name string, ip net.IP) error {
|
|
||||||
cidr := uint8(32)
|
|
||||||
if ip.To4() == nil {
|
|
||||||
cidr = 128
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := &ipset.Entry{
|
|
||||||
IP: ip,
|
|
||||||
CIDR: cidr,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ipset.Del(name, entry); err != nil {
|
|
||||||
return fmt.Errorf("delete IP from ipset %s: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *aclManager) flushIPSet(name string) error {
|
|
||||||
return ipset.Flush(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *aclManager) destroyIPSet(name string) error {
|
|
||||||
return ipset.Destroy(name)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ type iFaceMapper interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create iptables firewall manager
|
// Create iptables firewall manager
|
||||||
func Create(wgIface iFaceMapper, mtu uint16) (*Manager, error) {
|
func Create(wgIface iFaceMapper) (*Manager, error) {
|
||||||
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("init iptables: %w", err)
|
return nil, fmt.Errorf("init iptables: %w", err)
|
||||||
@@ -47,7 +47,7 @@ func Create(wgIface iFaceMapper, mtu uint16) (*Manager, error) {
|
|||||||
ipv4Client: iptablesClient,
|
ipv4Client: iptablesClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.router, err = newRouter(iptablesClient, wgIface, mtu)
|
m.router, err = newRouter(iptablesClient, wgIface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create router: %w", err)
|
return nil, fmt.Errorf("create router: %w", err)
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,6 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
|||||||
NameStr: m.wgIface.Name(),
|
NameStr: m.wgIface.Name(),
|
||||||
WGAddress: m.wgIface.Address(),
|
WGAddress: m.wgIface.Address(),
|
||||||
UserspaceBind: m.wgIface.IsUserspaceBind(),
|
UserspaceBind: m.wgIface.IsUserspaceBind(),
|
||||||
MTU: m.router.mtu,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateManager.RegisterState(state)
|
stateManager.RegisterState(state)
|
||||||
@@ -261,22 +260,6 @@ func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
|||||||
return m.router.UpdateSet(set, prefixes)
|
return m.router.UpdateSet(set, prefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
|
|
||||||
func (m *Manager) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
return m.router.AddInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveInboundDNAT removes an inbound DNAT rule.
|
|
||||||
func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
return m.router.RemoveInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConntrackEstablished() []string {
|
func getConntrackEstablished() []string {
|
||||||
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
|
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
manager, err := Create(ifaceMock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
@@ -115,7 +114,7 @@ func TestIptablesManagerDenyRules(t *testing.T) {
|
|||||||
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
manager, err := Create(ifaceMock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
@@ -199,7 +198,7 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock, iface.DefaultMTU)
|
manager, err := Create(mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
@@ -265,7 +264,7 @@ func TestIptablesCreatePerformance(t *testing.T) {
|
|||||||
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
||||||
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock, iface.DefaultMTU)
|
manager, err := Create(mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
ipset "github.com/lrh3321/ipset-go"
|
"github.com/nadoo/ipset"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants needed to manage and create iptable rules
|
// constants needed to manage and create iptable rules
|
||||||
@@ -30,20 +30,17 @@ const (
|
|||||||
|
|
||||||
chainPOSTROUTING = "POSTROUTING"
|
chainPOSTROUTING = "POSTROUTING"
|
||||||
chainPREROUTING = "PREROUTING"
|
chainPREROUTING = "PREROUTING"
|
||||||
chainFORWARD = "FORWARD"
|
|
||||||
chainRTNAT = "NETBIRD-RT-NAT"
|
chainRTNAT = "NETBIRD-RT-NAT"
|
||||||
chainRTFWDIN = "NETBIRD-RT-FWD-IN"
|
chainRTFWDIN = "NETBIRD-RT-FWD-IN"
|
||||||
chainRTFWDOUT = "NETBIRD-RT-FWD-OUT"
|
chainRTFWDOUT = "NETBIRD-RT-FWD-OUT"
|
||||||
chainRTPRE = "NETBIRD-RT-PRE"
|
chainRTPRE = "NETBIRD-RT-PRE"
|
||||||
chainRTRDR = "NETBIRD-RT-RDR"
|
chainRTRDR = "NETBIRD-RT-RDR"
|
||||||
chainRTMSSCLAMP = "NETBIRD-RT-MSSCLAMP"
|
|
||||||
routingFinalForwardJump = "ACCEPT"
|
routingFinalForwardJump = "ACCEPT"
|
||||||
routingFinalNatJump = "MASQUERADE"
|
routingFinalNatJump = "MASQUERADE"
|
||||||
|
|
||||||
jumpManglePre = "jump-mangle-pre"
|
jumpManglePre = "jump-mangle-pre"
|
||||||
jumpNatPre = "jump-nat-pre"
|
jumpNatPre = "jump-nat-pre"
|
||||||
jumpNatPost = "jump-nat-post"
|
jumpNatPost = "jump-nat-post"
|
||||||
jumpMSSClamp = "jump-mss-clamp"
|
|
||||||
markManglePre = "mark-mangle-pre"
|
markManglePre = "mark-mangle-pre"
|
||||||
markManglePost = "mark-mangle-post"
|
markManglePost = "mark-mangle-post"
|
||||||
matchSet = "--match-set"
|
matchSet = "--match-set"
|
||||||
@@ -51,9 +48,6 @@ const (
|
|||||||
dnatSuffix = "_dnat"
|
dnatSuffix = "_dnat"
|
||||||
snatSuffix = "_snat"
|
snatSuffix = "_snat"
|
||||||
fwdSuffix = "_fwd"
|
fwdSuffix = "_fwd"
|
||||||
|
|
||||||
// ipTCPHeaderMinSize represents minimum IP (20) + TCP (20) header size for MSS calculation
|
|
||||||
ipTCPHeaderMinSize = 40
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ruleInfo struct {
|
type ruleInfo struct {
|
||||||
@@ -83,18 +77,16 @@ type router struct {
|
|||||||
ipsetCounter *ipsetCounter
|
ipsetCounter *ipsetCounter
|
||||||
wgIface iFaceMapper
|
wgIface iFaceMapper
|
||||||
legacyManagement bool
|
legacyManagement bool
|
||||||
mtu uint16
|
|
||||||
|
|
||||||
stateManager *statemanager.Manager
|
stateManager *statemanager.Manager
|
||||||
ipFwdState *ipfwdstate.IPForwardingState
|
ipFwdState *ipfwdstate.IPForwardingState
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint16) (*router, error) {
|
func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper) (*router, error) {
|
||||||
r := &router{
|
r := &router{
|
||||||
iptablesClient: iptablesClient,
|
iptablesClient: iptablesClient,
|
||||||
rules: make(map[string][]string),
|
rules: make(map[string][]string),
|
||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
mtu: mtu,
|
|
||||||
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +99,10 @@ func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint1
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err := ipset.Init(); err != nil {
|
||||||
|
return nil, fmt.Errorf("init ipset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,12 +224,12 @@ func (r *router) findSets(rule []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) createIpSet(setName string, sources []netip.Prefix) error {
|
func (r *router) createIpSet(setName string, sources []netip.Prefix) error {
|
||||||
if err := r.createIPSet(setName); err != nil {
|
if err := ipset.Create(setName, ipset.OptTimeout(0)); err != nil {
|
||||||
return fmt.Errorf("create set %s: %w", setName, err)
|
return fmt.Errorf("create set %s: %w", setName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, prefix := range sources {
|
for _, prefix := range sources {
|
||||||
if err := r.addPrefixToIPSet(setName, prefix); err != nil {
|
if err := ipset.AddPrefix(setName, prefix); err != nil {
|
||||||
return fmt.Errorf("add element to set %s: %w", setName, err)
|
return fmt.Errorf("add element to set %s: %w", setName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +238,7 @@ func (r *router) createIpSet(setName string, sources []netip.Prefix) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) deleteIpSet(setName string) error {
|
func (r *router) deleteIpSet(setName string) error {
|
||||||
if err := r.destroyIPSet(setName); err != nil {
|
if err := ipset.Destroy(setName); err != nil {
|
||||||
return fmt.Errorf("destroy set %s: %w", setName, err)
|
return fmt.Errorf("destroy set %s: %w", setName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,7 +392,6 @@ func (r *router) cleanUpDefaultForwardRules() error {
|
|||||||
{chainRTPRE, tableMangle},
|
{chainRTPRE, tableMangle},
|
||||||
{chainRTNAT, tableNat},
|
{chainRTNAT, tableNat},
|
||||||
{chainRTRDR, tableNat},
|
{chainRTRDR, tableNat},
|
||||||
{chainRTMSSCLAMP, tableMangle},
|
|
||||||
} {
|
} {
|
||||||
ok, err := r.iptablesClient.ChainExists(chainInfo.table, chainInfo.chain)
|
ok, err := r.iptablesClient.ChainExists(chainInfo.table, chainInfo.chain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -421,7 +416,6 @@ func (r *router) createContainers() error {
|
|||||||
{chainRTPRE, tableMangle},
|
{chainRTPRE, tableMangle},
|
||||||
{chainRTNAT, tableNat},
|
{chainRTNAT, tableNat},
|
||||||
{chainRTRDR, tableNat},
|
{chainRTRDR, tableNat},
|
||||||
{chainRTMSSCLAMP, tableMangle},
|
|
||||||
} {
|
} {
|
||||||
if err := r.iptablesClient.NewChain(chainInfo.table, chainInfo.chain); err != nil {
|
if err := r.iptablesClient.NewChain(chainInfo.table, chainInfo.chain); err != nil {
|
||||||
return fmt.Errorf("create chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err)
|
return fmt.Errorf("create chain %s in table %s: %w", chainInfo.chain, chainInfo.table, err)
|
||||||
@@ -444,10 +438,6 @@ func (r *router) createContainers() error {
|
|||||||
return fmt.Errorf("add jump rules: %w", err)
|
return fmt.Errorf("add jump rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.addMSSClampingRules(); err != nil {
|
|
||||||
log.Errorf("failed to add MSS clamping rules: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,35 +518,6 @@ func (r *router) addPostroutingRules() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// addMSSClampingRules adds MSS clamping rules to prevent fragmentation for forwarded traffic.
|
|
||||||
// TODO: Add IPv6 support
|
|
||||||
func (r *router) addMSSClampingRules() error {
|
|
||||||
mss := r.mtu - ipTCPHeaderMinSize
|
|
||||||
|
|
||||||
// Add jump rule from FORWARD chain in mangle table to our custom chain
|
|
||||||
jumpRule := []string{
|
|
||||||
"-j", chainRTMSSCLAMP,
|
|
||||||
}
|
|
||||||
if err := r.iptablesClient.Insert(tableMangle, chainFORWARD, 1, jumpRule...); err != nil {
|
|
||||||
return fmt.Errorf("add jump to MSS clamp chain: %w", err)
|
|
||||||
}
|
|
||||||
r.rules[jumpMSSClamp] = jumpRule
|
|
||||||
|
|
||||||
ruleOut := []string{
|
|
||||||
"-o", r.wgIface.Name(),
|
|
||||||
"-p", "tcp",
|
|
||||||
"--tcp-flags", "SYN,RST", "SYN",
|
|
||||||
"-j", "TCPMSS",
|
|
||||||
"--set-mss", fmt.Sprintf("%d", mss),
|
|
||||||
}
|
|
||||||
if err := r.iptablesClient.Append(tableMangle, chainRTMSSCLAMP, ruleOut...); err != nil {
|
|
||||||
return fmt.Errorf("add outbound MSS clamp rule: %w", err)
|
|
||||||
}
|
|
||||||
r.rules["mss-clamp-out"] = ruleOut
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) insertEstablishedRule(chain string) error {
|
func (r *router) insertEstablishedRule(chain string) error {
|
||||||
establishedRule := getConntrackEstablished()
|
establishedRule := getConntrackEstablished()
|
||||||
|
|
||||||
@@ -597,7 +558,7 @@ func (r *router) addJumpRules() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) cleanJumpRules() error {
|
func (r *router) cleanJumpRules() error {
|
||||||
for _, ruleKey := range []string{jumpNatPost, jumpManglePre, jumpNatPre, jumpMSSClamp} {
|
for _, ruleKey := range []string{jumpNatPost, jumpManglePre, jumpNatPre} {
|
||||||
if rule, exists := r.rules[ruleKey]; exists {
|
if rule, exists := r.rules[ruleKey]; exists {
|
||||||
var table, chain string
|
var table, chain string
|
||||||
switch ruleKey {
|
switch ruleKey {
|
||||||
@@ -610,9 +571,6 @@ func (r *router) cleanJumpRules() error {
|
|||||||
case jumpNatPre:
|
case jumpNatPre:
|
||||||
table = tableNat
|
table = tableNat
|
||||||
chain = chainPREROUTING
|
chain = chainPREROUTING
|
||||||
case jumpMSSClamp:
|
|
||||||
table = tableMangle
|
|
||||||
chain = chainFORWARD
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unknown jump rule: %s", ruleKey)
|
return fmt.Errorf("unknown jump rule: %s", ruleKey)
|
||||||
}
|
}
|
||||||
@@ -911,8 +869,8 @@ func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
|||||||
log.Tracef("skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
|
log.Tracef("skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := r.addPrefixToIPSet(set.HashedName(), prefix); err != nil {
|
if err := ipset.AddPrefix(set.HashedName(), prefix); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("add prefix to ipset: %w", err))
|
merr = multierror.Append(merr, fmt.Errorf("increment ipset counter: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if merr == nil {
|
if merr == nil {
|
||||||
@@ -922,54 +880,6 @@ func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
|||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
|
|
||||||
func (r *router) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
|
||||||
|
|
||||||
if _, exists := r.rules[ruleID]; exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
dnatRule := []string{
|
|
||||||
"-i", r.wgIface.Name(),
|
|
||||||
"-p", strings.ToLower(string(protocol)),
|
|
||||||
"--dport", strconv.Itoa(int(sourcePort)),
|
|
||||||
"-d", localAddr.String(),
|
|
||||||
"-m", "addrtype", "--dst-type", "LOCAL",
|
|
||||||
"-j", "DNAT",
|
|
||||||
"--to-destination", ":" + strconv.Itoa(int(targetPort)),
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleInfo := ruleInfo{
|
|
||||||
table: tableNat,
|
|
||||||
chain: chainRTRDR,
|
|
||||||
rule: dnatRule,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.iptablesClient.Append(ruleInfo.table, ruleInfo.chain, ruleInfo.rule...); err != nil {
|
|
||||||
return fmt.Errorf("add inbound DNAT rule: %w", err)
|
|
||||||
}
|
|
||||||
r.rules[ruleID] = ruleInfo.rule
|
|
||||||
|
|
||||||
r.updateState()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveInboundDNAT removes an inbound DNAT rule.
|
|
||||||
func (r *router) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
|
||||||
|
|
||||||
if dnatRule, exists := r.rules[ruleID]; exists {
|
|
||||||
if err := r.iptablesClient.Delete(tableNat, chainRTRDR, dnatRule...); err != nil {
|
|
||||||
return fmt.Errorf("delete inbound DNAT rule: %w", err)
|
|
||||||
}
|
|
||||||
delete(r.rules, ruleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.updateState()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyPort(flag string, port *firewall.Port) []string {
|
func applyPort(flag string, port *firewall.Port) []string {
|
||||||
if port == nil {
|
if port == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -989,37 +899,3 @@ func applyPort(flag string, port *firewall.Port) []string {
|
|||||||
|
|
||||||
return []string{flag, strconv.Itoa(int(port.Values[0]))}
|
return []string{flag, strconv.Itoa(int(port.Values[0]))}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) createIPSet(name string) error {
|
|
||||||
opts := ipset.CreateOptions{
|
|
||||||
Replace: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ipset.Create(name, ipset.TypeHashNet, opts); err != nil {
|
|
||||||
return fmt.Errorf("create ipset %s: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("created ipset %s with type hash:net", name)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) addPrefixToIPSet(name string, prefix netip.Prefix) error {
|
|
||||||
addr := prefix.Addr()
|
|
||||||
ip := addr.AsSlice()
|
|
||||||
|
|
||||||
entry := &ipset.Entry{
|
|
||||||
IP: ip,
|
|
||||||
CIDR: uint8(prefix.Bits()),
|
|
||||||
Replace: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ipset.Add(name, entry); err != nil {
|
|
||||||
return fmt.Errorf("add prefix to ipset %s: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) destroyIPSet(name string) error {
|
|
||||||
return ipset.Destroy(name)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import (
|
|||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/test"
|
"github.com/netbirdio/netbird/client/firewall/test"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func isIptablesSupported() bool {
|
func isIptablesSupported() bool {
|
||||||
@@ -31,7 +30,7 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
require.NoError(t, err, "failed to init iptables client")
|
require.NoError(t, err, "failed to init iptables client")
|
||||||
|
|
||||||
manager, err := newRouter(iptablesClient, ifaceMock, iface.DefaultMTU)
|
manager, err := newRouter(iptablesClient, ifaceMock)
|
||||||
require.NoError(t, err, "should return a valid iptables manager")
|
require.NoError(t, err, "should return a valid iptables manager")
|
||||||
require.NoError(t, manager.init(nil))
|
require.NoError(t, manager.init(nil))
|
||||||
|
|
||||||
@@ -39,6 +38,7 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
assert.NoError(t, manager.Reset(), "shouldn't return error")
|
assert.NoError(t, manager.Reset(), "shouldn't return error")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Now 5 rules:
|
||||||
// 1. established rule forward in
|
// 1. established rule forward in
|
||||||
// 2. estbalished rule forward out
|
// 2. estbalished rule forward out
|
||||||
// 3. jump rule to POST nat chain
|
// 3. jump rule to POST nat chain
|
||||||
@@ -48,9 +48,7 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
// 7. static return masquerade rule
|
// 7. static return masquerade rule
|
||||||
// 8. mangle prerouting mark rule
|
// 8. mangle prerouting mark rule
|
||||||
// 9. mangle postrouting mark rule
|
// 9. mangle postrouting mark rule
|
||||||
// 10. jump rule to MSS clamping chain
|
require.Len(t, manager.rules, 9, "should have created rules map")
|
||||||
// 11. MSS clamping rule for outbound traffic
|
|
||||||
require.Len(t, manager.rules, 11, "should have created rules map")
|
|
||||||
|
|
||||||
exists, err := manager.iptablesClient.Exists(tableNat, chainPOSTROUTING, "-j", chainRTNAT)
|
exists, err := manager.iptablesClient.Exists(tableNat, chainPOSTROUTING, "-j", chainRTNAT)
|
||||||
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainPOSTROUTING)
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainPOSTROUTING)
|
||||||
@@ -84,7 +82,7 @@ func TestIptablesManager_AddNatRule(t *testing.T) {
|
|||||||
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
require.NoError(t, err, "failed to init iptables client")
|
require.NoError(t, err, "failed to init iptables client")
|
||||||
|
|
||||||
manager, err := newRouter(iptablesClient, ifaceMock, iface.DefaultMTU)
|
manager, err := newRouter(iptablesClient, ifaceMock)
|
||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
require.NoError(t, manager.init(nil))
|
require.NoError(t, manager.init(nil))
|
||||||
|
|
||||||
@@ -157,7 +155,7 @@ func TestIptablesManager_RemoveNatRule(t *testing.T) {
|
|||||||
t.Run(testCase.Name, func(t *testing.T) {
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
iptablesClient, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
iptablesClient, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
|
||||||
manager, err := newRouter(iptablesClient, ifaceMock, iface.DefaultMTU)
|
manager, err := newRouter(iptablesClient, ifaceMock)
|
||||||
require.NoError(t, err, "shouldn't return error")
|
require.NoError(t, err, "shouldn't return error")
|
||||||
require.NoError(t, manager.init(nil))
|
require.NoError(t, manager.init(nil))
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -219,7 +217,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
require.NoError(t, err, "Failed to create iptables client")
|
require.NoError(t, err, "Failed to create iptables client")
|
||||||
|
|
||||||
r, err := newRouter(iptablesClient, ifaceMock, iface.DefaultMTU)
|
r, err := newRouter(iptablesClient, ifaceMock)
|
||||||
require.NoError(t, err, "Failed to create router manager")
|
require.NoError(t, err, "Failed to create router manager")
|
||||||
require.NoError(t, r.init(nil))
|
require.NoError(t, r.init(nil))
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,7 +11,6 @@ type InterfaceState struct {
|
|||||||
NameStr string `json:"name"`
|
NameStr string `json:"name"`
|
||||||
WGAddress wgaddr.Address `json:"wg_address"`
|
WGAddress wgaddr.Address `json:"wg_address"`
|
||||||
UserspaceBind bool `json:"userspace_bind"`
|
UserspaceBind bool `json:"userspace_bind"`
|
||||||
MTU uint16 `json:"mtu"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *InterfaceState) Name() string {
|
func (i *InterfaceState) Name() string {
|
||||||
@@ -44,11 +42,7 @@ func (s *ShutdownState) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShutdownState) Cleanup() error {
|
func (s *ShutdownState) Cleanup() error {
|
||||||
mtu := s.InterfaceState.MTU
|
ipt, err := Create(s.InterfaceState)
|
||||||
if mtu == 0 {
|
|
||||||
mtu = iface.DefaultMTU
|
|
||||||
}
|
|
||||||
ipt, err := Create(s.InterfaceState, mtu)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create iptables manager: %w", err)
|
return fmt.Errorf("create iptables manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,9 +100,6 @@ type Manager interface {
|
|||||||
//
|
//
|
||||||
// If comment argument is empty firewall manager should set
|
// If comment argument is empty firewall manager should set
|
||||||
// rule ID as comment for the rule
|
// rule ID as comment for the rule
|
||||||
//
|
|
||||||
// Note: Callers should call Flush() after adding rules to ensure
|
|
||||||
// they are applied to the kernel and rule handles are refreshed.
|
|
||||||
AddPeerFiltering(
|
AddPeerFiltering(
|
||||||
id []byte,
|
id []byte,
|
||||||
ip net.IP,
|
ip net.IP,
|
||||||
@@ -154,20 +151,14 @@ type Manager interface {
|
|||||||
|
|
||||||
DisableRouting() error
|
DisableRouting() error
|
||||||
|
|
||||||
// AddDNATRule adds outbound DNAT rule for forwarding external traffic to the NetBird network.
|
// AddDNATRule adds a DNAT rule
|
||||||
AddDNATRule(ForwardRule) (Rule, error)
|
AddDNATRule(ForwardRule) (Rule, error)
|
||||||
|
|
||||||
// DeleteDNATRule deletes the outbound DNAT rule.
|
// DeleteDNATRule deletes a DNAT rule
|
||||||
DeleteDNATRule(Rule) error
|
DeleteDNATRule(Rule) error
|
||||||
|
|
||||||
// UpdateSet updates the set with the given prefixes
|
// UpdateSet updates the set with the given prefixes
|
||||||
UpdateSet(hash Set, prefixes []netip.Prefix) error
|
UpdateSet(hash Set, prefixes []netip.Prefix) error
|
||||||
|
|
||||||
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services
|
|
||||||
AddInboundDNAT(localAddr netip.Addr, protocol Protocol, sourcePort, targetPort uint16) error
|
|
||||||
|
|
||||||
// RemoveInboundDNAT removes inbound DNAT rule
|
|
||||||
RemoveInboundDNAT(localAddr netip.Addr, protocol Protocol, sourcePort, targetPort uint16) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenKey(format string, pair RouterPair) string {
|
func GenKey(format string, pair RouterPair) string {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -29,6 +29,8 @@ const (
|
|||||||
chainNameForwardFilter = "netbird-acl-forward-filter"
|
chainNameForwardFilter = "netbird-acl-forward-filter"
|
||||||
chainNameManglePrerouting = "netbird-mangle-prerouting"
|
chainNameManglePrerouting = "netbird-mangle-prerouting"
|
||||||
chainNameManglePostrouting = "netbird-mangle-postrouting"
|
chainNameManglePostrouting = "netbird-mangle-postrouting"
|
||||||
|
|
||||||
|
allowNetbirdInputRuleID = "allow Netbird incoming traffic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const flushError = "flush: %w"
|
const flushError = "flush: %w"
|
||||||
@@ -193,6 +195,25 @@ func (m *AclManager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
// createDefaultAllowRules creates default allow rules for the input and output chains
|
// createDefaultAllowRules creates default allow rules for the input and output chains
|
||||||
func (m *AclManager) createDefaultAllowRules() error {
|
func (m *AclManager) createDefaultAllowRules() error {
|
||||||
expIn := []expr.Any{
|
expIn := []expr.Any{
|
||||||
|
&expr.Payload{
|
||||||
|
DestRegister: 1,
|
||||||
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
|
Offset: 12,
|
||||||
|
Len: 4,
|
||||||
|
},
|
||||||
|
// mask
|
||||||
|
&expr.Bitwise{
|
||||||
|
SourceRegister: 1,
|
||||||
|
DestRegister: 1,
|
||||||
|
Len: 4,
|
||||||
|
Mask: []byte{0, 0, 0, 0},
|
||||||
|
Xor: []byte{0, 0, 0, 0},
|
||||||
|
},
|
||||||
|
// net address
|
||||||
|
&expr.Cmp{
|
||||||
|
Register: 1,
|
||||||
|
Data: []byte{0, 0, 0, 0},
|
||||||
|
},
|
||||||
&expr.Verdict{
|
&expr.Verdict{
|
||||||
Kind: expr.VerdictAccept,
|
Kind: expr.VerdictAccept,
|
||||||
},
|
},
|
||||||
@@ -237,7 +258,7 @@ func (m *AclManager) addIOFiltering(
|
|||||||
action firewall.Action,
|
action firewall.Action,
|
||||||
ipset *nftables.Set,
|
ipset *nftables.Set,
|
||||||
) (*Rule, error) {
|
) (*Rule, error) {
|
||||||
ruleId := generatePeerRuleId(ip, proto, sPort, dPort, action, ipset)
|
ruleId := generatePeerRuleId(ip, sPort, dPort, action, ipset)
|
||||||
if r, ok := m.rules[ruleId]; ok {
|
if r, ok := m.rules[ruleId]; ok {
|
||||||
return &Rule{
|
return &Rule{
|
||||||
nftRule: r.nftRule,
|
nftRule: r.nftRule,
|
||||||
@@ -336,12 +357,11 @@ func (m *AclManager) addIOFiltering(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
if err := m.rConn.Flush(); err != nil {
|
||||||
return nil, fmt.Errorf("flush input rule %s: %v", ruleId, err)
|
return nil, fmt.Errorf(flushError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleStruct := &Rule{
|
ruleStruct := &Rule{
|
||||||
nftRule: nftRule,
|
nftRule: nftRule,
|
||||||
// best effort mangle rule
|
|
||||||
mangleRule: m.createPreroutingRule(expressions, userData),
|
mangleRule: m.createPreroutingRule(expressions, userData),
|
||||||
nftSet: ipset,
|
nftSet: ipset,
|
||||||
ruleID: ruleId,
|
ruleID: ruleId,
|
||||||
@@ -400,19 +420,12 @@ func (m *AclManager) createPreroutingRule(expressions []expr.Any, userData []byt
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
nfRule := m.rConn.AddRule(&nftables.Rule{
|
return m.rConn.AddRule(&nftables.Rule{
|
||||||
Table: m.workTable,
|
Table: m.workTable,
|
||||||
Chain: m.chainPrerouting,
|
Chain: m.chainPrerouting,
|
||||||
Exprs: preroutingExprs,
|
Exprs: preroutingExprs,
|
||||||
UserData: userData,
|
UserData: userData,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
|
||||||
log.Errorf("failed to flush mangle rule %s: %v", string(userData), err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nfRule
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AclManager) createDefaultChains() (err error) {
|
func (m *AclManager) createDefaultChains() (err error) {
|
||||||
@@ -684,8 +697,8 @@ func (m *AclManager) refreshRuleHandles(chain *nftables.Chain, mangle bool) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generatePeerRuleId(ip net.IP, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, ipset *nftables.Set) string {
|
func generatePeerRuleId(ip net.IP, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action, ipset *nftables.Set) string {
|
||||||
rulesetID := ":" + string(proto) + ":"
|
rulesetID := ":"
|
||||||
if sPort != nil {
|
if sPort != nil {
|
||||||
rulesetID += sPort.String()
|
rulesetID += sPort.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
@@ -19,22 +19,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// tableNameNetbird is the default name of the table that is used for filtering by the Netbird client
|
// tableNameNetbird is the name of the table that is used for filtering by the Netbird client
|
||||||
tableNameNetbird = "netbird"
|
tableNameNetbird = "netbird"
|
||||||
// envTableName is the environment variable to override the table name
|
|
||||||
envTableName = "NB_NFTABLES_TABLE"
|
|
||||||
|
|
||||||
tableNameFilter = "filter"
|
tableNameFilter = "filter"
|
||||||
chainNameInput = "INPUT"
|
chainNameInput = "INPUT"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTableName() string {
|
|
||||||
if name := os.Getenv(envTableName); name != "" {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return tableNameNetbird
|
|
||||||
}
|
|
||||||
|
|
||||||
// iFaceMapper defines subset methods of interface required for manager
|
// iFaceMapper defines subset methods of interface required for manager
|
||||||
type iFaceMapper interface {
|
type iFaceMapper interface {
|
||||||
Name() string
|
Name() string
|
||||||
@@ -53,16 +44,16 @@ type Manager struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create nftables firewall manager
|
// Create nftables firewall manager
|
||||||
func Create(wgIface iFaceMapper, mtu uint16) (*Manager, error) {
|
func Create(wgIface iFaceMapper) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
rConn: &nftables.Conn{},
|
rConn: &nftables.Conn{},
|
||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
}
|
}
|
||||||
|
|
||||||
workTable := &nftables.Table{Name: getTableName(), Family: nftables.TableFamilyIPv4}
|
workTable := &nftables.Table{Name: tableNameNetbird, Family: nftables.TableFamilyIPv4}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
m.router, err = newRouter(workTable, wgIface, mtu)
|
m.router, err = newRouter(workTable, wgIface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create router: %w", err)
|
return nil, fmt.Errorf("create router: %w", err)
|
||||||
}
|
}
|
||||||
@@ -102,7 +93,6 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
|||||||
NameStr: m.wgIface.Name(),
|
NameStr: m.wgIface.Name(),
|
||||||
WGAddress: m.wgIface.Address(),
|
WGAddress: m.wgIface.Address(),
|
||||||
UserspaceBind: m.wgIface.IsUserspaceBind(),
|
UserspaceBind: m.wgIface.IsUserspaceBind(),
|
||||||
MTU: m.router.mtu,
|
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Errorf("failed to update state: %v", err)
|
log.Errorf("failed to update state: %v", err)
|
||||||
@@ -207,11 +197,44 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
if err := m.aclManager.createDefaultAllowRules(); err != nil {
|
err := m.aclManager.createDefaultAllowRules()
|
||||||
return fmt.Errorf("create default allow rules: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create default allow rules: %v", err)
|
||||||
}
|
}
|
||||||
if err := m.rConn.Flush(); err != nil {
|
|
||||||
return fmt.Errorf("flush allow input netbird rules: %w", err)
|
chains, err := m.rConn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list of chains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var chain *nftables.Chain
|
||||||
|
for _, c := range chains {
|
||||||
|
if c.Table.Name == tableNameFilter && c.Name == chainNameInput {
|
||||||
|
chain = c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chain == nil {
|
||||||
|
log.Debugf("chain INPUT not found. Skipping add allow netbird rule")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err := m.rConn.GetRules(chain.Table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get rules for the INPUT chain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule := m.detectAllowNetbirdRule(rules); rule != nil {
|
||||||
|
log.Debugf("allow netbird rule already exists: %v", rule)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.applyAllowNetbirdRules(chain)
|
||||||
|
|
||||||
|
err = m.rConn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to flush allow input netbird rules: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -227,6 +250,10 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
|||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
if err := m.resetNetbirdInputRules(); err != nil {
|
||||||
|
return fmt.Errorf("reset netbird input rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.router.Reset(); err != nil {
|
if err := m.router.Reset(); err != nil {
|
||||||
return fmt.Errorf("reset router: %v", err)
|
return fmt.Errorf("reset router: %v", err)
|
||||||
}
|
}
|
||||||
@@ -246,15 +273,49 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) resetNetbirdInputRules() error {
|
||||||
|
chains, err := m.rConn.ListChains()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list chains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.deleteNetbirdInputRules(chains)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) deleteNetbirdInputRules(chains []*nftables.Chain) {
|
||||||
|
for _, c := range chains {
|
||||||
|
if c.Table.Name == tableNameFilter && c.Name == chainNameInput {
|
||||||
|
rules, err := m.rConn.GetRules(c.Table, c)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get rules for chain %q: %v", c.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
m.deleteMatchingRules(rules)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) deleteMatchingRules(rules []*nftables.Rule) {
|
||||||
|
for _, r := range rules {
|
||||||
|
if bytes.Equal(r.UserData, []byte(allowNetbirdInputRuleID)) {
|
||||||
|
if err := m.rConn.DelRule(r); err != nil {
|
||||||
|
log.Errorf("delete rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) cleanupNetbirdTables() error {
|
func (m *Manager) cleanupNetbirdTables() error {
|
||||||
tables, err := m.rConn.ListTables()
|
tables, err := m.rConn.ListTables()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list tables: %w", err)
|
return fmt.Errorf("list tables: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableName := getTableName()
|
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
if t.Name == tableName {
|
if t.Name == tableNameNetbird {
|
||||||
m.rConn.DelTable(t)
|
m.rConn.DelTable(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -315,40 +376,61 @@ func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
|||||||
return m.router.UpdateSet(set, prefixes)
|
return m.router.UpdateSet(set, prefixes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
|
|
||||||
func (m *Manager) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
return m.router.AddInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveInboundDNAT removes an inbound DNAT rule.
|
|
||||||
func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
return m.router.RemoveInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) createWorkTable() (*nftables.Table, error) {
|
func (m *Manager) createWorkTable() (*nftables.Table, error) {
|
||||||
tables, err := m.rConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
tables, err := m.rConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list of tables: %w", err)
|
return nil, fmt.Errorf("list of tables: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableName := getTableName()
|
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
if t.Name == tableName {
|
if t.Name == tableNameNetbird {
|
||||||
m.rConn.DelTable(t)
|
m.rConn.DelTable(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table := m.rConn.AddTable(&nftables.Table{Name: getTableName(), Family: nftables.TableFamilyIPv4})
|
table := m.rConn.AddTable(&nftables.Table{Name: tableNameNetbird, Family: nftables.TableFamilyIPv4})
|
||||||
err = m.rConn.Flush()
|
err = m.rConn.Flush()
|
||||||
return table, err
|
return table, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) applyAllowNetbirdRules(chain *nftables.Chain) {
|
||||||
|
rule := &nftables.Rule{
|
||||||
|
Table: chain.Table,
|
||||||
|
Chain: chain,
|
||||||
|
Exprs: []expr.Any{
|
||||||
|
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: ifname(m.wgIface.Name()),
|
||||||
|
},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserData: []byte(allowNetbirdInputRuleID),
|
||||||
|
}
|
||||||
|
_ = m.rConn.InsertRule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) detectAllowNetbirdRule(existedRules []*nftables.Rule) *nftables.Rule {
|
||||||
|
ifName := ifname(m.wgIface.Name())
|
||||||
|
for _, rule := range existedRules {
|
||||||
|
if rule.Table.Name == tableNameFilter && rule.Chain.Name == chainNameInput {
|
||||||
|
if len(rule.Exprs) < 4 {
|
||||||
|
if e, ok := rule.Exprs[0].(*expr.Meta); !ok || e.Key != expr.MetaKeyIIFNAME {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e, ok := rule.Exprs[1].(*expr.Cmp); !ok || e.Op != expr.CmpOpEq || !bytes.Equal(e.Data, ifName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func insertReturnTrafficRule(conn *nftables.Conn, table *nftables.Table, chain *nftables.Chain) {
|
func insertReturnTrafficRule(conn *nftables.Conn, table *nftables.Table, chain *nftables.Chain) {
|
||||||
rule := &nftables.Rule{
|
rule := &nftables.Rule{
|
||||||
Table: table,
|
Table: table,
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ func (i *iFaceMock) IsUserspaceBind() bool { return false }
|
|||||||
func TestNftablesManager(t *testing.T) {
|
func TestNftablesManager(t *testing.T) {
|
||||||
|
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
manager, err := Create(ifaceMock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
time.Sleep(time.Second * 3)
|
time.Sleep(time.Second * 3)
|
||||||
@@ -169,7 +168,7 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
func TestNftablesManagerRuleOrder(t *testing.T) {
|
func TestNftablesManagerRuleOrder(t *testing.T) {
|
||||||
// This test verifies rule insertion order in nftables peer ACLs
|
// This test verifies rule insertion order in nftables peer ACLs
|
||||||
// We add accept rule first, then deny rule to test ordering behavior
|
// We add accept rule first, then deny rule to test ordering behavior
|
||||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
manager, err := Create(ifaceMock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
@@ -262,7 +261,7 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
|||||||
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
||||||
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock, iface.DefaultMTU)
|
manager, err := Create(mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
time.Sleep(time.Second * 3)
|
time.Sleep(time.Second * 3)
|
||||||
@@ -346,7 +345,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
|||||||
stdout, stderr := runIptablesSave(t)
|
stdout, stderr := runIptablesSave(t)
|
||||||
verifyIptablesOutput(t, stdout, stderr)
|
verifyIptablesOutput(t, stdout, stderr)
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
manager, err := Create(ifaceMock)
|
||||||
require.NoError(t, err, "failed to create manager")
|
require.NoError(t, err, "failed to create manager")
|
||||||
require.NoError(t, manager.Init(nil))
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ import (
|
|||||||
"github.com/google/nftables/xt"
|
"github.com/google/nftables/xt"
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
nbid "github.com/netbirdio/netbird/client/internal/acl/id"
|
nbid "github.com/netbirdio/netbird/client/internal/acl/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
"github.com/netbirdio/netbird/client/internal/routemanager/ipfwdstate"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,17 +32,12 @@ const (
|
|||||||
chainNameRoutingNat = "netbird-rt-postrouting"
|
chainNameRoutingNat = "netbird-rt-postrouting"
|
||||||
chainNameRoutingRdr = "netbird-rt-redirect"
|
chainNameRoutingRdr = "netbird-rt-redirect"
|
||||||
chainNameForward = "FORWARD"
|
chainNameForward = "FORWARD"
|
||||||
chainNameMangleForward = "netbird-mangle-forward"
|
|
||||||
|
|
||||||
userDataAcceptForwardRuleIif = "frwacceptiif"
|
userDataAcceptForwardRuleIif = "frwacceptiif"
|
||||||
userDataAcceptForwardRuleOif = "frwacceptoif"
|
userDataAcceptForwardRuleOif = "frwacceptoif"
|
||||||
userDataAcceptInputRule = "inputaccept"
|
|
||||||
|
|
||||||
dnatSuffix = "_dnat"
|
dnatSuffix = "_dnat"
|
||||||
snatSuffix = "_snat"
|
snatSuffix = "_snat"
|
||||||
|
|
||||||
// ipTCPHeaderMinSize represents minimum IP (20) + TCP (20) header size for MSS calculation
|
|
||||||
ipTCPHeaderMinSize = 40
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const refreshRulesMapError = "refresh rules map: %w"
|
const refreshRulesMapError = "refresh rules map: %w"
|
||||||
@@ -69,10 +63,9 @@ type router struct {
|
|||||||
wgIface iFaceMapper
|
wgIface iFaceMapper
|
||||||
ipFwdState *ipfwdstate.IPForwardingState
|
ipFwdState *ipfwdstate.IPForwardingState
|
||||||
legacyManagement bool
|
legacyManagement bool
|
||||||
mtu uint16
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*router, error) {
|
func newRouter(workTable *nftables.Table, wgIface iFaceMapper) (*router, error) {
|
||||||
r := &router{
|
r := &router{
|
||||||
conn: &nftables.Conn{},
|
conn: &nftables.Conn{},
|
||||||
workTable: workTable,
|
workTable: workTable,
|
||||||
@@ -80,7 +73,6 @@ func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*rou
|
|||||||
rules: make(map[string]*nftables.Rule),
|
rules: make(map[string]*nftables.Rule),
|
||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
||||||
mtu: mtu,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ipsetCounter = refcounter.New(
|
r.ipsetCounter = refcounter.New(
|
||||||
@@ -91,7 +83,11 @@ func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*rou
|
|||||||
var err error
|
var err error
|
||||||
r.filterTable, err = r.loadFilterTable()
|
r.filterTable, err = r.loadFilterTable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to load filter table, skipping accept rules: %v", err)
|
if errors.Is(err, errFilterTableNotFound) {
|
||||||
|
log.Warnf("table 'filter' not found for forward rules")
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("load filter table: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
@@ -100,8 +96,8 @@ func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*rou
|
|||||||
func (r *router) init(workTable *nftables.Table) error {
|
func (r *router) init(workTable *nftables.Table) error {
|
||||||
r.workTable = workTable
|
r.workTable = workTable
|
||||||
|
|
||||||
if err := r.removeAcceptFilterRules(); err != nil {
|
if err := r.removeAcceptForwardRules(); err != nil {
|
||||||
log.Errorf("failed to clean up rules from filter table: %s", err)
|
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.createContainers(); err != nil {
|
if err := r.createContainers(); err != nil {
|
||||||
@@ -115,15 +111,15 @@ func (r *router) init(workTable *nftables.Table) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset cleans existing nftables filter table rules from the system
|
// Reset cleans existing nftables default forward rules from the system
|
||||||
func (r *router) Reset() error {
|
func (r *router) Reset() error {
|
||||||
// clear without deleting the ipsets, the nf table will be deleted by the caller
|
// clear without deleting the ipsets, the nf table will be deleted by the caller
|
||||||
r.ipsetCounter.Clear()
|
r.ipsetCounter.Clear()
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
if err := r.removeAcceptFilterRules(); err != nil {
|
if err := r.removeAcceptForwardRules(); err != nil {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("remove accept filter rules: %w", err))
|
merr = multierror.Append(merr, fmt.Errorf("remove accept forward rules: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeNatPreroutingRules(); err != nil {
|
if err := r.removeNatPreroutingRules(); err != nil {
|
||||||
@@ -171,7 +167,7 @@ func (r *router) removeNatPreroutingRules() error {
|
|||||||
func (r *router) loadFilterTable() (*nftables.Table, error) {
|
func (r *router) loadFilterTable() (*nftables.Table, error) {
|
||||||
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("list tables: %w", err)
|
return nil, fmt.Errorf("unable to list tables: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
@@ -189,6 +185,8 @@ func (r *router) createContainers() error {
|
|||||||
Table: r.workTable,
|
Table: r.workTable,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
insertReturnTrafficRule(r.conn, r.workTable, r.chains[chainNameRoutingFw])
|
||||||
|
|
||||||
prio := *nftables.ChainPriorityNATSource - 1
|
prio := *nftables.ChainPriorityNATSource - 1
|
||||||
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
||||||
Name: chainNameRoutingNat,
|
Name: chainNameRoutingNat,
|
||||||
@@ -222,24 +220,9 @@ func (r *router) createContainers() error {
|
|||||||
Type: nftables.ChainTypeFilter,
|
Type: nftables.ChainTypeFilter,
|
||||||
})
|
})
|
||||||
|
|
||||||
r.chains[chainNameMangleForward] = r.conn.AddChain(&nftables.Chain{
|
// Add the single NAT rule that matches on mark
|
||||||
Name: chainNameMangleForward,
|
if err := r.addPostroutingRules(); err != nil {
|
||||||
Table: r.workTable,
|
return fmt.Errorf("add single nat rule: %v", err)
|
||||||
Hooknum: nftables.ChainHookForward,
|
|
||||||
Priority: nftables.ChainPriorityMangle,
|
|
||||||
Type: nftables.ChainTypeFilter,
|
|
||||||
})
|
|
||||||
|
|
||||||
insertReturnTrafficRule(r.conn, r.workTable, r.chains[chainNameRoutingFw])
|
|
||||||
|
|
||||||
r.addPostroutingRules()
|
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
|
||||||
return fmt.Errorf("initialize tables: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.addMSSClampingRules(); err != nil {
|
|
||||||
log.Errorf("failed to add MSS clamping rules: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.acceptForwardRules(); err != nil {
|
if err := r.acceptForwardRules(); err != nil {
|
||||||
@@ -247,7 +230,11 @@ func (r *router) createContainers() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
log.Errorf("failed to refresh rules: %s", err)
|
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("initialize tables: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -688,7 +675,7 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addPostroutingRules adds the masquerade rules
|
// addPostroutingRules adds the masquerade rules
|
||||||
func (r *router) addPostroutingRules() {
|
func (r *router) addPostroutingRules() error {
|
||||||
// First masquerade rule for traffic coming in from WireGuard interface
|
// First masquerade rule for traffic coming in from WireGuard interface
|
||||||
exprs := []expr.Any{
|
exprs := []expr.Any{
|
||||||
// Match on the first fwmark
|
// Match on the first fwmark
|
||||||
@@ -754,83 +741,8 @@ func (r *router) addPostroutingRules() {
|
|||||||
Chain: r.chains[chainNameRoutingNat],
|
Chain: r.chains[chainNameRoutingNat],
|
||||||
Exprs: exprs2,
|
Exprs: exprs2,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// addMSSClampingRules adds MSS clamping rules to prevent fragmentation for forwarded traffic.
|
return nil
|
||||||
// TODO: Add IPv6 support
|
|
||||||
func (r *router) addMSSClampingRules() error {
|
|
||||||
mss := r.mtu - ipTCPHeaderMinSize
|
|
||||||
|
|
||||||
exprsOut := []expr.Any{
|
|
||||||
&expr.Meta{
|
|
||||||
Key: expr.MetaKeyOIFNAME,
|
|
||||||
Register: 1,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname(r.wgIface.Name()),
|
|
||||||
},
|
|
||||||
&expr.Meta{
|
|
||||||
Key: expr.MetaKeyL4PROTO,
|
|
||||||
Register: 1,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: []byte{unix.IPPROTO_TCP},
|
|
||||||
},
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 13,
|
|
||||||
Len: 1,
|
|
||||||
},
|
|
||||||
&expr.Bitwise{
|
|
||||||
DestRegister: 1,
|
|
||||||
SourceRegister: 1,
|
|
||||||
Len: 1,
|
|
||||||
Mask: []byte{0x02},
|
|
||||||
Xor: []byte{0x00},
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpNeq,
|
|
||||||
Register: 1,
|
|
||||||
Data: []byte{0x00},
|
|
||||||
},
|
|
||||||
&expr.Counter{},
|
|
||||||
&expr.Exthdr{
|
|
||||||
DestRegister: 1,
|
|
||||||
Type: 2,
|
|
||||||
Offset: 2,
|
|
||||||
Len: 2,
|
|
||||||
Op: expr.ExthdrOpTcpopt,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpGt,
|
|
||||||
Register: 1,
|
|
||||||
Data: binaryutil.BigEndian.PutUint16(uint16(mss)),
|
|
||||||
},
|
|
||||||
&expr.Immediate{
|
|
||||||
Register: 1,
|
|
||||||
Data: binaryutil.BigEndian.PutUint16(uint16(mss)),
|
|
||||||
},
|
|
||||||
&expr.Exthdr{
|
|
||||||
SourceRegister: 1,
|
|
||||||
Type: 2,
|
|
||||||
Offset: 2,
|
|
||||||
Len: 2,
|
|
||||||
Op: expr.ExthdrOpTcpopt,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
r.conn.AddRule(&nftables.Rule{
|
|
||||||
Table: r.workTable,
|
|
||||||
Chain: r.chains[chainNameMangleForward],
|
|
||||||
Exprs: exprsOut,
|
|
||||||
})
|
|
||||||
|
|
||||||
return r.conn.Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// addLegacyRouteRule adds a legacy routing rule for mgmt servers pre route acls
|
// addLegacyRouteRule adds a legacy routing rule for mgmt servers pre route acls
|
||||||
@@ -928,7 +840,6 @@ func (r *router) RemoveAllLegacyRouteRules() error {
|
|||||||
// that our traffic is not dropped by existing rules there.
|
// that our traffic is not dropped by existing rules there.
|
||||||
// The existing FORWARD rules/policies decide outbound traffic towards our interface.
|
// The existing FORWARD rules/policies decide outbound traffic towards our interface.
|
||||||
// In case the FORWARD policy is set to "drop", we add an established/related rule to allow return traffic for the inbound rule.
|
// In case the FORWARD policy is set to "drop", we add an established/related rule to allow return traffic for the inbound rule.
|
||||||
// This method also adds INPUT chain rules to allow traffic to the local interface.
|
|
||||||
func (r *router) acceptForwardRules() error {
|
func (r *router) acceptForwardRules() error {
|
||||||
if r.filterTable == nil {
|
if r.filterTable == nil {
|
||||||
log.Debugf("table 'filter' not found for forward rules, skipping accept rules")
|
log.Debugf("table 'filter' not found for forward rules, skipping accept rules")
|
||||||
@@ -938,7 +849,7 @@ func (r *router) acceptForwardRules() error {
|
|||||||
fw := "iptables"
|
fw := "iptables"
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
log.Debugf("Used %s to add accept forward and input rules", fw)
|
log.Debugf("Used %s to add accept forward rules", fw)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Try iptables first and fallback to nftables if iptables is not available
|
// Try iptables first and fallback to nftables if iptables is not available
|
||||||
@@ -948,30 +859,22 @@ func (r *router) acceptForwardRules() error {
|
|||||||
log.Warnf("Will use nftables to manipulate the filter table because iptables is not available: %v", err)
|
log.Warnf("Will use nftables to manipulate the filter table because iptables is not available: %v", err)
|
||||||
|
|
||||||
fw = "nftables"
|
fw = "nftables"
|
||||||
return r.acceptFilterRulesNftables()
|
return r.acceptForwardRulesNftables()
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.acceptFilterRulesIptables(ipt)
|
return r.acceptForwardRulesIptables(ipt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) acceptFilterRulesIptables(ipt *iptables.IPTables) error {
|
func (r *router) acceptForwardRulesIptables(ipt *iptables.IPTables) error {
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
for _, rule := range r.getAcceptForwardRules() {
|
for _, rule := range r.getAcceptForwardRules() {
|
||||||
if err := ipt.Insert("filter", chainNameForward, 1, rule...); err != nil {
|
if err := ipt.Insert("filter", chainNameForward, 1, rule...); err != nil {
|
||||||
merr = multierror.Append(err, fmt.Errorf("add iptables forward rule: %v", err))
|
merr = multierror.Append(err, fmt.Errorf("add iptables rule: %v", err))
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("added iptables forward rule: %v", rule)
|
log.Debugf("added iptables rule: %v", rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRule := r.getAcceptInputRule()
|
|
||||||
if err := ipt.Insert("filter", chainNameInput, 1, inputRule...); err != nil {
|
|
||||||
merr = multierror.Append(err, fmt.Errorf("add iptables input rule: %v", err))
|
|
||||||
} else {
|
|
||||||
log.Debugf("added iptables input rule: %v", inputRule)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -983,13 +886,10 @@ func (r *router) getAcceptForwardRules() [][]string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) getAcceptInputRule() []string {
|
func (r *router) acceptForwardRulesNftables() error {
|
||||||
return []string{"-i", r.wgIface.Name(), "-j", "ACCEPT"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *router) acceptFilterRulesNftables() error {
|
|
||||||
intf := ifname(r.wgIface.Name())
|
intf := ifname(r.wgIface.Name())
|
||||||
|
|
||||||
|
// Rule for incoming interface (iif) with counter
|
||||||
iifRule := &nftables.Rule{
|
iifRule := &nftables.Rule{
|
||||||
Table: r.filterTable,
|
Table: r.filterTable,
|
||||||
Chain: &nftables.Chain{
|
Chain: &nftables.Chain{
|
||||||
@@ -1022,10 +922,11 @@ func (r *router) acceptFilterRulesNftables() error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rule for outgoing interface (oif) with counter
|
||||||
oifRule := &nftables.Rule{
|
oifRule := &nftables.Rule{
|
||||||
Table: r.filterTable,
|
Table: r.filterTable,
|
||||||
Chain: &nftables.Chain{
|
Chain: &nftables.Chain{
|
||||||
Name: chainNameForward,
|
Name: "FORWARD",
|
||||||
Table: r.filterTable,
|
Table: r.filterTable,
|
||||||
Type: nftables.ChainTypeFilter,
|
Type: nftables.ChainTypeFilter,
|
||||||
Hooknum: nftables.ChainHookForward,
|
Hooknum: nftables.ChainHookForward,
|
||||||
@@ -1034,60 +935,35 @@ func (r *router) acceptFilterRulesNftables() error {
|
|||||||
Exprs: append(oifExprs, getEstablishedExprs(2)...),
|
Exprs: append(oifExprs, getEstablishedExprs(2)...),
|
||||||
UserData: []byte(userDataAcceptForwardRuleOif),
|
UserData: []byte(userDataAcceptForwardRuleOif),
|
||||||
}
|
}
|
||||||
|
|
||||||
r.conn.InsertRule(oifRule)
|
r.conn.InsertRule(oifRule)
|
||||||
|
|
||||||
inputRule := &nftables.Rule{
|
return nil
|
||||||
Table: r.filterTable,
|
|
||||||
Chain: &nftables.Chain{
|
|
||||||
Name: chainNameInput,
|
|
||||||
Table: r.filterTable,
|
|
||||||
Type: nftables.ChainTypeFilter,
|
|
||||||
Hooknum: nftables.ChainHookInput,
|
|
||||||
Priority: nftables.ChainPriorityFilter,
|
|
||||||
},
|
|
||||||
Exprs: []expr.Any{
|
|
||||||
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: intf,
|
|
||||||
},
|
|
||||||
&expr.Counter{},
|
|
||||||
&expr.Verdict{Kind: expr.VerdictAccept},
|
|
||||||
},
|
|
||||||
UserData: []byte(userDataAcceptInputRule),
|
|
||||||
}
|
|
||||||
r.conn.InsertRule(inputRule)
|
|
||||||
|
|
||||||
return r.conn.Flush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) removeAcceptFilterRules() error {
|
func (r *router) removeAcceptForwardRules() error {
|
||||||
if r.filterTable == nil {
|
if r.filterTable == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try iptables first and fallback to nftables if iptables is not available
|
||||||
ipt, err := iptables.New()
|
ipt, err := iptables.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Will use nftables to manipulate the filter table because iptables is not available: %v", err)
|
log.Warnf("Will use nftables to manipulate the filter table because iptables is not available: %v", err)
|
||||||
return r.removeAcceptFilterRulesNftables()
|
return r.removeAcceptForwardRulesNftables()
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.removeAcceptFilterRulesIptables(ipt)
|
return r.removeAcceptForwardRulesIptables(ipt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) removeAcceptFilterRulesNftables() error {
|
func (r *router) removeAcceptForwardRulesNftables() error {
|
||||||
chains, err := r.conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
chains, err := r.conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list chains: %v", err)
|
return fmt.Errorf("list chains: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, chain := range chains {
|
for _, chain := range chains {
|
||||||
if chain.Table.Name != r.filterTable.Name {
|
if chain.Table.Name != r.filterTable.Name || chain.Name != chainNameForward {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if chain.Name != chainNameForward && chain.Name != chainNameInput {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1098,8 +974,7 @@ func (r *router) removeAcceptFilterRulesNftables() error {
|
|||||||
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if bytes.Equal(rule.UserData, []byte(userDataAcceptForwardRuleIif)) ||
|
if bytes.Equal(rule.UserData, []byte(userDataAcceptForwardRuleIif)) ||
|
||||||
bytes.Equal(rule.UserData, []byte(userDataAcceptForwardRuleOif)) ||
|
bytes.Equal(rule.UserData, []byte(userDataAcceptForwardRuleOif)) {
|
||||||
bytes.Equal(rule.UserData, []byte(userDataAcceptInputRule)) {
|
|
||||||
if err := r.conn.DelRule(rule); err != nil {
|
if err := r.conn.DelRule(rule); err != nil {
|
||||||
return fmt.Errorf("delete rule: %v", err)
|
return fmt.Errorf("delete rule: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1114,20 +989,14 @@ func (r *router) removeAcceptFilterRulesNftables() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) removeAcceptFilterRulesIptables(ipt *iptables.IPTables) error {
|
func (r *router) removeAcceptForwardRulesIptables(ipt *iptables.IPTables) error {
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
for _, rule := range r.getAcceptForwardRules() {
|
for _, rule := range r.getAcceptForwardRules() {
|
||||||
if err := ipt.DeleteIfExists("filter", chainNameForward, rule...); err != nil {
|
if err := ipt.DeleteIfExists("filter", chainNameForward, rule...); err != nil {
|
||||||
merr = multierror.Append(err, fmt.Errorf("remove iptables forward rule: %v", err))
|
merr = multierror.Append(err, fmt.Errorf("remove iptables rule: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputRule := r.getAcceptInputRule()
|
|
||||||
if err := ipt.DeleteIfExists("filter", chainNameInput, inputRule...); err != nil {
|
|
||||||
merr = multierror.Append(err, fmt.Errorf("remove iptables input rule: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1187,7 +1056,7 @@ func (r *router) refreshRulesMap() error {
|
|||||||
for _, chain := range r.chains {
|
for _, chain := range r.chains {
|
||||||
rules, err := r.conn.GetRules(chain.Table, chain)
|
rules, err := r.conn.GetRules(chain.Table, chain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list rules: %w", err)
|
return fmt.Errorf(" unable to list rules: %v", err)
|
||||||
}
|
}
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if len(rule.UserData) > 0 {
|
if len(rule.UserData) > 0 {
|
||||||
@@ -1481,103 +1350,6 @@ func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
|
|
||||||
func (r *router) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
|
||||||
|
|
||||||
if _, exists := r.rules[ruleID]; exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
protoNum, err := protoToInt(protocol)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("convert protocol to number: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exprs := []expr.Any{
|
|
||||||
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname(r.wgIface.Name()),
|
|
||||||
},
|
|
||||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 2},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 2,
|
|
||||||
Data: []byte{protoNum},
|
|
||||||
},
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 3,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 2,
|
|
||||||
Len: 2,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 3,
|
|
||||||
Data: binaryutil.BigEndian.PutUint16(sourcePort),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
exprs = append(exprs, applyPrefix(netip.PrefixFrom(localAddr, 32), false)...)
|
|
||||||
|
|
||||||
exprs = append(exprs,
|
|
||||||
&expr.Immediate{
|
|
||||||
Register: 1,
|
|
||||||
Data: localAddr.AsSlice(),
|
|
||||||
},
|
|
||||||
&expr.Immediate{
|
|
||||||
Register: 2,
|
|
||||||
Data: binaryutil.BigEndian.PutUint16(targetPort),
|
|
||||||
},
|
|
||||||
&expr.NAT{
|
|
||||||
Type: expr.NATTypeDestNAT,
|
|
||||||
Family: uint32(nftables.TableFamilyIPv4),
|
|
||||||
RegAddrMin: 1,
|
|
||||||
RegProtoMin: 2,
|
|
||||||
RegProtoMax: 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
dnatRule := &nftables.Rule{
|
|
||||||
Table: r.workTable,
|
|
||||||
Chain: r.chains[chainNameRoutingRdr],
|
|
||||||
Exprs: exprs,
|
|
||||||
UserData: []byte(ruleID),
|
|
||||||
}
|
|
||||||
r.conn.AddRule(dnatRule)
|
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
|
||||||
return fmt.Errorf("add inbound DNAT rule: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.rules[ruleID] = dnatRule
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveInboundDNAT removes an inbound DNAT rule.
|
|
||||||
func (r *router) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
|
||||||
return fmt.Errorf(refreshRulesMapError, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleID := fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
|
||||||
|
|
||||||
if rule, exists := r.rules[ruleID]; exists {
|
|
||||||
if err := r.conn.DelRule(rule); err != nil {
|
|
||||||
return fmt.Errorf("delete inbound DNAT rule %s: %w", ruleID, err)
|
|
||||||
}
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
|
||||||
return fmt.Errorf("flush delete inbound DNAT rule: %w", err)
|
|
||||||
}
|
|
||||||
delete(r.rules, ruleID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyNetwork generates nftables expressions for networks (CIDR) or sets
|
// applyNetwork generates nftables expressions for networks (CIDR) or sets
|
||||||
func (r *router) applyNetwork(
|
func (r *router) applyNetwork(
|
||||||
network firewall.Network,
|
network firewall.Network,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/test"
|
"github.com/netbirdio/netbird/client/firewall/test"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -37,7 +36,7 @@ func TestNftablesManager_AddNatRule(t *testing.T) {
|
|||||||
for _, testCase := range test.InsertRuleTestCases {
|
for _, testCase := range test.InsertRuleTestCases {
|
||||||
t.Run(testCase.Name, func(t *testing.T) {
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
// need fw manager to init both acl mgr and router for all chains to be present
|
// need fw manager to init both acl mgr and router for all chains to be present
|
||||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
manager, err := Create(ifaceMock)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -126,7 +125,7 @@ func TestNftablesManager_RemoveNatRule(t *testing.T) {
|
|||||||
|
|
||||||
for _, testCase := range test.RemoveRuleTestCases {
|
for _, testCase := range test.RemoveRuleTestCases {
|
||||||
t.Run(testCase.Name, func(t *testing.T) {
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
manager, err := Create(ifaceMock, iface.DefaultMTU)
|
manager, err := Create(ifaceMock)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -198,7 +197,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
|
|
||||||
defer deleteWorkTable()
|
defer deleteWorkTable()
|
||||||
|
|
||||||
r, err := newRouter(workTable, ifaceMock, iface.DefaultMTU)
|
r, err := newRouter(workTable, ifaceMock)
|
||||||
require.NoError(t, err, "Failed to create router")
|
require.NoError(t, err, "Failed to create router")
|
||||||
require.NoError(t, r.init(workTable))
|
require.NoError(t, r.init(workTable))
|
||||||
|
|
||||||
@@ -365,7 +364,7 @@ func TestNftablesCreateIpSet(t *testing.T) {
|
|||||||
|
|
||||||
defer deleteWorkTable()
|
defer deleteWorkTable()
|
||||||
|
|
||||||
r, err := newRouter(workTable, ifaceMock, iface.DefaultMTU)
|
r, err := newRouter(workTable, ifaceMock)
|
||||||
require.NoError(t, err, "Failed to create router")
|
require.NoError(t, err, "Failed to create router")
|
||||||
require.NoError(t, r.init(workTable))
|
require.NoError(t, r.init(workTable))
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package nftables
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,7 +10,6 @@ type InterfaceState struct {
|
|||||||
NameStr string `json:"name"`
|
NameStr string `json:"name"`
|
||||||
WGAddress wgaddr.Address `json:"wg_address"`
|
WGAddress wgaddr.Address `json:"wg_address"`
|
||||||
UserspaceBind bool `json:"userspace_bind"`
|
UserspaceBind bool `json:"userspace_bind"`
|
||||||
MTU uint16 `json:"mtu"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *InterfaceState) Name() string {
|
func (i *InterfaceState) Name() string {
|
||||||
@@ -35,11 +33,7 @@ func (s *ShutdownState) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ShutdownState) Cleanup() error {
|
func (s *ShutdownState) Cleanup() error {
|
||||||
mtu := s.InterfaceState.MTU
|
nft, err := Create(s.InterfaceState)
|
||||||
if mtu == 0 {
|
|
||||||
mtu = iface.DefaultMTU
|
|
||||||
}
|
|
||||||
nft, err := Create(s.InterfaceState, mtu)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create nftables manager: %w", err)
|
return fmt.Errorf("create nftables manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ type BaseConnTrack struct {
|
|||||||
PacketsRx atomic.Uint64
|
PacketsRx atomic.Uint64
|
||||||
BytesTx atomic.Uint64
|
BytesTx atomic.Uint64
|
||||||
BytesRx atomic.Uint64
|
BytesRx atomic.Uint64
|
||||||
|
|
||||||
DNATOrigPort atomic.Uint32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// these small methods will be inlined by the compiler
|
// these small methods will be inlined by the compiler
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ func NewTCPTracker(timeout time.Duration, logger *nblog.Logger, flowLogger nftyp
|
|||||||
return tracker
|
return tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TCPTracker) updateIfExists(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, direction nftypes.Direction, size int) (ConnKey, uint16, bool) {
|
func (t *TCPTracker) updateIfExists(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, direction nftypes.Direction, size int) (ConnKey, bool) {
|
||||||
key := ConnKey{
|
key := ConnKey{
|
||||||
SrcIP: srcIP,
|
SrcIP: srcIP,
|
||||||
DstIP: dstIP,
|
DstIP: dstIP,
|
||||||
@@ -171,30 +171,28 @@ func (t *TCPTracker) updateIfExists(srcIP, dstIP netip.Addr, srcPort, dstPort ui
|
|||||||
|
|
||||||
if exists {
|
if exists {
|
||||||
t.updateState(key, conn, flags, direction, size)
|
t.updateState(key, conn, flags, direction, size)
|
||||||
return key, uint16(conn.DNATOrigPort.Load()), true
|
return key, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, 0, false
|
return key, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackOutbound records an outbound TCP connection and returns the original port if DNAT reversal is needed
|
// TrackOutbound records an outbound TCP connection
|
||||||
func (t *TCPTracker) TrackOutbound(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, size int) uint16 {
|
func (t *TCPTracker) TrackOutbound(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, size int) {
|
||||||
if _, origPort, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, flags, nftypes.Egress, size); exists {
|
if _, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, flags, nftypes.Egress, size); !exists {
|
||||||
return origPort
|
// if (inverted direction) conn is not tracked, track this direction
|
||||||
|
t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Egress, nil, size)
|
||||||
}
|
}
|
||||||
// if (inverted direction) conn is not tracked, track this direction
|
|
||||||
t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Egress, nil, size, 0)
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackInbound processes an inbound TCP packet and updates connection state
|
// TrackInbound processes an inbound TCP packet and updates connection state
|
||||||
func (t *TCPTracker) TrackInbound(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, ruleID []byte, size int, dnatOrigPort uint16) {
|
func (t *TCPTracker) TrackInbound(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, ruleID []byte, size int) {
|
||||||
t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Ingress, ruleID, size, dnatOrigPort)
|
t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Ingress, ruleID, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// track is the common implementation for tracking both inbound and outbound connections
|
// track is the common implementation for tracking both inbound and outbound connections
|
||||||
func (t *TCPTracker) track(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, direction nftypes.Direction, ruleID []byte, size int, origPort uint16) {
|
func (t *TCPTracker) track(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, direction nftypes.Direction, ruleID []byte, size int) {
|
||||||
key, _, exists := t.updateIfExists(srcIP, dstIP, srcPort, dstPort, flags, direction, size)
|
key, exists := t.updateIfExists(srcIP, dstIP, srcPort, dstPort, flags, direction, size)
|
||||||
if exists || flags&TCPSyn == 0 {
|
if exists || flags&TCPSyn == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -212,13 +210,8 @@ 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))
|
||||||
conn.DNATOrigPort.Store(uint32(origPort))
|
|
||||||
|
|
||||||
if origPort != 0 {
|
t.logger.Trace2("New %s TCP connection: %s", direction, key)
|
||||||
t.logger.Trace4("New %s TCP connection: %s (port DNAT %d -> %d)", direction, key, origPort, dstPort)
|
|
||||||
} else {
|
|
||||||
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()
|
||||||
@@ -456,21 +449,6 @@ func (t *TCPTracker) cleanup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConnection safely retrieves a connection state
|
|
||||||
func (t *TCPTracker) GetConnection(srcIP netip.Addr, srcPort uint16, dstIP netip.Addr, dstPort uint16) (*TCPConnTrack, bool) {
|
|
||||||
t.mutex.RLock()
|
|
||||||
defer t.mutex.RUnlock()
|
|
||||||
|
|
||||||
key := ConnKey{
|
|
||||||
SrcIP: srcIP,
|
|
||||||
DstIP: dstIP,
|
|
||||||
SrcPort: srcPort,
|
|
||||||
DstPort: dstPort,
|
|
||||||
}
|
|
||||||
conn, exists := t.connections[key]
|
|
||||||
return conn, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close stops the cleanup routine and releases resources
|
// Close stops the cleanup routine and releases resources
|
||||||
func (t *TCPTracker) Close() {
|
func (t *TCPTracker) Close() {
|
||||||
t.tickerCancel()
|
t.tickerCancel()
|
||||||
|
|||||||
@@ -603,7 +603,7 @@ func TestTCPInboundInitiatedConnection(t *testing.T) {
|
|||||||
serverPort := uint16(80)
|
serverPort := uint16(80)
|
||||||
|
|
||||||
// 1. Client sends SYN (we receive it as inbound)
|
// 1. Client sends SYN (we receive it as inbound)
|
||||||
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPSyn, nil, 100, 0)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPSyn, nil, 100)
|
||||||
|
|
||||||
key := ConnKey{
|
key := ConnKey{
|
||||||
SrcIP: clientIP,
|
SrcIP: clientIP,
|
||||||
@@ -623,12 +623,12 @@ func TestTCPInboundInitiatedConnection(t *testing.T) {
|
|||||||
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPSyn|TCPAck, 100)
|
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPSyn|TCPAck, 100)
|
||||||
|
|
||||||
// 3. Client sends ACK to complete handshake
|
// 3. Client sends ACK to complete handshake
|
||||||
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100, 0)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100)
|
||||||
require.Equal(t, TCPStateEstablished, conn.GetState(), "Connection should be ESTABLISHED after handshake completion")
|
require.Equal(t, TCPStateEstablished, conn.GetState(), "Connection should be ESTABLISHED after handshake completion")
|
||||||
|
|
||||||
// 4. Test data transfer
|
// 4. Test data transfer
|
||||||
// Client sends data
|
// Client sends data
|
||||||
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPPush|TCPAck, nil, 1000, 0)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPPush|TCPAck, nil, 1000)
|
||||||
|
|
||||||
// Server sends ACK for data
|
// Server sends ACK for data
|
||||||
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPAck, 100)
|
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPAck, 100)
|
||||||
@@ -637,7 +637,7 @@ func TestTCPInboundInitiatedConnection(t *testing.T) {
|
|||||||
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPPush|TCPAck, 1500)
|
tracker.TrackOutbound(serverIP, clientIP, serverPort, clientPort, TCPPush|TCPAck, 1500)
|
||||||
|
|
||||||
// Client sends ACK for data
|
// Client sends ACK for data
|
||||||
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100, 0)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100)
|
||||||
|
|
||||||
// Verify state and counters
|
// Verify state and counters
|
||||||
require.Equal(t, TCPStateEstablished, conn.GetState())
|
require.Equal(t, TCPStateEstablished, conn.GetState())
|
||||||
|
|||||||
@@ -58,23 +58,20 @@ func NewUDPTracker(timeout time.Duration, logger *nblog.Logger, flowLogger nftyp
|
|||||||
return tracker
|
return tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackOutbound records an outbound UDP connection and returns the original port if DNAT reversal is needed
|
// TrackOutbound records an outbound UDP connection
|
||||||
func (t *UDPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, size int) uint16 {
|
func (t *UDPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, size int) {
|
||||||
_, origPort, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, nftypes.Egress, size)
|
if _, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, nftypes.Egress, size); !exists {
|
||||||
if exists {
|
// if (inverted direction) conn is not tracked, track this direction
|
||||||
return origPort
|
t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Egress, nil, size)
|
||||||
}
|
}
|
||||||
// if (inverted direction) conn is not tracked, track this direction
|
|
||||||
t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Egress, nil, size, 0)
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackInbound records an inbound UDP connection
|
// TrackInbound records an inbound UDP connection
|
||||||
func (t *UDPTracker) TrackInbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, ruleID []byte, size int, dnatOrigPort uint16) {
|
func (t *UDPTracker) TrackInbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, ruleID []byte, size int) {
|
||||||
t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Ingress, ruleID, size, dnatOrigPort)
|
t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Ingress, ruleID, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, size int) (ConnKey, uint16, bool) {
|
func (t *UDPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, size int) (ConnKey, bool) {
|
||||||
key := ConnKey{
|
key := ConnKey{
|
||||||
SrcIP: srcIP,
|
SrcIP: srcIP,
|
||||||
DstIP: dstIP,
|
DstIP: dstIP,
|
||||||
@@ -89,15 +86,15 @@ func (t *UDPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, srcPort
|
|||||||
if exists {
|
if exists {
|
||||||
conn.UpdateLastSeen()
|
conn.UpdateLastSeen()
|
||||||
conn.UpdateCounters(direction, size)
|
conn.UpdateCounters(direction, size)
|
||||||
return key, uint16(conn.DNATOrigPort.Load()), true
|
return key, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, 0, false
|
return key, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// track is the common implementation for tracking both inbound and outbound connections
|
// track is the common implementation for tracking both inbound and outbound connections
|
||||||
func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, ruleID []byte, size int, origPort uint16) {
|
func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, ruleID []byte, size int) {
|
||||||
key, _, exists := t.updateIfExists(srcIP, dstIP, srcPort, dstPort, direction, size)
|
key, exists := t.updateIfExists(srcIP, dstIP, srcPort, dstPort, direction, size)
|
||||||
if exists {
|
if exists {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -112,7 +109,6 @@ func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, d
|
|||||||
SourcePort: srcPort,
|
SourcePort: srcPort,
|
||||||
DestPort: dstPort,
|
DestPort: dstPort,
|
||||||
}
|
}
|
||||||
conn.DNATOrigPort.Store(uint32(origPort))
|
|
||||||
conn.UpdateLastSeen()
|
conn.UpdateLastSeen()
|
||||||
conn.UpdateCounters(direction, size)
|
conn.UpdateCounters(direction, size)
|
||||||
|
|
||||||
@@ -120,11 +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()
|
||||||
|
|
||||||
if origPort != 0 {
|
t.logger.Trace2("New %s UDP connection: %s", direction, key)
|
||||||
t.logger.Trace4("New %s UDP connection: %s (port DNAT %d -> %d)", direction, key, origPort, dstPort)
|
|
||||||
} else {
|
|
||||||
t.logger.Trace2("New %s UDP connection: %s", direction, key)
|
|
||||||
}
|
|
||||||
t.sendEvent(nftypes.TypeStart, conn, ruleID)
|
t.sendEvent(nftypes.TypeStart, conn, ruleID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package uspfilter
|
package uspfilter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -28,18 +27,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const layerTypeAll = 0
|
||||||
layerTypeAll = 0
|
|
||||||
|
|
||||||
// ipTCPHeaderMinSize represents minimum IP (20) + TCP (20) header size for MSS calculation
|
|
||||||
ipTCPHeaderMinSize = 40
|
|
||||||
)
|
|
||||||
|
|
||||||
// serviceKey represents a protocol/port combination for netstack service registry
|
|
||||||
type serviceKey struct {
|
|
||||||
protocol gopacket.LayerType
|
|
||||||
port uint16
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// EnvDisableConntrack disables the stateful filter, replies to outbound traffic won't be allowed.
|
// EnvDisableConntrack disables the stateful filter, replies to outbound traffic won't be allowed.
|
||||||
@@ -48,9 +36,6 @@ const (
|
|||||||
// EnvDisableUserspaceRouting disables userspace routing, to-be-routed packets will be dropped.
|
// EnvDisableUserspaceRouting disables userspace routing, to-be-routed packets will be dropped.
|
||||||
EnvDisableUserspaceRouting = "NB_DISABLE_USERSPACE_ROUTING"
|
EnvDisableUserspaceRouting = "NB_DISABLE_USERSPACE_ROUTING"
|
||||||
|
|
||||||
// EnvDisableMSSClamping disables TCP MSS clamping for forwarded traffic.
|
|
||||||
EnvDisableMSSClamping = "NB_DISABLE_MSS_CLAMPING"
|
|
||||||
|
|
||||||
// EnvForceUserspaceRouter forces userspace routing even if native routing is available.
|
// EnvForceUserspaceRouter forces userspace routing even if native routing is available.
|
||||||
EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER"
|
EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER"
|
||||||
|
|
||||||
@@ -124,17 +109,6 @@ type Manager struct {
|
|||||||
dnatMappings map[netip.Addr]netip.Addr
|
dnatMappings map[netip.Addr]netip.Addr
|
||||||
dnatMutex sync.RWMutex
|
dnatMutex sync.RWMutex
|
||||||
dnatBiMap *biDNATMap
|
dnatBiMap *biDNATMap
|
||||||
|
|
||||||
portDNATEnabled atomic.Bool
|
|
||||||
portDNATRules []portDNATRule
|
|
||||||
portDNATMutex sync.RWMutex
|
|
||||||
|
|
||||||
netstackServices map[serviceKey]struct{}
|
|
||||||
netstackServiceMutex sync.RWMutex
|
|
||||||
|
|
||||||
mtu uint16
|
|
||||||
mssClampValue uint16
|
|
||||||
mssClampEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// decoder for packages
|
// decoder for packages
|
||||||
@@ -148,21 +122,19 @@ type decoder struct {
|
|||||||
icmp6 layers.ICMPv6
|
icmp6 layers.ICMPv6
|
||||||
decoded []gopacket.LayerType
|
decoded []gopacket.LayerType
|
||||||
parser *gopacket.DecodingLayerParser
|
parser *gopacket.DecodingLayerParser
|
||||||
|
|
||||||
dnatOrigPort uint16
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create userspace firewall manager constructor
|
// Create userspace firewall manager constructor
|
||||||
func Create(iface common.IFaceMapper, disableServerRoutes bool, flowLogger nftypes.FlowLogger, mtu uint16) (*Manager, error) {
|
func Create(iface common.IFaceMapper, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (*Manager, error) {
|
||||||
return create(iface, nil, disableServerRoutes, flowLogger, mtu)
|
return create(iface, nil, disableServerRoutes, flowLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger, mtu uint16) (*Manager, error) {
|
func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (*Manager, error) {
|
||||||
if nativeFirewall == nil {
|
if nativeFirewall == nil {
|
||||||
return nil, errors.New("native firewall is nil")
|
return nil, errors.New("native firewall is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr, err := create(iface, nativeFirewall, disableServerRoutes, flowLogger, mtu)
|
mgr, err := create(iface, nativeFirewall, disableServerRoutes, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -170,8 +142,8 @@ func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.
|
|||||||
return mgr, nil
|
return mgr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCreateEnv() (bool, bool, bool) {
|
func parseCreateEnv() (bool, bool) {
|
||||||
var disableConntrack, enableLocalForwarding, disableMSSClamping bool
|
var disableConntrack, enableLocalForwarding bool
|
||||||
var err error
|
var err error
|
||||||
if val := os.Getenv(EnvDisableConntrack); val != "" {
|
if val := os.Getenv(EnvDisableConntrack); val != "" {
|
||||||
disableConntrack, err = strconv.ParseBool(val)
|
disableConntrack, err = strconv.ParseBool(val)
|
||||||
@@ -190,18 +162,12 @@ func parseCreateEnv() (bool, bool, bool) {
|
|||||||
log.Warnf("failed to parse %s: %v", EnvEnableLocalForwarding, err)
|
log.Warnf("failed to parse %s: %v", EnvEnableLocalForwarding, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if val := os.Getenv(EnvDisableMSSClamping); val != "" {
|
|
||||||
disableMSSClamping, err = strconv.ParseBool(val)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to parse %s: %v", EnvDisableMSSClamping, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return disableConntrack, enableLocalForwarding, disableMSSClamping
|
return disableConntrack, enableLocalForwarding
|
||||||
}
|
}
|
||||||
|
|
||||||
func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger, mtu uint16) (*Manager, error) {
|
func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool, flowLogger nftypes.FlowLogger) (*Manager, error) {
|
||||||
disableConntrack, enableLocalForwarding, disableMSSClamping := parseCreateEnv()
|
disableConntrack, enableLocalForwarding := parseCreateEnv()
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
decoders: sync.Pool{
|
decoders: sync.Pool{
|
||||||
@@ -230,19 +196,13 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
|
|||||||
netstack: netstack.IsEnabled(),
|
netstack: netstack.IsEnabled(),
|
||||||
localForwarding: enableLocalForwarding,
|
localForwarding: enableLocalForwarding,
|
||||||
dnatMappings: make(map[netip.Addr]netip.Addr),
|
dnatMappings: make(map[netip.Addr]netip.Addr),
|
||||||
portDNATRules: []portDNATRule{},
|
|
||||||
netstackServices: make(map[serviceKey]struct{}),
|
|
||||||
mtu: mtu,
|
|
||||||
}
|
}
|
||||||
m.routingEnabled.Store(false)
|
m.routingEnabled.Store(false)
|
||||||
|
|
||||||
if !disableMSSClamping {
|
|
||||||
m.mssClampEnabled = true
|
|
||||||
m.mssClampValue = mtu - ipTCPHeaderMinSize
|
|
||||||
}
|
|
||||||
if err := m.localipmanager.UpdateLocalIPs(iface); err != nil {
|
if err := m.localipmanager.UpdateLocalIPs(iface); err != nil {
|
||||||
return nil, fmt.Errorf("update local IPs: %w", err)
|
return nil, fmt.Errorf("update local IPs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if disableConntrack {
|
if disableConntrack {
|
||||||
log.Info("conntrack is disabled")
|
log.Info("conntrack is disabled")
|
||||||
} else {
|
} else {
|
||||||
@@ -250,11 +210,14 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
|
|||||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, flowLogger)
|
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, flowLogger)
|
||||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, flowLogger)
|
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, flowLogger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// netstack needs the forwarder for local traffic
|
||||||
if m.netstack && m.localForwarding {
|
if m.netstack && m.localForwarding {
|
||||||
if err := m.initForwarder(); err != nil {
|
if err := m.initForwarder(); err != nil {
|
||||||
log.Errorf("failed to initialize forwarder: %v", err)
|
log.Errorf("failed to initialize forwarder: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := iface.SetFilter(m); err != nil {
|
if err := iface.SetFilter(m); err != nil {
|
||||||
return nil, fmt.Errorf("set filter: %w", err)
|
return nil, fmt.Errorf("set filter: %w", err)
|
||||||
}
|
}
|
||||||
@@ -357,7 +320,7 @@ func (m *Manager) initForwarder() error {
|
|||||||
return errors.New("forwarding not supported")
|
return errors.New("forwarding not supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
forwarder, err := forwarder.New(m.wgIface, m.logger, m.flowLogger, m.netstack, m.mtu)
|
forwarder, err := forwarder.New(m.wgIface, m.logger, m.flowLogger, m.netstack)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.routingEnabled.Store(false)
|
m.routingEnabled.Store(false)
|
||||||
return fmt.Errorf("create forwarder: %w", err)
|
return fmt.Errorf("create forwarder: %w", err)
|
||||||
@@ -663,20 +626,11 @@ func (m *Manager) filterOutbound(packetData []byte, size int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
switch d.decoded[1] {
|
if d.decoded[1] == layers.LayerTypeUDP && m.udpHooksDrop(uint16(d.udp.DstPort), dstIP, packetData) {
|
||||||
case layers.LayerTypeUDP:
|
return true
|
||||||
if m.udpHooksDrop(uint16(d.udp.DstPort), dstIP, packetData) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case layers.LayerTypeTCP:
|
|
||||||
// Clamp MSS on all TCP SYN packets, including those from local IPs.
|
|
||||||
// SNATed routed traffic may appear as local IP but still requires clamping.
|
|
||||||
if m.mssClampEnabled {
|
|
||||||
m.clampTCPMSS(packetData, d)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m.trackOutbound(d, srcIP, dstIP, packetData, size)
|
m.trackOutbound(d, srcIP, dstIP, size)
|
||||||
m.translateOutboundDNAT(packetData, d)
|
m.translateOutboundDNAT(packetData, d)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -720,117 +674,14 @@ func getTCPFlags(tcp *layers.TCP) uint8 {
|
|||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
// clampTCPMSS clamps the TCP MSS option in SYN and SYN-ACK packets to prevent fragmentation.
|
func (m *Manager) trackOutbound(d *decoder, srcIP, dstIP netip.Addr, size int) {
|
||||||
// Both sides advertise their MSS during connection establishment, so we need to clamp both.
|
|
||||||
func (m *Manager) clampTCPMSS(packetData []byte, d *decoder) bool {
|
|
||||||
if !d.tcp.SYN {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(d.tcp.Options) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
mssOptionIndex := -1
|
|
||||||
var currentMSS uint16
|
|
||||||
for i, opt := range d.tcp.Options {
|
|
||||||
if opt.OptionType == layers.TCPOptionKindMSS && len(opt.OptionData) == 2 {
|
|
||||||
currentMSS = binary.BigEndian.Uint16(opt.OptionData)
|
|
||||||
if currentMSS > m.mssClampValue {
|
|
||||||
mssOptionIndex = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if mssOptionIndex == -1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ipHeaderSize := int(d.ip4.IHL) * 4
|
|
||||||
if ipHeaderSize < 20 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.updateMSSOption(packetData, d, mssOptionIndex, ipHeaderSize) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
m.logger.Trace2("Clamped TCP MSS from %d to %d", currentMSS, m.mssClampValue)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) updateMSSOption(packetData []byte, d *decoder, mssOptionIndex, ipHeaderSize int) bool {
|
|
||||||
tcpHeaderStart := ipHeaderSize
|
|
||||||
tcpOptionsStart := tcpHeaderStart + 20
|
|
||||||
|
|
||||||
optOffset := tcpOptionsStart
|
|
||||||
for j := 0; j < mssOptionIndex; j++ {
|
|
||||||
switch d.tcp.Options[j].OptionType {
|
|
||||||
case layers.TCPOptionKindEndList, layers.TCPOptionKindNop:
|
|
||||||
optOffset++
|
|
||||||
default:
|
|
||||||
optOffset += 2 + len(d.tcp.Options[j].OptionData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mssValueOffset := optOffset + 2
|
|
||||||
binary.BigEndian.PutUint16(packetData[mssValueOffset:mssValueOffset+2], m.mssClampValue)
|
|
||||||
|
|
||||||
m.recalculateTCPChecksum(packetData, d, tcpHeaderStart)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) recalculateTCPChecksum(packetData []byte, d *decoder, tcpHeaderStart int) {
|
|
||||||
tcpLayer := packetData[tcpHeaderStart:]
|
|
||||||
tcpLength := len(packetData) - tcpHeaderStart
|
|
||||||
|
|
||||||
tcpLayer[16] = 0
|
|
||||||
tcpLayer[17] = 0
|
|
||||||
|
|
||||||
var pseudoSum uint32
|
|
||||||
pseudoSum += uint32(d.ip4.SrcIP[0])<<8 | uint32(d.ip4.SrcIP[1])
|
|
||||||
pseudoSum += uint32(d.ip4.SrcIP[2])<<8 | uint32(d.ip4.SrcIP[3])
|
|
||||||
pseudoSum += uint32(d.ip4.DstIP[0])<<8 | uint32(d.ip4.DstIP[1])
|
|
||||||
pseudoSum += uint32(d.ip4.DstIP[2])<<8 | uint32(d.ip4.DstIP[3])
|
|
||||||
pseudoSum += uint32(d.ip4.Protocol)
|
|
||||||
pseudoSum += uint32(tcpLength)
|
|
||||||
|
|
||||||
var sum uint32 = pseudoSum
|
|
||||||
for i := 0; i < tcpLength-1; i += 2 {
|
|
||||||
sum += uint32(tcpLayer[i])<<8 | uint32(tcpLayer[i+1])
|
|
||||||
}
|
|
||||||
if tcpLength%2 == 1 {
|
|
||||||
sum += uint32(tcpLayer[tcpLength-1]) << 8
|
|
||||||
}
|
|
||||||
|
|
||||||
for sum > 0xFFFF {
|
|
||||||
sum = (sum & 0xFFFF) + (sum >> 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
checksum := ^uint16(sum)
|
|
||||||
binary.BigEndian.PutUint16(tcpLayer[16:18], checksum)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) trackOutbound(d *decoder, srcIP, dstIP netip.Addr, packetData []byte, size int) {
|
|
||||||
transport := d.decoded[1]
|
transport := d.decoded[1]
|
||||||
switch transport {
|
switch transport {
|
||||||
case layers.LayerTypeUDP:
|
case layers.LayerTypeUDP:
|
||||||
origPort := m.udpTracker.TrackOutbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), size)
|
m.udpTracker.TrackOutbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), size)
|
||||||
if origPort == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err := m.rewriteUDPPort(packetData, d, origPort, sourcePortOffset); err != nil {
|
|
||||||
m.logger.Error1("failed to rewrite UDP port: %v", err)
|
|
||||||
}
|
|
||||||
case layers.LayerTypeTCP:
|
case layers.LayerTypeTCP:
|
||||||
flags := getTCPFlags(&d.tcp)
|
flags := getTCPFlags(&d.tcp)
|
||||||
origPort := m.tcpTracker.TrackOutbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, size)
|
m.tcpTracker.TrackOutbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, size)
|
||||||
if origPort == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err := m.rewriteTCPPort(packetData, d, origPort, sourcePortOffset); err != nil {
|
|
||||||
m.logger.Error1("failed to rewrite TCP port: %v", err)
|
|
||||||
}
|
|
||||||
case layers.LayerTypeICMPv4:
|
case layers.LayerTypeICMPv4:
|
||||||
m.icmpTracker.TrackOutbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, d.icmp4.Payload, size)
|
m.icmpTracker.TrackOutbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, d.icmp4.Payload, size)
|
||||||
}
|
}
|
||||||
@@ -840,15 +691,13 @@ func (m *Manager) trackInbound(d *decoder, srcIP, dstIP netip.Addr, ruleID []byt
|
|||||||
transport := d.decoded[1]
|
transport := d.decoded[1]
|
||||||
switch transport {
|
switch transport {
|
||||||
case layers.LayerTypeUDP:
|
case layers.LayerTypeUDP:
|
||||||
m.udpTracker.TrackInbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), ruleID, size, d.dnatOrigPort)
|
m.udpTracker.TrackInbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), ruleID, size)
|
||||||
case layers.LayerTypeTCP:
|
case layers.LayerTypeTCP:
|
||||||
flags := getTCPFlags(&d.tcp)
|
flags := getTCPFlags(&d.tcp)
|
||||||
m.tcpTracker.TrackInbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, ruleID, size, d.dnatOrigPort)
|
m.tcpTracker.TrackInbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, ruleID, size)
|
||||||
case layers.LayerTypeICMPv4:
|
case layers.LayerTypeICMPv4:
|
||||||
m.icmpTracker.TrackInbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, ruleID, d.icmp4.Payload, size)
|
m.icmpTracker.TrackInbound(srcIP, dstIP, d.icmp4.Id, d.icmp4.TypeCode, ruleID, d.icmp4.Payload, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.dnatOrigPort = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// udpHooksDrop checks if any UDP hooks should drop the packet
|
// udpHooksDrop checks if any UDP hooks should drop the packet
|
||||||
@@ -910,20 +759,10 @@ func (m *Manager) filterInbound(packetData []byte, size int) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optimize port DNAT by caching matched rules in conntrack
|
|
||||||
if translated := m.translateInboundPortDNAT(packetData, d, srcIP, dstIP); translated {
|
|
||||||
// Re-decode after port DNAT translation to update port information
|
|
||||||
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
|
|
||||||
m.logger.Error1("failed to re-decode packet after port DNAT: %v", err)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
srcIP, dstIP = m.extractIPs(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Error1("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)
|
||||||
@@ -968,7 +807,9 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.shouldForward(d, dstIP) {
|
// If requested we pass local traffic to internal interfaces to the forwarder.
|
||||||
|
// netstack doesn't have an interface to forward packets to the native stack so we always need to use the forwarder.
|
||||||
|
if m.localForwarding && (m.netstack || dstIP != m.wgIface.Address().IP) {
|
||||||
return m.handleForwardedLocalTraffic(packetData)
|
return m.handleForwardedLocalTraffic(packetData)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1402,86 +1243,3 @@ func (m *Manager) DisableRouting() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterNetstackService registers a service as listening on the netstack for the given protocol and port
|
|
||||||
func (m *Manager) RegisterNetstackService(protocol nftypes.Protocol, port uint16) {
|
|
||||||
m.netstackServiceMutex.Lock()
|
|
||||||
defer m.netstackServiceMutex.Unlock()
|
|
||||||
layerType := m.protocolToLayerType(protocol)
|
|
||||||
key := serviceKey{protocol: layerType, port: port}
|
|
||||||
m.netstackServices[key] = struct{}{}
|
|
||||||
m.logger.Debug3("RegisterNetstackService: registered %s:%d (layerType=%s)", protocol, port, layerType)
|
|
||||||
m.logger.Debug1("RegisterNetstackService: current registry size: %d", len(m.netstackServices))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnregisterNetstackService removes a service from the netstack registry
|
|
||||||
func (m *Manager) UnregisterNetstackService(protocol nftypes.Protocol, port uint16) {
|
|
||||||
m.netstackServiceMutex.Lock()
|
|
||||||
defer m.netstackServiceMutex.Unlock()
|
|
||||||
layerType := m.protocolToLayerType(protocol)
|
|
||||||
key := serviceKey{protocol: layerType, port: port}
|
|
||||||
delete(m.netstackServices, key)
|
|
||||||
m.logger.Debug2("Unregistered netstack service on protocol %s port %d", protocol, port)
|
|
||||||
}
|
|
||||||
|
|
||||||
// protocolToLayerType converts nftypes.Protocol to gopacket.LayerType for internal use
|
|
||||||
func (m *Manager) protocolToLayerType(protocol nftypes.Protocol) gopacket.LayerType {
|
|
||||||
switch protocol {
|
|
||||||
case nftypes.TCP:
|
|
||||||
return layers.LayerTypeTCP
|
|
||||||
case nftypes.UDP:
|
|
||||||
return layers.LayerTypeUDP
|
|
||||||
case nftypes.ICMP:
|
|
||||||
return layers.LayerTypeICMPv4
|
|
||||||
default:
|
|
||||||
return gopacket.LayerType(0) // Invalid/unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shouldForward determines if a packet should be forwarded to the forwarder.
|
|
||||||
// The forwarder handles routing packets to the native OS network stack.
|
|
||||||
// Returns true if packet should go to the forwarder, false if it should go to netstack listeners or the native stack directly.
|
|
||||||
func (m *Manager) shouldForward(d *decoder, dstIP netip.Addr) bool {
|
|
||||||
// not enabled, never forward
|
|
||||||
if !m.localForwarding {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// netstack always needs to forward because it's lacking a native interface
|
|
||||||
// exception for registered netstack services, those should go to netstack listeners
|
|
||||||
if m.netstack {
|
|
||||||
return !m.hasMatchingNetstackService(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
// traffic to our other local interfaces (not NetBird IP) - always forward
|
|
||||||
if dstIP != m.wgIface.Address().IP {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// traffic to our NetBird IP, not netstack mode - send to netstack listeners
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// hasMatchingNetstackService checks if there's a registered netstack service for this packet
|
|
||||||
func (m *Manager) hasMatchingNetstackService(d *decoder) bool {
|
|
||||||
if len(d.decoded) < 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var dstPort uint16
|
|
||||||
switch d.decoded[1] {
|
|
||||||
case layers.LayerTypeTCP:
|
|
||||||
dstPort = uint16(d.tcp.DstPort)
|
|
||||||
case layers.LayerTypeUDP:
|
|
||||||
dstPort = uint16(d.udp.DstPort)
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
key := serviceKey{protocol: d.decoded[1], port: dstPort}
|
|
||||||
m.netstackServiceMutex.RLock()
|
|
||||||
_, exists := m.netstackServices[key]
|
|
||||||
m.netstackServiceMutex.RUnlock()
|
|
||||||
|
|
||||||
return exists
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -170,7 +169,7 @@ func BenchmarkCoreFiltering(b *testing.B) {
|
|||||||
// Create manager and basic setup
|
// Create manager and basic setup
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -210,7 +209,7 @@ func BenchmarkStateScaling(b *testing.B) {
|
|||||||
b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) {
|
b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) {
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -253,7 +252,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) {
|
|||||||
b.Run(sc.name, func(b *testing.B) {
|
b.Run(sc.name, func(b *testing.B) {
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -411,7 +410,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
b.Run(sc.name, func(b *testing.B) {
|
b.Run(sc.name, func(b *testing.B) {
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -538,7 +537,7 @@ func BenchmarkLongLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -621,7 +620,7 @@ func BenchmarkShortLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -732,7 +731,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -812,7 +811,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
@@ -897,6 +896,38 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateTCPPacketWithFlags creates a TCP packet with specific flags
|
||||||
|
func generateTCPPacketWithFlags(b *testing.B, srcIP, dstIP net.IP, srcPort, dstPort, flags uint16) []byte {
|
||||||
|
b.Helper()
|
||||||
|
|
||||||
|
ipv4 := &layers.IPv4{
|
||||||
|
TTL: 64,
|
||||||
|
Version: 4,
|
||||||
|
SrcIP: srcIP,
|
||||||
|
DstIP: dstIP,
|
||||||
|
Protocol: layers.IPProtocolTCP,
|
||||||
|
}
|
||||||
|
|
||||||
|
tcp := &layers.TCP{
|
||||||
|
SrcPort: layers.TCPPort(srcPort),
|
||||||
|
DstPort: layers.TCPPort(dstPort),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set TCP flags
|
||||||
|
tcp.SYN = (flags & uint16(conntrack.TCPSyn)) != 0
|
||||||
|
tcp.ACK = (flags & uint16(conntrack.TCPAck)) != 0
|
||||||
|
tcp.PSH = (flags & uint16(conntrack.TCPPush)) != 0
|
||||||
|
tcp.RST = (flags & uint16(conntrack.TCPRst)) != 0
|
||||||
|
tcp.FIN = (flags & uint16(conntrack.TCPFin)) != 0
|
||||||
|
|
||||||
|
require.NoError(b, tcp.SetNetworkLayerForChecksum(ipv4))
|
||||||
|
|
||||||
|
buf := gopacket.NewSerializeBuffer()
|
||||||
|
opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
|
||||||
|
require.NoError(b, gopacket.SerializeLayers(buf, opts, ipv4, tcp, gopacket.Payload("test")))
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkRouteACLs(b *testing.B) {
|
func BenchmarkRouteACLs(b *testing.B) {
|
||||||
manager := setupRoutedManager(b, "10.10.0.100/16")
|
manager := setupRoutedManager(b, "10.10.0.100/16")
|
||||||
|
|
||||||
@@ -959,231 +990,3 @@ func BenchmarkRouteACLs(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkMSSClamping benchmarks the MSS clamping impact on filterOutbound.
|
|
||||||
// This shows the overhead difference between the common case (non-SYN packets, fast path)
|
|
||||||
// and the rare case (SYN packets that need clamping, expensive path).
|
|
||||||
func BenchmarkMSSClamping(b *testing.B) {
|
|
||||||
scenarios := []struct {
|
|
||||||
name string
|
|
||||||
description string
|
|
||||||
genPacket func(*testing.B, net.IP, net.IP) []byte
|
|
||||||
frequency string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "syn_needs_clamp",
|
|
||||||
description: "SYN packet needing MSS clamping",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateSYNPacketWithMSS(b, src, dst, 12345, 80, 1460)
|
|
||||||
},
|
|
||||||
frequency: "~0.1% of traffic - EXPENSIVE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "syn_no_clamp_needed",
|
|
||||||
description: "SYN packet with already-small MSS",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateSYNPacketWithMSS(b, src, dst, 12345, 80, 1200)
|
|
||||||
},
|
|
||||||
frequency: "~0.05% of traffic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tcp_ack",
|
|
||||||
description: "Non-SYN TCP packet (ACK, data transfer)",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateTCPPacketWithFlags(b, src, dst, 12345, 80, uint16(conntrack.TCPAck))
|
|
||||||
},
|
|
||||||
frequency: "~60-70% of traffic - FAST PATH",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tcp_psh_ack",
|
|
||||||
description: "TCP data packet (PSH+ACK)",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateTCPPacketWithFlags(b, src, dst, 12345, 80, uint16(conntrack.TCPPush|conntrack.TCPAck))
|
|
||||||
},
|
|
||||||
frequency: "~10-20% of traffic - FAST PATH",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "udp",
|
|
||||||
description: "UDP packet",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generatePacket(b, src, dst, 12345, 80, layers.IPProtocolUDP)
|
|
||||||
},
|
|
||||||
frequency: "~20-30% of traffic - FAST PATH",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sc := range scenarios {
|
|
||||||
b.Run(sc.name, func(b *testing.B) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(b, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(b, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
manager.mssClampEnabled = true
|
|
||||||
manager.mssClampValue = 1240
|
|
||||||
|
|
||||||
srcIP := net.ParseIP("100.64.0.2")
|
|
||||||
dstIP := net.ParseIP("8.8.8.8")
|
|
||||||
packet := sc.genPacket(b, srcIP, dstIP)
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
manager.filterOutbound(packet, len(packet))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkMSSClampingOverhead compares overhead of MSS clamping enabled vs disabled
|
|
||||||
// for the common case (non-SYN TCP packets).
|
|
||||||
func BenchmarkMSSClampingOverhead(b *testing.B) {
|
|
||||||
scenarios := []struct {
|
|
||||||
name string
|
|
||||||
enabled bool
|
|
||||||
genPacket func(*testing.B, net.IP, net.IP) []byte
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "disabled_tcp_ack",
|
|
||||||
enabled: false,
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateTCPPacketWithFlags(b, src, dst, 12345, 80, uint16(conntrack.TCPAck))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "enabled_tcp_ack",
|
|
||||||
enabled: true,
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateTCPPacketWithFlags(b, src, dst, 12345, 80, uint16(conntrack.TCPAck))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "disabled_syn_needs_clamp",
|
|
||||||
enabled: false,
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateSYNPacketWithMSS(b, src, dst, 12345, 80, 1460)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "enabled_syn_needs_clamp",
|
|
||||||
enabled: true,
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateSYNPacketWithMSS(b, src, dst, 12345, 80, 1460)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sc := range scenarios {
|
|
||||||
b.Run(sc.name, func(b *testing.B) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(b, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(b, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
manager.mssClampEnabled = sc.enabled
|
|
||||||
if sc.enabled {
|
|
||||||
manager.mssClampValue = 1240
|
|
||||||
}
|
|
||||||
|
|
||||||
srcIP := net.ParseIP("100.64.0.2")
|
|
||||||
dstIP := net.ParseIP("8.8.8.8")
|
|
||||||
packet := sc.genPacket(b, srcIP, dstIP)
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
manager.filterOutbound(packet, len(packet))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkMSSClampingMemory measures memory allocations for common vs rare cases
|
|
||||||
func BenchmarkMSSClampingMemory(b *testing.B) {
|
|
||||||
scenarios := []struct {
|
|
||||||
name string
|
|
||||||
genPacket func(*testing.B, net.IP, net.IP) []byte
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "tcp_ack_fast_path",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateTCPPacketWithFlags(b, src, dst, 12345, 80, uint16(conntrack.TCPAck))
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "syn_needs_clamp",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generateSYNPacketWithMSS(b, src, dst, 12345, 80, 1460)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "udp_fast_path",
|
|
||||||
genPacket: func(b *testing.B, src, dst net.IP) []byte {
|
|
||||||
return generatePacket(b, src, dst, 12345, 80, layers.IPProtocolUDP)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sc := range scenarios {
|
|
||||||
b.Run(sc.name, func(b *testing.B) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(b, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(b, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
manager.mssClampEnabled = true
|
|
||||||
manager.mssClampValue = 1240
|
|
||||||
|
|
||||||
srcIP := net.ParseIP("100.64.0.2")
|
|
||||||
dstIP := net.ParseIP("8.8.8.8")
|
|
||||||
packet := sc.genPacket(b, srcIP, dstIP)
|
|
||||||
|
|
||||||
b.ReportAllocs()
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
manager.filterOutbound(packet, len(packet))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSYNPacketNoMSS(b *testing.B, srcIP, dstIP net.IP, srcPort, dstPort uint16) []byte {
|
|
||||||
b.Helper()
|
|
||||||
|
|
||||||
ip := &layers.IPv4{
|
|
||||||
Version: 4,
|
|
||||||
IHL: 5,
|
|
||||||
TTL: 64,
|
|
||||||
Protocol: layers.IPProtocolTCP,
|
|
||||||
SrcIP: srcIP,
|
|
||||||
DstIP: dstIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
tcp := &layers.TCP{
|
|
||||||
SrcPort: layers.TCPPort(srcPort),
|
|
||||||
DstPort: layers.TCPPort(dstPort),
|
|
||||||
SYN: true,
|
|
||||||
Seq: 1000,
|
|
||||||
Window: 65535,
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(b, tcp.SetNetworkLayerForChecksum(ip))
|
|
||||||
|
|
||||||
buf := gopacket.NewSerializeBuffer()
|
|
||||||
opts := gopacket.SerializeOptions{
|
|
||||||
FixLengths: true,
|
|
||||||
ComputeChecksums: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(b, gopacket.SerializeLayers(buf, opts, ip, tcp, gopacket.Payload([]byte{})))
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
"github.com/netbirdio/netbird/client/iface/mocks"
|
"github.com/netbirdio/netbird/client/iface/mocks"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
@@ -32,7 +31,7 @@ func TestPeerACLFiltering(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, manager)
|
require.NotNil(t, manager)
|
||||||
|
|
||||||
@@ -617,7 +616,7 @@ func setupRoutedManager(tb testing.TB, network string) *Manager {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
require.NoError(tb, manager.EnableRouting())
|
require.NoError(tb, manager.EnableRouting())
|
||||||
require.NotNil(tb, manager)
|
require.NotNil(tb, manager)
|
||||||
@@ -1463,7 +1462,7 @@ func TestRouteACLSet(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package uspfilter
|
package uspfilter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -18,11 +17,9 @@ import (
|
|||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
nbiface "github.com/netbirdio/netbird/client/iface"
|
|
||||||
"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/internal/netflow"
|
"github.com/netbirdio/netbird/client/internal/netflow"
|
||||||
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,7 +66,7 @@ func TestManagerCreate(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
m, err := Create(ifaceMock, false, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -89,7 +86,7 @@ func TestManagerAddPeerFiltering(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
m, err := Create(ifaceMock, false, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -122,7 +119,7 @@ func TestManagerDeleteRule(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
m, err := Create(ifaceMock, false, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -218,7 +215,7 @@ func TestAddUDPPacketHook(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, nbiface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook)
|
manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook)
|
||||||
@@ -268,7 +265,7 @@ func TestManagerReset(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
m, err := Create(ifaceMock, false, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -307,7 +304,7 @@ func TestNotMatchByIP(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
m, err := Create(ifaceMock, false, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -370,7 +367,7 @@ func TestRemovePacketHook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// creating manager instance
|
// creating manager instance
|
||||||
manager, err := Create(iface, false, flowLogger, nbiface.DefaultMTU)
|
manager, err := Create(iface, false, flowLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create Manager: %s", err)
|
t.Fatalf("Failed to create Manager: %s", err)
|
||||||
}
|
}
|
||||||
@@ -416,7 +413,7 @@ func TestRemovePacketHook(t *testing.T) {
|
|||||||
func TestProcessOutgoingHooks(t *testing.T) {
|
func TestProcessOutgoingHooks(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, nbiface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.udpTracker.Close()
|
manager.udpTracker.Close()
|
||||||
@@ -498,7 +495,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
|
|||||||
ifaceMock := &IFaceMock{
|
ifaceMock := &IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
@@ -525,7 +522,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
|
|||||||
func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, nbiface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.udpTracker.Close() // Close the existing tracker
|
manager.udpTracker.Close() // Close the existing tracker
|
||||||
@@ -732,7 +729,7 @@ func TestUpdateSetMerge(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
@@ -818,7 +815,7 @@ func TestUpdateSetDeduplication(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
@@ -926,327 +923,3 @@ func TestUpdateSetDeduplication(t *testing.T) {
|
|||||||
require.Equal(t, tc.expected, isAllowed, tc.desc)
|
require.Equal(t, tc.expected, isAllowed, tc.desc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMSSClamping(t *testing.T) {
|
|
||||||
ifaceMock := &IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
AddressFunc: func() wgaddr.Address {
|
|
||||||
return wgaddr.Address{
|
|
||||||
IP: netip.MustParseAddr("100.10.0.100"),
|
|
||||||
Network: netip.MustParsePrefix("100.10.0.0/16"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, 1280)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
require.True(t, manager.mssClampEnabled, "MSS clamping should be enabled by default")
|
|
||||||
expectedMSSValue := uint16(1280 - ipTCPHeaderMinSize)
|
|
||||||
require.Equal(t, expectedMSSValue, manager.mssClampValue, "MSS clamp value should be MTU - 40")
|
|
||||||
|
|
||||||
err = manager.UpdateLocalIPs()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
srcIP := net.ParseIP("100.10.0.2")
|
|
||||||
dstIP := net.ParseIP("8.8.8.8")
|
|
||||||
|
|
||||||
t.Run("SYN packet with high MSS gets clamped", func(t *testing.T) {
|
|
||||||
highMSS := uint16(1460)
|
|
||||||
packet := generateSYNPacketWithMSS(t, srcIP, dstIP, 12345, 80, highMSS)
|
|
||||||
|
|
||||||
manager.filterOutbound(packet, len(packet))
|
|
||||||
|
|
||||||
d := parsePacket(t, packet)
|
|
||||||
require.Len(t, d.tcp.Options, 1, "Should have MSS option")
|
|
||||||
require.Equal(t, uint8(layers.TCPOptionKindMSS), uint8(d.tcp.Options[0].OptionType))
|
|
||||||
actualMSS := binary.BigEndian.Uint16(d.tcp.Options[0].OptionData)
|
|
||||||
require.Equal(t, expectedMSSValue, actualMSS, "MSS should be clamped to MTU - 40")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("SYN packet with low MSS unchanged", func(t *testing.T) {
|
|
||||||
lowMSS := uint16(1200)
|
|
||||||
packet := generateSYNPacketWithMSS(t, srcIP, dstIP, 12345, 80, lowMSS)
|
|
||||||
|
|
||||||
manager.filterOutbound(packet, len(packet))
|
|
||||||
|
|
||||||
d := parsePacket(t, packet)
|
|
||||||
require.Len(t, d.tcp.Options, 1, "Should have MSS option")
|
|
||||||
actualMSS := binary.BigEndian.Uint16(d.tcp.Options[0].OptionData)
|
|
||||||
require.Equal(t, lowMSS, actualMSS, "Low MSS should not be modified")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("SYN-ACK packet gets clamped", func(t *testing.T) {
|
|
||||||
highMSS := uint16(1460)
|
|
||||||
packet := generateSYNACKPacketWithMSS(t, srcIP, dstIP, 12345, 80, highMSS)
|
|
||||||
|
|
||||||
manager.filterOutbound(packet, len(packet))
|
|
||||||
|
|
||||||
d := parsePacket(t, packet)
|
|
||||||
require.Len(t, d.tcp.Options, 1, "Should have MSS option")
|
|
||||||
actualMSS := binary.BigEndian.Uint16(d.tcp.Options[0].OptionData)
|
|
||||||
require.Equal(t, expectedMSSValue, actualMSS, "MSS in SYN-ACK should be clamped")
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Non-SYN packet unchanged", func(t *testing.T) {
|
|
||||||
packet := generateTCPPacketWithFlags(t, srcIP, dstIP, 12345, 80, uint16(conntrack.TCPAck))
|
|
||||||
|
|
||||||
manager.filterOutbound(packet, len(packet))
|
|
||||||
|
|
||||||
d := parsePacket(t, packet)
|
|
||||||
require.Empty(t, d.tcp.Options, "ACK packet should have no options")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSYNPacketWithMSS(tb testing.TB, srcIP, dstIP net.IP, srcPort, dstPort uint16, mss uint16) []byte {
|
|
||||||
tb.Helper()
|
|
||||||
|
|
||||||
ipLayer := &layers.IPv4{
|
|
||||||
Version: 4,
|
|
||||||
TTL: 64,
|
|
||||||
Protocol: layers.IPProtocolTCP,
|
|
||||||
SrcIP: srcIP,
|
|
||||||
DstIP: dstIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpLayer := &layers.TCP{
|
|
||||||
SrcPort: layers.TCPPort(srcPort),
|
|
||||||
DstPort: layers.TCPPort(dstPort),
|
|
||||||
SYN: true,
|
|
||||||
Window: 65535,
|
|
||||||
Options: []layers.TCPOption{
|
|
||||||
{
|
|
||||||
OptionType: layers.TCPOptionKindMSS,
|
|
||||||
OptionLength: 4,
|
|
||||||
OptionData: binary.BigEndian.AppendUint16(nil, mss),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := tcpLayer.SetNetworkLayerForChecksum(ipLayer)
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
buf := gopacket.NewSerializeBuffer()
|
|
||||||
opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
|
|
||||||
err = gopacket.SerializeLayers(buf, opts, ipLayer, tcpLayer, gopacket.Payload([]byte{}))
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateSYNACKPacketWithMSS(tb testing.TB, srcIP, dstIP net.IP, srcPort, dstPort uint16, mss uint16) []byte {
|
|
||||||
tb.Helper()
|
|
||||||
|
|
||||||
ipLayer := &layers.IPv4{
|
|
||||||
Version: 4,
|
|
||||||
TTL: 64,
|
|
||||||
Protocol: layers.IPProtocolTCP,
|
|
||||||
SrcIP: srcIP,
|
|
||||||
DstIP: dstIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpLayer := &layers.TCP{
|
|
||||||
SrcPort: layers.TCPPort(srcPort),
|
|
||||||
DstPort: layers.TCPPort(dstPort),
|
|
||||||
SYN: true,
|
|
||||||
ACK: true,
|
|
||||||
Window: 65535,
|
|
||||||
Options: []layers.TCPOption{
|
|
||||||
{
|
|
||||||
OptionType: layers.TCPOptionKindMSS,
|
|
||||||
OptionLength: 4,
|
|
||||||
OptionData: binary.BigEndian.AppendUint16(nil, mss),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := tcpLayer.SetNetworkLayerForChecksum(ipLayer)
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
buf := gopacket.NewSerializeBuffer()
|
|
||||||
opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
|
|
||||||
err = gopacket.SerializeLayers(buf, opts, ipLayer, tcpLayer, gopacket.Payload([]byte{}))
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateTCPPacketWithFlags(tb testing.TB, srcIP, dstIP net.IP, srcPort, dstPort uint16, flags uint16) []byte {
|
|
||||||
tb.Helper()
|
|
||||||
|
|
||||||
ipLayer := &layers.IPv4{
|
|
||||||
Version: 4,
|
|
||||||
TTL: 64,
|
|
||||||
Protocol: layers.IPProtocolTCP,
|
|
||||||
SrcIP: srcIP,
|
|
||||||
DstIP: dstIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpLayer := &layers.TCP{
|
|
||||||
SrcPort: layers.TCPPort(srcPort),
|
|
||||||
DstPort: layers.TCPPort(dstPort),
|
|
||||||
Window: 65535,
|
|
||||||
}
|
|
||||||
|
|
||||||
if flags&uint16(conntrack.TCPSyn) != 0 {
|
|
||||||
tcpLayer.SYN = true
|
|
||||||
}
|
|
||||||
if flags&uint16(conntrack.TCPAck) != 0 {
|
|
||||||
tcpLayer.ACK = true
|
|
||||||
}
|
|
||||||
if flags&uint16(conntrack.TCPFin) != 0 {
|
|
||||||
tcpLayer.FIN = true
|
|
||||||
}
|
|
||||||
if flags&uint16(conntrack.TCPRst) != 0 {
|
|
||||||
tcpLayer.RST = true
|
|
||||||
}
|
|
||||||
if flags&uint16(conntrack.TCPPush) != 0 {
|
|
||||||
tcpLayer.PSH = true
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tcpLayer.SetNetworkLayerForChecksum(ipLayer)
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
buf := gopacket.NewSerializeBuffer()
|
|
||||||
opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
|
|
||||||
err = gopacket.SerializeLayers(buf, opts, ipLayer, tcpLayer, gopacket.Payload([]byte{}))
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldForward(t *testing.T) {
|
|
||||||
// Set up test addresses
|
|
||||||
wgIP := netip.MustParseAddr("100.10.0.1")
|
|
||||||
otherIP := netip.MustParseAddr("100.10.0.2")
|
|
||||||
|
|
||||||
// Create test manager with mock interface
|
|
||||||
ifaceMock := &IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}
|
|
||||||
// Set the mock to return our test WG IP
|
|
||||||
ifaceMock.AddressFunc = func() wgaddr.Address {
|
|
||||||
return wgaddr.Address{IP: wgIP, Network: netip.PrefixFrom(wgIP, 24)}
|
|
||||||
}
|
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger, nbiface.DefaultMTU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Helper to create decoder with TCP packet
|
|
||||||
createTCPDecoder := func(dstPort uint16) *decoder {
|
|
||||||
ipv4 := &layers.IPv4{
|
|
||||||
Version: 4,
|
|
||||||
Protocol: layers.IPProtocolTCP,
|
|
||||||
SrcIP: net.ParseIP("192.168.1.100"),
|
|
||||||
DstIP: wgIP.AsSlice(),
|
|
||||||
}
|
|
||||||
tcp := &layers.TCP{
|
|
||||||
SrcPort: 54321,
|
|
||||||
DstPort: layers.TCPPort(dstPort),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := tcp.SetNetworkLayerForChecksum(ipv4)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
buf := gopacket.NewSerializeBuffer()
|
|
||||||
opts := gopacket.SerializeOptions{ComputeChecksums: true, FixLengths: true}
|
|
||||||
err = gopacket.SerializeLayers(buf, opts, ipv4, tcp, gopacket.Payload("test"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
d := &decoder{
|
|
||||||
decoded: []gopacket.LayerType{},
|
|
||||||
}
|
|
||||||
d.parser = gopacket.NewDecodingLayerParser(
|
|
||||||
layers.LayerTypeIPv4,
|
|
||||||
&d.eth, &d.ip4, &d.ip6, &d.icmp4, &d.icmp6, &d.tcp, &d.udp,
|
|
||||||
)
|
|
||||||
d.parser.IgnoreUnsupported = true
|
|
||||||
|
|
||||||
err = d.parser.DecodeLayers(buf.Bytes(), &d.decoded)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
localForwarding bool
|
|
||||||
netstack bool
|
|
||||||
dstIP netip.Addr
|
|
||||||
serviceRegistered bool
|
|
||||||
servicePort uint16
|
|
||||||
expected bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no local forwarding",
|
|
||||||
localForwarding: false,
|
|
||||||
netstack: true,
|
|
||||||
dstIP: wgIP,
|
|
||||||
expected: false,
|
|
||||||
description: "should never forward when local forwarding disabled",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "traffic to other local interface",
|
|
||||||
localForwarding: true,
|
|
||||||
netstack: false,
|
|
||||||
dstIP: otherIP,
|
|
||||||
expected: true,
|
|
||||||
description: "should forward traffic to our other local interfaces (not NetBird IP)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "traffic to NetBird IP, no netstack",
|
|
||||||
localForwarding: true,
|
|
||||||
netstack: false,
|
|
||||||
dstIP: wgIP,
|
|
||||||
expected: false,
|
|
||||||
description: "should send to netstack listeners (final return false path)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "traffic to our IP, netstack mode, no service",
|
|
||||||
localForwarding: true,
|
|
||||||
netstack: true,
|
|
||||||
dstIP: wgIP,
|
|
||||||
expected: true,
|
|
||||||
description: "should forward when in netstack mode with no matching service",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "traffic to our IP, netstack mode, with service",
|
|
||||||
localForwarding: true,
|
|
||||||
netstack: true,
|
|
||||||
dstIP: wgIP,
|
|
||||||
serviceRegistered: true,
|
|
||||||
servicePort: 22,
|
|
||||||
expected: false,
|
|
||||||
description: "should send to netstack listeners when service is registered",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Configure manager
|
|
||||||
manager.localForwarding = tt.localForwarding
|
|
||||||
manager.netstack = tt.netstack
|
|
||||||
|
|
||||||
// Register service if needed
|
|
||||||
if tt.serviceRegistered {
|
|
||||||
manager.RegisterNetstackService(nftypes.TCP, tt.servicePort)
|
|
||||||
defer manager.UnregisterNetstackService(nftypes.TCP, tt.servicePort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create decoder for the test
|
|
||||||
decoder := createTCPDecoder(tt.servicePort)
|
|
||||||
if !tt.serviceRegistered {
|
|
||||||
decoder = createTCPDecoder(8080) // Use non-registered port
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test the method
|
|
||||||
result := manager.shouldForward(decoder, tt.dstIP)
|
|
||||||
require.Equal(t, tt.expected, result, tt.description)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ type Forwarder struct {
|
|||||||
netstack bool
|
netstack bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.FlowLogger, netstack bool, mtu uint16) (*Forwarder, error) {
|
func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.FlowLogger, netstack bool) (*Forwarder, error) {
|
||||||
s := stack.New(stack.Options{
|
s := stack.New(stack.Options{
|
||||||
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
|
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
|
||||||
TransportProtocols: []stack.TransportProtocolFactory{
|
TransportProtocols: []stack.TransportProtocolFactory{
|
||||||
@@ -56,6 +56,10 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
|
|||||||
HandleLocal: false,
|
HandleLocal: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
mtu, err := iface.GetDevice().MTU()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get MTU: %w", err)
|
||||||
|
}
|
||||||
nicID := tcpip.NICID(1)
|
nicID := tcpip.NICID(1)
|
||||||
endpoint := &endpoint{
|
endpoint := &endpoint{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -64,7 +68,7 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.CreateNIC(nicID, endpoint); err != nil {
|
if err := s.CreateNIC(nicID, endpoint); err != nil {
|
||||||
return nil, fmt.Errorf("create NIC: %v", err)
|
return nil, fmt.Errorf("failed to create NIC: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
protoAddr := tcpip.ProtocolAddress{
|
protoAddr := tcpip.ProtocolAddress{
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ type idleConn struct {
|
|||||||
conn *udpPacketConn
|
conn *udpPacketConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUDPForwarder(mtu uint16, logger *nblog.Logger, flowLogger nftypes.FlowLogger) *udpForwarder {
|
func newUDPForwarder(mtu int, logger *nblog.Logger, flowLogger nftypes.FlowLogger) *udpForwarder {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
f := &udpForwarder{
|
f := &udpForwarder{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ type logMessage struct {
|
|||||||
arg4 any
|
arg4 any
|
||||||
arg5 any
|
arg5 any
|
||||||
arg6 any
|
arg6 any
|
||||||
arg7 any
|
|
||||||
arg8 any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logger is a high-performance, non-blocking logger
|
// Logger is a high-performance, non-blocking logger
|
||||||
@@ -96,6 +94,7 @@ 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) Error(format string) {
|
func (l *Logger) Error(format string) {
|
||||||
if l.level.Load() >= uint32(LevelError) {
|
if l.level.Load() >= uint32(LevelError) {
|
||||||
select {
|
select {
|
||||||
@@ -186,15 +185,6 @@ func (l *Logger) Debug2(format string, arg1, arg2 any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Debug3(format string, arg1, arg2, arg3 any) {
|
|
||||||
if l.level.Load() >= uint32(LevelDebug) {
|
|
||||||
select {
|
|
||||||
case l.msgChannel <- logMessage{level: LevelDebug, format: format, arg1: arg1, arg2: arg2, arg3: arg3}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) Trace1(format string, arg1 any) {
|
func (l *Logger) Trace1(format string, arg1 any) {
|
||||||
if l.level.Load() >= uint32(LevelTrace) {
|
if l.level.Load() >= uint32(LevelTrace) {
|
||||||
select {
|
select {
|
||||||
@@ -249,16 +239,6 @@ func (l *Logger) Trace6(format string, arg1, arg2, arg3, arg4, arg5, arg6 any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trace8 logs a trace message with 8 arguments (8 placeholder in format string)
|
|
||||||
func (l *Logger) Trace8(format string, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 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, arg7: arg7, arg8: arg8}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *Logger) formatMessage(buf *[]byte, msg logMessage) {
|
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")
|
||||||
@@ -280,12 +260,6 @@ func (l *Logger) formatMessage(buf *[]byte, msg logMessage) {
|
|||||||
argCount++
|
argCount++
|
||||||
if msg.arg6 != nil {
|
if msg.arg6 != nil {
|
||||||
argCount++
|
argCount++
|
||||||
if msg.arg7 != nil {
|
|
||||||
argCount++
|
|
||||||
if msg.arg8 != nil {
|
|
||||||
argCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -309,10 +283,6 @@ func (l *Logger) formatMessage(buf *[]byte, msg logMessage) {
|
|||||||
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5)
|
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5)
|
||||||
case 6:
|
case 6:
|
||||||
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5, msg.arg6)
|
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5, msg.arg6)
|
||||||
case 7:
|
|
||||||
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5, msg.arg6, msg.arg7)
|
|
||||||
case 8:
|
|
||||||
formatted = fmt.Sprintf(msg.format, msg.arg1, msg.arg2, msg.arg3, msg.arg4, msg.arg5, msg.arg6, msg.arg7, msg.arg8)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*buf = append(*buf, formatted...)
|
*buf = append(*buf, formatted...)
|
||||||
@@ -420,4 +390,4 @@ func (l *Logger) Stop(ctx context.Context) error {
|
|||||||
case <-done:
|
case <-done:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,9 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
@@ -15,21 +13,6 @@ import (
|
|||||||
|
|
||||||
var ErrIPv4Only = errors.New("only IPv4 is supported for DNAT")
|
var ErrIPv4Only = errors.New("only IPv4 is supported for DNAT")
|
||||||
|
|
||||||
var (
|
|
||||||
errInvalidIPHeaderLength = errors.New("invalid IP header length")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Port offsets in TCP/UDP headers
|
|
||||||
sourcePortOffset = 0
|
|
||||||
destinationPortOffset = 2
|
|
||||||
|
|
||||||
// IP address offsets in IPv4 header
|
|
||||||
sourceIPOffset = 12
|
|
||||||
destinationIPOffset = 16
|
|
||||||
)
|
|
||||||
|
|
||||||
// ipv4Checksum calculates IPv4 header checksum.
|
|
||||||
func ipv4Checksum(header []byte) uint16 {
|
func ipv4Checksum(header []byte) uint16 {
|
||||||
if len(header) < 20 {
|
if len(header) < 20 {
|
||||||
return 0
|
return 0
|
||||||
@@ -69,7 +52,6 @@ func ipv4Checksum(header []byte) uint16 {
|
|||||||
return ^uint16(sum)
|
return ^uint16(sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// icmpChecksum calculates ICMP checksum.
|
|
||||||
func icmpChecksum(data []byte) uint16 {
|
func icmpChecksum(data []byte) uint16 {
|
||||||
var sum1, sum2, sum3, sum4 uint32
|
var sum1, sum2, sum3, sum4 uint32
|
||||||
i := 0
|
i := 0
|
||||||
@@ -107,21 +89,11 @@ func icmpChecksum(data []byte) uint16 {
|
|||||||
return ^uint16(sum)
|
return ^uint16(sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// biDNATMap maintains bidirectional DNAT mappings.
|
|
||||||
type biDNATMap struct {
|
type biDNATMap struct {
|
||||||
forward map[netip.Addr]netip.Addr
|
forward map[netip.Addr]netip.Addr
|
||||||
reverse map[netip.Addr]netip.Addr
|
reverse map[netip.Addr]netip.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
// portDNATRule represents a port-specific DNAT rule.
|
|
||||||
type portDNATRule struct {
|
|
||||||
protocol gopacket.LayerType
|
|
||||||
origPort uint16
|
|
||||||
targetPort uint16
|
|
||||||
targetIP netip.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// newBiDNATMap creates a new bidirectional DNAT mapping structure.
|
|
||||||
func newBiDNATMap() *biDNATMap {
|
func newBiDNATMap() *biDNATMap {
|
||||||
return &biDNATMap{
|
return &biDNATMap{
|
||||||
forward: make(map[netip.Addr]netip.Addr),
|
forward: make(map[netip.Addr]netip.Addr),
|
||||||
@@ -129,13 +101,11 @@ func newBiDNATMap() *biDNATMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set adds a bidirectional DNAT mapping between original and translated addresses.
|
|
||||||
func (b *biDNATMap) set(original, translated netip.Addr) {
|
func (b *biDNATMap) set(original, translated netip.Addr) {
|
||||||
b.forward[original] = translated
|
b.forward[original] = translated
|
||||||
b.reverse[translated] = original
|
b.reverse[translated] = original
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete removes a bidirectional DNAT mapping for the given original address.
|
|
||||||
func (b *biDNATMap) delete(original netip.Addr) {
|
func (b *biDNATMap) delete(original netip.Addr) {
|
||||||
if translated, exists := b.forward[original]; exists {
|
if translated, exists := b.forward[original]; exists {
|
||||||
delete(b.forward, original)
|
delete(b.forward, original)
|
||||||
@@ -143,25 +113,19 @@ func (b *biDNATMap) delete(original netip.Addr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTranslated returns the translated address for a given original address.
|
|
||||||
func (b *biDNATMap) getTranslated(original netip.Addr) (netip.Addr, bool) {
|
func (b *biDNATMap) getTranslated(original netip.Addr) (netip.Addr, bool) {
|
||||||
translated, exists := b.forward[original]
|
translated, exists := b.forward[original]
|
||||||
return translated, exists
|
return translated, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOriginal returns the original address for a given translated address.
|
|
||||||
func (b *biDNATMap) getOriginal(translated netip.Addr) (netip.Addr, bool) {
|
func (b *biDNATMap) getOriginal(translated netip.Addr) (netip.Addr, bool) {
|
||||||
original, exists := b.reverse[translated]
|
original, exists := b.reverse[translated]
|
||||||
return original, exists
|
return original, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddInternalDNATMapping adds a 1:1 IP address mapping for internal DNAT translation.
|
|
||||||
func (m *Manager) AddInternalDNATMapping(originalAddr, translatedAddr netip.Addr) error {
|
func (m *Manager) AddInternalDNATMapping(originalAddr, translatedAddr netip.Addr) error {
|
||||||
if !originalAddr.IsValid() {
|
if !originalAddr.IsValid() || !translatedAddr.IsValid() {
|
||||||
return fmt.Errorf("invalid original IP address")
|
return fmt.Errorf("invalid IP addresses")
|
||||||
}
|
|
||||||
if !translatedAddr.IsValid() {
|
|
||||||
return fmt.Errorf("invalid translated IP address")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.localipmanager.IsLocalIP(translatedAddr) {
|
if m.localipmanager.IsLocalIP(translatedAddr) {
|
||||||
@@ -171,6 +135,7 @@ func (m *Manager) AddInternalDNATMapping(originalAddr, translatedAddr netip.Addr
|
|||||||
m.dnatMutex.Lock()
|
m.dnatMutex.Lock()
|
||||||
defer m.dnatMutex.Unlock()
|
defer m.dnatMutex.Unlock()
|
||||||
|
|
||||||
|
// Initialize both maps together if either is nil
|
||||||
if m.dnatMappings == nil || m.dnatBiMap == nil {
|
if m.dnatMappings == nil || m.dnatBiMap == nil {
|
||||||
m.dnatMappings = make(map[netip.Addr]netip.Addr)
|
m.dnatMappings = make(map[netip.Addr]netip.Addr)
|
||||||
m.dnatBiMap = newBiDNATMap()
|
m.dnatBiMap = newBiDNATMap()
|
||||||
@@ -186,7 +151,7 @@ func (m *Manager) AddInternalDNATMapping(originalAddr, translatedAddr netip.Addr
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveInternalDNATMapping removes a 1:1 IP address mapping.
|
// RemoveInternalDNATMapping removes a 1:1 IP address mapping
|
||||||
func (m *Manager) RemoveInternalDNATMapping(originalAddr netip.Addr) error {
|
func (m *Manager) RemoveInternalDNATMapping(originalAddr netip.Addr) error {
|
||||||
m.dnatMutex.Lock()
|
m.dnatMutex.Lock()
|
||||||
defer m.dnatMutex.Unlock()
|
defer m.dnatMutex.Unlock()
|
||||||
@@ -204,7 +169,7 @@ func (m *Manager) RemoveInternalDNATMapping(originalAddr netip.Addr) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDNATTranslation returns the translated address if a mapping exists.
|
// getDNATTranslation returns the translated address if a mapping exists
|
||||||
func (m *Manager) getDNATTranslation(addr netip.Addr) (netip.Addr, bool) {
|
func (m *Manager) getDNATTranslation(addr netip.Addr) (netip.Addr, bool) {
|
||||||
if !m.dnatEnabled.Load() {
|
if !m.dnatEnabled.Load() {
|
||||||
return addr, false
|
return addr, false
|
||||||
@@ -216,7 +181,7 @@ func (m *Manager) getDNATTranslation(addr netip.Addr) (netip.Addr, bool) {
|
|||||||
return translated, exists
|
return translated, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// findReverseDNATMapping finds original address for return traffic.
|
// findReverseDNATMapping finds original address for return traffic
|
||||||
func (m *Manager) findReverseDNATMapping(translatedAddr netip.Addr) (netip.Addr, bool) {
|
func (m *Manager) findReverseDNATMapping(translatedAddr netip.Addr) (netip.Addr, bool) {
|
||||||
if !m.dnatEnabled.Load() {
|
if !m.dnatEnabled.Load() {
|
||||||
return translatedAddr, false
|
return translatedAddr, false
|
||||||
@@ -228,12 +193,16 @@ func (m *Manager) findReverseDNATMapping(translatedAddr netip.Addr) (netip.Addr,
|
|||||||
return original, exists
|
return original, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// translateOutboundDNAT applies DNAT translation to outbound packets.
|
// translateOutboundDNAT applies DNAT translation to outbound packets
|
||||||
func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool {
|
func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool {
|
||||||
if !m.dnatEnabled.Load() {
|
if !m.dnatEnabled.Load() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
dstIP := netip.AddrFrom4([4]byte{packetData[16], packetData[17], packetData[18], packetData[19]})
|
dstIP := netip.AddrFrom4([4]byte{packetData[16], packetData[17], packetData[18], packetData[19]})
|
||||||
|
|
||||||
translatedIP, exists := m.getDNATTranslation(dstIP)
|
translatedIP, exists := m.getDNATTranslation(dstIP)
|
||||||
@@ -241,8 +210,8 @@ func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.rewritePacketIP(packetData, d, translatedIP, destinationIPOffset); err != nil {
|
if err := m.rewritePacketDestination(packetData, d, translatedIP); err != nil {
|
||||||
m.logger.Error1("failed to rewrite packet destination: %v", err)
|
m.logger.Error1("Failed to rewrite packet destination: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,12 +219,16 @@ func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// translateInboundReverse applies reverse DNAT to inbound return traffic.
|
// translateInboundReverse applies reverse DNAT to inbound return traffic
|
||||||
func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool {
|
func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool {
|
||||||
if !m.dnatEnabled.Load() {
|
if !m.dnatEnabled.Load() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
srcIP := netip.AddrFrom4([4]byte{packetData[12], packetData[13], packetData[14], packetData[15]})
|
srcIP := netip.AddrFrom4([4]byte{packetData[12], packetData[13], packetData[14], packetData[15]})
|
||||||
|
|
||||||
originalIP, exists := m.findReverseDNATMapping(srcIP)
|
originalIP, exists := m.findReverseDNATMapping(srcIP)
|
||||||
@@ -263,8 +236,8 @@ func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.rewritePacketIP(packetData, d, originalIP, sourceIPOffset); err != nil {
|
if err := m.rewritePacketSource(packetData, d, originalIP); err != nil {
|
||||||
m.logger.Error1("failed to rewrite packet source: %v", err)
|
m.logger.Error1("Failed to rewrite packet source: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,21 +245,21 @@ func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewritePacketIP replaces an IP address (source or destination) in the packet and updates checksums.
|
// rewritePacketDestination replaces destination IP in the packet
|
||||||
func (m *Manager) rewritePacketIP(packetData []byte, d *decoder, newIP netip.Addr, ipOffset int) error {
|
func (m *Manager) rewritePacketDestination(packetData []byte, d *decoder, newIP netip.Addr) error {
|
||||||
if !newIP.Is4() {
|
if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 || !newIP.Is4() {
|
||||||
return ErrIPv4Only
|
return ErrIPv4Only
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldIP [4]byte
|
var oldDst [4]byte
|
||||||
copy(oldIP[:], packetData[ipOffset:ipOffset+4])
|
copy(oldDst[:], packetData[16:20])
|
||||||
newIPBytes := newIP.As4()
|
newDst := newIP.As4()
|
||||||
|
|
||||||
copy(packetData[ipOffset:ipOffset+4], newIPBytes[:])
|
copy(packetData[16:20], newDst[:])
|
||||||
|
|
||||||
ipHeaderLen := int(d.ip4.IHL) * 4
|
ipHeaderLen := int(d.ip4.IHL) * 4
|
||||||
if ipHeaderLen < 20 || ipHeaderLen > len(packetData) {
|
if ipHeaderLen < 20 || ipHeaderLen > len(packetData) {
|
||||||
return errInvalidIPHeaderLength
|
return fmt.Errorf("invalid IP header length")
|
||||||
}
|
}
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(packetData[10:12], 0)
|
binary.BigEndian.PutUint16(packetData[10:12], 0)
|
||||||
@@ -296,9 +269,44 @@ func (m *Manager) rewritePacketIP(packetData []byte, d *decoder, newIP netip.Add
|
|||||||
if len(d.decoded) > 1 {
|
if len(d.decoded) > 1 {
|
||||||
switch d.decoded[1] {
|
switch d.decoded[1] {
|
||||||
case layers.LayerTypeTCP:
|
case layers.LayerTypeTCP:
|
||||||
m.updateTCPChecksum(packetData, ipHeaderLen, oldIP[:], newIPBytes[:])
|
m.updateTCPChecksum(packetData, ipHeaderLen, oldDst[:], newDst[:])
|
||||||
case layers.LayerTypeUDP:
|
case layers.LayerTypeUDP:
|
||||||
m.updateUDPChecksum(packetData, ipHeaderLen, oldIP[:], newIPBytes[:])
|
m.updateUDPChecksum(packetData, ipHeaderLen, oldDst[:], newDst[:])
|
||||||
|
case layers.LayerTypeICMPv4:
|
||||||
|
m.updateICMPChecksum(packetData, ipHeaderLen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewritePacketSource replaces the source IP address in the packet
|
||||||
|
func (m *Manager) rewritePacketSource(packetData []byte, d *decoder, newIP netip.Addr) error {
|
||||||
|
if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 || !newIP.Is4() {
|
||||||
|
return ErrIPv4Only
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldSrc [4]byte
|
||||||
|
copy(oldSrc[:], packetData[12:16])
|
||||||
|
newSrc := newIP.As4()
|
||||||
|
|
||||||
|
copy(packetData[12:16], newSrc[:])
|
||||||
|
|
||||||
|
ipHeaderLen := int(d.ip4.IHL) * 4
|
||||||
|
if ipHeaderLen < 20 || ipHeaderLen > len(packetData) {
|
||||||
|
return fmt.Errorf("invalid IP header length")
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.BigEndian.PutUint16(packetData[10:12], 0)
|
||||||
|
ipChecksum := ipv4Checksum(packetData[:ipHeaderLen])
|
||||||
|
binary.BigEndian.PutUint16(packetData[10:12], ipChecksum)
|
||||||
|
|
||||||
|
if len(d.decoded) > 1 {
|
||||||
|
switch d.decoded[1] {
|
||||||
|
case layers.LayerTypeTCP:
|
||||||
|
m.updateTCPChecksum(packetData, ipHeaderLen, oldSrc[:], newSrc[:])
|
||||||
|
case layers.LayerTypeUDP:
|
||||||
|
m.updateUDPChecksum(packetData, ipHeaderLen, oldSrc[:], newSrc[:])
|
||||||
case layers.LayerTypeICMPv4:
|
case layers.LayerTypeICMPv4:
|
||||||
m.updateICMPChecksum(packetData, ipHeaderLen)
|
m.updateICMPChecksum(packetData, ipHeaderLen)
|
||||||
}
|
}
|
||||||
@@ -307,7 +315,6 @@ func (m *Manager) rewritePacketIP(packetData []byte, d *decoder, newIP netip.Add
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateTCPChecksum updates TCP checksum after IP address change per RFC 1624.
|
|
||||||
func (m *Manager) updateTCPChecksum(packetData []byte, ipHeaderLen int, oldIP, newIP []byte) {
|
func (m *Manager) updateTCPChecksum(packetData []byte, ipHeaderLen int, oldIP, newIP []byte) {
|
||||||
tcpStart := ipHeaderLen
|
tcpStart := ipHeaderLen
|
||||||
if len(packetData) < tcpStart+18 {
|
if len(packetData) < tcpStart+18 {
|
||||||
@@ -320,7 +327,6 @@ func (m *Manager) updateTCPChecksum(packetData []byte, ipHeaderLen int, oldIP, n
|
|||||||
binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum)
|
binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateUDPChecksum updates UDP checksum after IP address change per RFC 1624.
|
|
||||||
func (m *Manager) updateUDPChecksum(packetData []byte, ipHeaderLen int, oldIP, newIP []byte) {
|
func (m *Manager) updateUDPChecksum(packetData []byte, ipHeaderLen int, oldIP, newIP []byte) {
|
||||||
udpStart := ipHeaderLen
|
udpStart := ipHeaderLen
|
||||||
if len(packetData) < udpStart+8 {
|
if len(packetData) < udpStart+8 {
|
||||||
@@ -338,7 +344,6 @@ func (m *Manager) updateUDPChecksum(packetData []byte, ipHeaderLen int, oldIP, n
|
|||||||
binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum)
|
binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateICMPChecksum recalculates ICMP checksum after packet modification.
|
|
||||||
func (m *Manager) updateICMPChecksum(packetData []byte, ipHeaderLen int) {
|
func (m *Manager) updateICMPChecksum(packetData []byte, ipHeaderLen int) {
|
||||||
icmpStart := ipHeaderLen
|
icmpStart := ipHeaderLen
|
||||||
if len(packetData) < icmpStart+8 {
|
if len(packetData) < icmpStart+8 {
|
||||||
@@ -351,7 +356,7 @@ func (m *Manager) updateICMPChecksum(packetData []byte, ipHeaderLen int) {
|
|||||||
binary.BigEndian.PutUint16(icmpData[2:4], checksum)
|
binary.BigEndian.PutUint16(icmpData[2:4], checksum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// incrementalUpdate performs incremental checksum update per RFC 1624.
|
// incrementalUpdate performs incremental checksum update per RFC 1624
|
||||||
func incrementalUpdate(oldChecksum uint16, oldBytes, newBytes []byte) uint16 {
|
func incrementalUpdate(oldChecksum uint16, oldBytes, newBytes []byte) uint16 {
|
||||||
sum := uint32(^oldChecksum)
|
sum := uint32(^oldChecksum)
|
||||||
|
|
||||||
@@ -386,7 +391,7 @@ func incrementalUpdate(oldChecksum uint16, oldBytes, newBytes []byte) uint16 {
|
|||||||
return ^uint16(sum)
|
return ^uint16(sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDNATRule adds outbound DNAT rule for forwarding external traffic to NetBird network.
|
// AddDNATRule adds a DNAT rule (delegates to native firewall for port forwarding)
|
||||||
func (m *Manager) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
func (m *Manager) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
||||||
if m.nativeFirewall == nil {
|
if m.nativeFirewall == nil {
|
||||||
return nil, errNatNotSupported
|
return nil, errNatNotSupported
|
||||||
@@ -394,184 +399,10 @@ func (m *Manager) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error)
|
|||||||
return m.nativeFirewall.AddDNATRule(rule)
|
return m.nativeFirewall.AddDNATRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDNATRule deletes outbound DNAT rule.
|
// DeleteDNATRule deletes a DNAT rule (delegates to native firewall)
|
||||||
func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
|
func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
|
||||||
if m.nativeFirewall == nil {
|
if m.nativeFirewall == nil {
|
||||||
return errNatNotSupported
|
return errNatNotSupported
|
||||||
}
|
}
|
||||||
return m.nativeFirewall.DeleteDNATRule(rule)
|
return m.nativeFirewall.DeleteDNATRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addPortRedirection adds a port redirection rule.
|
|
||||||
func (m *Manager) addPortRedirection(targetIP netip.Addr, protocol gopacket.LayerType, sourcePort, targetPort uint16) error {
|
|
||||||
m.portDNATMutex.Lock()
|
|
||||||
defer m.portDNATMutex.Unlock()
|
|
||||||
|
|
||||||
rule := portDNATRule{
|
|
||||||
protocol: protocol,
|
|
||||||
origPort: sourcePort,
|
|
||||||
targetPort: targetPort,
|
|
||||||
targetIP: targetIP,
|
|
||||||
}
|
|
||||||
|
|
||||||
m.portDNATRules = append(m.portDNATRules, rule)
|
|
||||||
m.portDNATEnabled.Store(true)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
|
|
||||||
func (m *Manager) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
var layerType gopacket.LayerType
|
|
||||||
switch protocol {
|
|
||||||
case firewall.ProtocolTCP:
|
|
||||||
layerType = layers.LayerTypeTCP
|
|
||||||
case firewall.ProtocolUDP:
|
|
||||||
layerType = layers.LayerTypeUDP
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported protocol: %s", protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.addPortRedirection(localAddr, layerType, sourcePort, targetPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// removePortRedirection removes a port redirection rule.
|
|
||||||
func (m *Manager) removePortRedirection(targetIP netip.Addr, protocol gopacket.LayerType, sourcePort, targetPort uint16) error {
|
|
||||||
m.portDNATMutex.Lock()
|
|
||||||
defer m.portDNATMutex.Unlock()
|
|
||||||
|
|
||||||
m.portDNATRules = slices.DeleteFunc(m.portDNATRules, func(rule portDNATRule) bool {
|
|
||||||
return rule.protocol == protocol && rule.origPort == sourcePort && rule.targetPort == targetPort && rule.targetIP.Compare(targetIP) == 0
|
|
||||||
})
|
|
||||||
|
|
||||||
if len(m.portDNATRules) == 0 {
|
|
||||||
m.portDNATEnabled.Store(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveInboundDNAT removes an inbound DNAT rule.
|
|
||||||
func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
|
||||||
var layerType gopacket.LayerType
|
|
||||||
switch protocol {
|
|
||||||
case firewall.ProtocolTCP:
|
|
||||||
layerType = layers.LayerTypeTCP
|
|
||||||
case firewall.ProtocolUDP:
|
|
||||||
layerType = layers.LayerTypeUDP
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported protocol: %s", protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.removePortRedirection(localAddr, layerType, sourcePort, targetPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
// translateInboundPortDNAT applies port-specific DNAT translation to inbound packets.
|
|
||||||
func (m *Manager) translateInboundPortDNAT(packetData []byte, d *decoder, srcIP, dstIP netip.Addr) bool {
|
|
||||||
if !m.portDNATEnabled.Load() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
switch d.decoded[1] {
|
|
||||||
case layers.LayerTypeTCP:
|
|
||||||
dstPort := uint16(d.tcp.DstPort)
|
|
||||||
return m.applyPortRule(packetData, d, srcIP, dstIP, dstPort, layers.LayerTypeTCP, m.rewriteTCPPort)
|
|
||||||
case layers.LayerTypeUDP:
|
|
||||||
dstPort := uint16(d.udp.DstPort)
|
|
||||||
return m.applyPortRule(packetData, d, netip.Addr{}, dstIP, dstPort, layers.LayerTypeUDP, m.rewriteUDPPort)
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type portRewriteFunc func(packetData []byte, d *decoder, newPort uint16, portOffset int) error
|
|
||||||
|
|
||||||
func (m *Manager) applyPortRule(packetData []byte, d *decoder, srcIP, dstIP netip.Addr, port uint16, protocol gopacket.LayerType, rewriteFn portRewriteFunc) bool {
|
|
||||||
m.portDNATMutex.RLock()
|
|
||||||
defer m.portDNATMutex.RUnlock()
|
|
||||||
|
|
||||||
for _, rule := range m.portDNATRules {
|
|
||||||
if rule.protocol != protocol || rule.targetIP.Compare(dstIP) != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule.targetPort == port && rule.targetIP.Compare(srcIP) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if rule.origPort != port {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := rewriteFn(packetData, d, rule.targetPort, destinationPortOffset); err != nil {
|
|
||||||
m.logger.Error1("failed to rewrite port: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
d.dnatOrigPort = rule.origPort
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// rewriteTCPPort rewrites a TCP port (source or destination) and updates checksum.
|
|
||||||
func (m *Manager) rewriteTCPPort(packetData []byte, d *decoder, newPort uint16, portOffset int) error {
|
|
||||||
ipHeaderLen := int(d.ip4.IHL) * 4
|
|
||||||
if ipHeaderLen < 20 || ipHeaderLen > len(packetData) {
|
|
||||||
return errInvalidIPHeaderLength
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpStart := ipHeaderLen
|
|
||||||
if len(packetData) < tcpStart+4 {
|
|
||||||
return fmt.Errorf("packet too short for TCP header")
|
|
||||||
}
|
|
||||||
|
|
||||||
portStart := tcpStart + portOffset
|
|
||||||
oldPort := binary.BigEndian.Uint16(packetData[portStart : portStart+2])
|
|
||||||
binary.BigEndian.PutUint16(packetData[portStart:portStart+2], newPort)
|
|
||||||
|
|
||||||
if len(packetData) >= tcpStart+18 {
|
|
||||||
checksumOffset := tcpStart + 16
|
|
||||||
oldChecksum := binary.BigEndian.Uint16(packetData[checksumOffset : checksumOffset+2])
|
|
||||||
|
|
||||||
var oldPortBytes, newPortBytes [2]byte
|
|
||||||
binary.BigEndian.PutUint16(oldPortBytes[:], oldPort)
|
|
||||||
binary.BigEndian.PutUint16(newPortBytes[:], newPort)
|
|
||||||
|
|
||||||
newChecksum := incrementalUpdate(oldChecksum, oldPortBytes[:], newPortBytes[:])
|
|
||||||
binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// rewriteUDPPort rewrites a UDP port (source or destination) and updates checksum.
|
|
||||||
func (m *Manager) rewriteUDPPort(packetData []byte, d *decoder, newPort uint16, portOffset int) error {
|
|
||||||
ipHeaderLen := int(d.ip4.IHL) * 4
|
|
||||||
if ipHeaderLen < 20 || ipHeaderLen > len(packetData) {
|
|
||||||
return errInvalidIPHeaderLength
|
|
||||||
}
|
|
||||||
|
|
||||||
udpStart := ipHeaderLen
|
|
||||||
if len(packetData) < udpStart+8 {
|
|
||||||
return fmt.Errorf("packet too short for UDP header")
|
|
||||||
}
|
|
||||||
|
|
||||||
portStart := udpStart + portOffset
|
|
||||||
oldPort := binary.BigEndian.Uint16(packetData[portStart : portStart+2])
|
|
||||||
binary.BigEndian.PutUint16(packetData[portStart:portStart+2], newPort)
|
|
||||||
|
|
||||||
checksumOffset := udpStart + 6
|
|
||||||
if len(packetData) >= udpStart+8 {
|
|
||||||
oldChecksum := binary.BigEndian.Uint16(packetData[checksumOffset : checksumOffset+2])
|
|
||||||
if oldChecksum != 0 {
|
|
||||||
var oldPortBytes, newPortBytes [2]byte
|
|
||||||
binary.BigEndian.PutUint16(oldPortBytes[:], oldPort)
|
|
||||||
binary.BigEndian.PutUint16(newPortBytes[:], newPort)
|
|
||||||
|
|
||||||
newChecksum := incrementalUpdate(oldChecksum, oldPortBytes[:], newPortBytes[:])
|
|
||||||
binary.BigEndian.PutUint16(packetData[checksumOffset:checksumOffset+2], newChecksum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,7 +65,7 @@ func BenchmarkDNATTranslation(b *testing.B) {
|
|||||||
b.Run(sc.name, func(b *testing.B) {
|
b.Run(sc.name, func(b *testing.B) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
@@ -126,7 +125,7 @@ func BenchmarkDNATTranslation(b *testing.B) {
|
|||||||
func BenchmarkDNATConcurrency(b *testing.B) {
|
func BenchmarkDNATConcurrency(b *testing.B) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
@@ -198,7 +197,7 @@ func BenchmarkDNATScaling(b *testing.B) {
|
|||||||
b.Run(fmt.Sprintf("mappings_%d", count), func(b *testing.B) {
|
b.Run(fmt.Sprintf("mappings_%d", count), func(b *testing.B) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
@@ -310,7 +309,7 @@ func BenchmarkChecksumUpdate(b *testing.B) {
|
|||||||
func BenchmarkDNATMemoryAllocations(b *testing.B) {
|
func BenchmarkDNATMemoryAllocations(b *testing.B) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
@@ -415,127 +414,3 @@ func BenchmarkChecksumOptimizations(b *testing.B) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkPortDNAT measures the performance of port DNAT operations
|
|
||||||
func BenchmarkPortDNAT(b *testing.B) {
|
|
||||||
scenarios := []struct {
|
|
||||||
name string
|
|
||||||
proto layers.IPProtocol
|
|
||||||
setupDNAT bool
|
|
||||||
useMatchPort bool
|
|
||||||
description string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "tcp_inbound_dnat_match",
|
|
||||||
proto: layers.IPProtocolTCP,
|
|
||||||
setupDNAT: true,
|
|
||||||
useMatchPort: true,
|
|
||||||
description: "TCP inbound port DNAT translation (22 → 22022)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tcp_inbound_dnat_nomatch",
|
|
||||||
proto: layers.IPProtocolTCP,
|
|
||||||
setupDNAT: true,
|
|
||||||
useMatchPort: false,
|
|
||||||
description: "TCP inbound with DNAT configured but no port match",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tcp_inbound_no_dnat",
|
|
||||||
proto: layers.IPProtocolTCP,
|
|
||||||
setupDNAT: false,
|
|
||||||
useMatchPort: false,
|
|
||||||
description: "TCP inbound without DNAT (baseline)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "udp_inbound_dnat_match",
|
|
||||||
proto: layers.IPProtocolUDP,
|
|
||||||
setupDNAT: true,
|
|
||||||
useMatchPort: true,
|
|
||||||
description: "UDP inbound port DNAT translation (5353 → 22054)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "udp_inbound_dnat_nomatch",
|
|
||||||
proto: layers.IPProtocolUDP,
|
|
||||||
setupDNAT: true,
|
|
||||||
useMatchPort: false,
|
|
||||||
description: "UDP inbound with DNAT configured but no port match",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "udp_inbound_no_dnat",
|
|
||||||
proto: layers.IPProtocolUDP,
|
|
||||||
setupDNAT: false,
|
|
||||||
useMatchPort: false,
|
|
||||||
description: "UDP inbound without DNAT (baseline)",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, sc := range scenarios {
|
|
||||||
b.Run(sc.name, func(b *testing.B) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(b, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(b, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Set logger to error level to reduce noise during benchmarking
|
|
||||||
manager.SetLogLevel(log.ErrorLevel)
|
|
||||||
defer func() {
|
|
||||||
// Restore to info level after benchmark
|
|
||||||
manager.SetLogLevel(log.InfoLevel)
|
|
||||||
}()
|
|
||||||
|
|
||||||
localAddr := netip.MustParseAddr("100.0.2.175")
|
|
||||||
clientIP := netip.MustParseAddr("100.0.169.249")
|
|
||||||
|
|
||||||
var origPort, targetPort, testPort uint16
|
|
||||||
if sc.proto == layers.IPProtocolTCP {
|
|
||||||
origPort, targetPort = 22, 22022
|
|
||||||
} else {
|
|
||||||
origPort, targetPort = 5353, 22054
|
|
||||||
}
|
|
||||||
|
|
||||||
if sc.useMatchPort {
|
|
||||||
testPort = origPort
|
|
||||||
} else {
|
|
||||||
testPort = 443 // Different port
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup port DNAT mapping if needed
|
|
||||||
if sc.setupDNAT {
|
|
||||||
err := manager.AddInboundDNAT(localAddr, protocolToFirewall(sc.proto), origPort, targetPort)
|
|
||||||
require.NoError(b, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-establish inbound connection for outbound reverse test
|
|
||||||
if sc.setupDNAT && sc.useMatchPort {
|
|
||||||
inboundPacket := generateDNATTestPacket(b, clientIP, localAddr, sc.proto, 54321, origPort)
|
|
||||||
manager.filterInbound(inboundPacket, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
b.ReportAllocs()
|
|
||||||
|
|
||||||
// Benchmark inbound DNAT translation
|
|
||||||
b.Run("inbound", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
// Create fresh packet each time
|
|
||||||
packet := generateDNATTestPacket(b, clientIP, localAddr, sc.proto, 54321, testPort)
|
|
||||||
manager.filterInbound(packet, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Benchmark outbound reverse DNAT translation (only if DNAT is set up and port matches)
|
|
||||||
if sc.setupDNAT && sc.useMatchPort {
|
|
||||||
b.Run("outbound_reverse", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
// Create fresh return packet (from target port)
|
|
||||||
packet := generateDNATTestPacket(b, localAddr, clientIP, sc.proto, targetPort, 54321)
|
|
||||||
manager.filterOutbound(packet, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
package uspfilter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/gopacket/layers"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestPortDNATBasic tests basic port DNAT functionality
|
|
||||||
func TestPortDNATBasic(t *testing.T) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Define peer IPs
|
|
||||||
peerA := netip.MustParseAddr("100.10.0.50")
|
|
||||||
peerB := netip.MustParseAddr("100.10.0.51")
|
|
||||||
|
|
||||||
// Add SSH port redirection rule for peer B (the target)
|
|
||||||
err = manager.addPortRedirection(peerB, layers.LayerTypeTCP, 22, 22022)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Scenario: Peer A connects to Peer B on port 22 (should get NAT)
|
|
||||||
packetAtoB := generateDNATTestPacket(t, peerA, peerB, layers.IPProtocolTCP, 54321, 22)
|
|
||||||
d := parsePacket(t, packetAtoB)
|
|
||||||
translatedAtoB := manager.translateInboundPortDNAT(packetAtoB, d, peerA, peerB)
|
|
||||||
require.True(t, translatedAtoB, "Peer A to Peer B should be translated (NAT applied)")
|
|
||||||
|
|
||||||
// Verify port was translated to 22022
|
|
||||||
d = parsePacket(t, packetAtoB)
|
|
||||||
require.Equal(t, uint16(22022), uint16(d.tcp.DstPort), "Port should be rewritten to 22022")
|
|
||||||
|
|
||||||
// Scenario: Return traffic from Peer B to Peer A should NOT be translated
|
|
||||||
// (prevents double NAT - original port stored in conntrack)
|
|
||||||
returnPacket := generateDNATTestPacket(t, peerB, peerA, layers.IPProtocolTCP, 22022, 54321)
|
|
||||||
d2 := parsePacket(t, returnPacket)
|
|
||||||
translatedReturn := manager.translateInboundPortDNAT(returnPacket, d2, peerB, peerA)
|
|
||||||
require.False(t, translatedReturn, "Return traffic from same IP should not be translated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPortDNATMultipleRules tests multiple port DNAT rules
|
|
||||||
func TestPortDNATMultipleRules(t *testing.T) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Define peer IPs
|
|
||||||
peerA := netip.MustParseAddr("100.10.0.50")
|
|
||||||
peerB := netip.MustParseAddr("100.10.0.51")
|
|
||||||
|
|
||||||
// Add SSH port redirection rules for both peers
|
|
||||||
err = manager.addPortRedirection(peerA, layers.LayerTypeTCP, 22, 22022)
|
|
||||||
require.NoError(t, err)
|
|
||||||
err = manager.addPortRedirection(peerB, layers.LayerTypeTCP, 22, 22022)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Test traffic to peer B gets translated
|
|
||||||
packetToB := generateDNATTestPacket(t, peerA, peerB, layers.IPProtocolTCP, 54321, 22)
|
|
||||||
d1 := parsePacket(t, packetToB)
|
|
||||||
translatedToB := manager.translateInboundPortDNAT(packetToB, d1, peerA, peerB)
|
|
||||||
require.True(t, translatedToB, "Traffic to peer B should be translated")
|
|
||||||
d1 = parsePacket(t, packetToB)
|
|
||||||
require.Equal(t, uint16(22022), uint16(d1.tcp.DstPort), "Port should be 22022")
|
|
||||||
|
|
||||||
// Test traffic to peer A gets translated
|
|
||||||
packetToA := generateDNATTestPacket(t, peerB, peerA, layers.IPProtocolTCP, 54322, 22)
|
|
||||||
d2 := parsePacket(t, packetToA)
|
|
||||||
translatedToA := manager.translateInboundPortDNAT(packetToA, d2, peerB, peerA)
|
|
||||||
require.True(t, translatedToA, "Traffic to peer A should be translated")
|
|
||||||
d2 = parsePacket(t, packetToA)
|
|
||||||
require.Equal(t, uint16(22022), uint16(d2.tcp.DstPort), "Port should be 22022")
|
|
||||||
}
|
|
||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,7 +15,7 @@ import (
|
|||||||
func TestDNATTranslationCorrectness(t *testing.T) {
|
func TestDNATTranslationCorrectness(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
@@ -101,7 +99,7 @@ func parsePacket(t testing.TB, packetData []byte) *decoder {
|
|||||||
func TestDNATMappingManagement(t *testing.T) {
|
func TestDNATMappingManagement(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
}, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
@@ -145,111 +143,3 @@ func TestDNATMappingManagement(t *testing.T) {
|
|||||||
err = manager.RemoveInternalDNATMapping(originalIP)
|
err = manager.RemoveInternalDNATMapping(originalIP)
|
||||||
require.Error(t, err, "Should error when removing non-existent mapping")
|
require.Error(t, err, "Should error when removing non-existent mapping")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInboundPortDNAT(t *testing.T) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
localAddr := netip.MustParseAddr("100.0.2.175")
|
|
||||||
clientIP := netip.MustParseAddr("100.0.169.249")
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
protocol layers.IPProtocol
|
|
||||||
sourcePort uint16
|
|
||||||
targetPort uint16
|
|
||||||
}{
|
|
||||||
{"TCP SSH", layers.IPProtocolTCP, 22, 22022},
|
|
||||||
{"UDP DNS", layers.IPProtocolUDP, 5353, 22054},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
err := manager.AddInboundDNAT(localAddr, protocolToFirewall(tc.protocol), tc.sourcePort, tc.targetPort)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
inboundPacket := generateDNATTestPacket(t, clientIP, localAddr, tc.protocol, 54321, tc.sourcePort)
|
|
||||||
d := parsePacket(t, inboundPacket)
|
|
||||||
|
|
||||||
translated := manager.translateInboundPortDNAT(inboundPacket, d, clientIP, localAddr)
|
|
||||||
require.True(t, translated, "Inbound packet should be translated")
|
|
||||||
|
|
||||||
d = parsePacket(t, inboundPacket)
|
|
||||||
var dstPort uint16
|
|
||||||
switch tc.protocol {
|
|
||||||
case layers.IPProtocolTCP:
|
|
||||||
dstPort = uint16(d.tcp.DstPort)
|
|
||||||
case layers.IPProtocolUDP:
|
|
||||||
dstPort = uint16(d.udp.DstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
require.Equal(t, tc.targetPort, dstPort, "Destination port should be rewritten to target port")
|
|
||||||
|
|
||||||
err = manager.RemoveInboundDNAT(localAddr, protocolToFirewall(tc.protocol), tc.sourcePort, tc.targetPort)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInboundPortDNATNegative(t *testing.T) {
|
|
||||||
manager, err := Create(&IFaceMock{
|
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
|
||||||
}, false, flowLogger, iface.DefaultMTU)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
require.NoError(t, manager.Close(nil))
|
|
||||||
}()
|
|
||||||
|
|
||||||
localAddr := netip.MustParseAddr("100.0.2.175")
|
|
||||||
clientIP := netip.MustParseAddr("100.0.169.249")
|
|
||||||
|
|
||||||
err = manager.AddInboundDNAT(localAddr, firewall.ProtocolTCP, 22, 22022)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
protocol layers.IPProtocol
|
|
||||||
srcIP netip.Addr
|
|
||||||
dstIP netip.Addr
|
|
||||||
srcPort uint16
|
|
||||||
dstPort uint16
|
|
||||||
}{
|
|
||||||
{"Wrong port", layers.IPProtocolTCP, clientIP, localAddr, 54321, 80},
|
|
||||||
{"Wrong IP", layers.IPProtocolTCP, clientIP, netip.MustParseAddr("100.64.0.99"), 54321, 22},
|
|
||||||
{"Wrong protocol", layers.IPProtocolUDP, clientIP, localAddr, 54321, 22},
|
|
||||||
{"ICMP", layers.IPProtocolICMPv4, clientIP, localAddr, 0, 0},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
packet := generateDNATTestPacket(t, tc.srcIP, tc.dstIP, tc.protocol, tc.srcPort, tc.dstPort)
|
|
||||||
d := parsePacket(t, packet)
|
|
||||||
|
|
||||||
translated := manager.translateInboundPortDNAT(packet, d, tc.srcIP, tc.dstIP)
|
|
||||||
require.False(t, translated, "Packet should NOT be translated for %s", tc.name)
|
|
||||||
|
|
||||||
d = parsePacket(t, packet)
|
|
||||||
if tc.protocol == layers.IPProtocolTCP {
|
|
||||||
require.Equal(t, tc.dstPort, uint16(d.tcp.DstPort), "Port should remain unchanged")
|
|
||||||
} else if tc.protocol == layers.IPProtocolUDP {
|
|
||||||
require.Equal(t, tc.dstPort, uint16(d.udp.DstPort), "Port should remain unchanged")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func protocolToFirewall(proto layers.IPProtocol) firewall.Protocol {
|
|
||||||
switch proto {
|
|
||||||
case layers.IPProtocolTCP:
|
|
||||||
return firewall.ProtocolTCP
|
|
||||||
case layers.IPProtocolUDP:
|
|
||||||
return firewall.ProtocolUDP
|
|
||||||
default:
|
|
||||||
return firewall.ProtocolALL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,33 +16,25 @@ type PacketStage int
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
StageReceived PacketStage = iota
|
StageReceived PacketStage = iota
|
||||||
StageInboundPortDNAT
|
|
||||||
StageInbound1to1NAT
|
|
||||||
StageConntrack
|
StageConntrack
|
||||||
StagePeerACL
|
StagePeerACL
|
||||||
StageRouting
|
StageRouting
|
||||||
StageRouteACL
|
StageRouteACL
|
||||||
StageForwarding
|
StageForwarding
|
||||||
StageCompleted
|
StageCompleted
|
||||||
StageOutbound1to1NAT
|
|
||||||
StageOutboundPortReverse
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const msgProcessingCompleted = "Processing completed"
|
const msgProcessingCompleted = "Processing completed"
|
||||||
|
|
||||||
func (s PacketStage) String() string {
|
func (s PacketStage) String() string {
|
||||||
return map[PacketStage]string{
|
return map[PacketStage]string{
|
||||||
StageReceived: "Received",
|
StageReceived: "Received",
|
||||||
StageInboundPortDNAT: "Inbound Port DNAT",
|
StageConntrack: "Connection Tracking",
|
||||||
StageInbound1to1NAT: "Inbound 1:1 NAT",
|
StagePeerACL: "Peer ACL",
|
||||||
StageConntrack: "Connection Tracking",
|
StageRouting: "Routing",
|
||||||
StagePeerACL: "Peer ACL",
|
StageRouteACL: "Route ACL",
|
||||||
StageRouting: "Routing",
|
StageForwarding: "Forwarding",
|
||||||
StageRouteACL: "Route ACL",
|
StageCompleted: "Completed",
|
||||||
StageForwarding: "Forwarding",
|
|
||||||
StageCompleted: "Completed",
|
|
||||||
StageOutbound1to1NAT: "Outbound 1:1 NAT",
|
|
||||||
StageOutboundPortReverse: "Outbound DNAT Reverse",
|
|
||||||
}[s]
|
}[s]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,10 +261,6 @@ func (m *Manager) TracePacket(packetData []byte, direction fw.RuleDirection) *Pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) traceInbound(packetData []byte, trace *PacketTrace, d *decoder, srcIP netip.Addr, dstIP netip.Addr) *PacketTrace {
|
func (m *Manager) traceInbound(packetData []byte, trace *PacketTrace, d *decoder, srcIP netip.Addr, dstIP netip.Addr) *PacketTrace {
|
||||||
if m.handleInboundDNAT(trace, packetData, d, &srcIP, &dstIP) {
|
|
||||||
return trace
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.stateful && m.handleConntrackState(trace, d, srcIP, dstIP) {
|
if m.stateful && m.handleConntrackState(trace, d, srcIP, dstIP) {
|
||||||
return trace
|
return trace
|
||||||
}
|
}
|
||||||
@@ -412,16 +400,7 @@ func (m *Manager) addForwardingResult(trace *PacketTrace, action, remoteAddr str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTrace {
|
func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTrace {
|
||||||
d := m.decoders.Get().(*decoder)
|
// will create or update the connection state
|
||||||
defer m.decoders.Put(d)
|
|
||||||
|
|
||||||
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
|
|
||||||
trace.AddResult(StageCompleted, "Packet dropped - decode error", false)
|
|
||||||
return trace
|
|
||||||
}
|
|
||||||
|
|
||||||
m.handleOutboundDNAT(trace, packetData, d)
|
|
||||||
|
|
||||||
dropped := m.filterOutbound(packetData, 0)
|
dropped := m.filterOutbound(packetData, 0)
|
||||||
if dropped {
|
if dropped {
|
||||||
trace.AddResult(StageCompleted, "Packet dropped by outgoing hook", false)
|
trace.AddResult(StageCompleted, "Packet dropped by outgoing hook", false)
|
||||||
@@ -430,199 +409,3 @@ func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTr
|
|||||||
}
|
}
|
||||||
return trace
|
return trace
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) handleInboundDNAT(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP *netip.Addr) bool {
|
|
||||||
portDNATApplied := m.traceInboundPortDNAT(trace, packetData, d)
|
|
||||||
if portDNATApplied {
|
|
||||||
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
|
|
||||||
trace.AddResult(StageInboundPortDNAT, "Failed to re-decode after port DNAT", false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
*srcIP, *dstIP = m.extractIPs(d)
|
|
||||||
trace.DestinationPort = m.getDestPort(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
nat1to1Applied := m.traceInbound1to1NAT(trace, packetData, d)
|
|
||||||
if nat1to1Applied {
|
|
||||||
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
|
|
||||||
trace.AddResult(StageInbound1to1NAT, "Failed to re-decode after 1:1 NAT", false)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
*srcIP, *dstIP = m.extractIPs(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) traceInboundPortDNAT(trace *PacketTrace, packetData []byte, d *decoder) bool {
|
|
||||||
if !m.portDNATEnabled.Load() {
|
|
||||||
trace.AddResult(StageInboundPortDNAT, "Port DNAT not enabled", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 {
|
|
||||||
trace.AddResult(StageInboundPortDNAT, "Not IPv4, skipping port DNAT", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.decoded) < 2 {
|
|
||||||
trace.AddResult(StageInboundPortDNAT, "No transport layer, skipping port DNAT", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol := d.decoded[1]
|
|
||||||
if protocol != layers.LayerTypeTCP && protocol != layers.LayerTypeUDP {
|
|
||||||
trace.AddResult(StageInboundPortDNAT, "Not TCP/UDP, skipping port DNAT", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
srcIP := netip.AddrFrom4([4]byte{packetData[12], packetData[13], packetData[14], packetData[15]})
|
|
||||||
dstIP := netip.AddrFrom4([4]byte{packetData[16], packetData[17], packetData[18], packetData[19]})
|
|
||||||
var originalPort uint16
|
|
||||||
if protocol == layers.LayerTypeTCP {
|
|
||||||
originalPort = uint16(d.tcp.DstPort)
|
|
||||||
} else {
|
|
||||||
originalPort = uint16(d.udp.DstPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
translated := m.translateInboundPortDNAT(packetData, d, srcIP, dstIP)
|
|
||||||
if translated {
|
|
||||||
ipHeaderLen := int((packetData[0] & 0x0F) * 4)
|
|
||||||
translatedPort := uint16(packetData[ipHeaderLen+2])<<8 | uint16(packetData[ipHeaderLen+3])
|
|
||||||
|
|
||||||
protoStr := "TCP"
|
|
||||||
if protocol == layers.LayerTypeUDP {
|
|
||||||
protoStr = "UDP"
|
|
||||||
}
|
|
||||||
msg := fmt.Sprintf("%s port DNAT applied: %s:%d -> %s:%d", protoStr, dstIP, originalPort, dstIP, translatedPort)
|
|
||||||
trace.AddResult(StageInboundPortDNAT, msg, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
trace.AddResult(StageInboundPortDNAT, "No matching port DNAT rule", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) traceInbound1to1NAT(trace *PacketTrace, packetData []byte, d *decoder) bool {
|
|
||||||
if !m.dnatEnabled.Load() {
|
|
||||||
trace.AddResult(StageInbound1to1NAT, "1:1 NAT not enabled", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
srcIP := netip.AddrFrom4([4]byte{packetData[12], packetData[13], packetData[14], packetData[15]})
|
|
||||||
|
|
||||||
translated := m.translateInboundReverse(packetData, d)
|
|
||||||
if translated {
|
|
||||||
m.dnatMutex.RLock()
|
|
||||||
translatedIP, exists := m.dnatBiMap.getOriginal(srcIP)
|
|
||||||
m.dnatMutex.RUnlock()
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
msg := fmt.Sprintf("1:1 NAT reverse applied: %s -> %s", srcIP, translatedIP)
|
|
||||||
trace.AddResult(StageInbound1to1NAT, msg, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trace.AddResult(StageInbound1to1NAT, "No matching 1:1 NAT rule", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) handleOutboundDNAT(trace *PacketTrace, packetData []byte, d *decoder) {
|
|
||||||
m.traceOutbound1to1NAT(trace, packetData, d)
|
|
||||||
m.traceOutboundPortReverse(trace, packetData, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) traceOutbound1to1NAT(trace *PacketTrace, packetData []byte, d *decoder) bool {
|
|
||||||
if !m.dnatEnabled.Load() {
|
|
||||||
trace.AddResult(StageOutbound1to1NAT, "1:1 NAT not enabled", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
dstIP := netip.AddrFrom4([4]byte{packetData[16], packetData[17], packetData[18], packetData[19]})
|
|
||||||
|
|
||||||
translated := m.translateOutboundDNAT(packetData, d)
|
|
||||||
if translated {
|
|
||||||
m.dnatMutex.RLock()
|
|
||||||
translatedIP, exists := m.dnatMappings[dstIP]
|
|
||||||
m.dnatMutex.RUnlock()
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
msg := fmt.Sprintf("1:1 NAT applied: %s -> %s", dstIP, translatedIP)
|
|
||||||
trace.AddResult(StageOutbound1to1NAT, msg, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trace.AddResult(StageOutbound1to1NAT, "No matching 1:1 NAT rule", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) traceOutboundPortReverse(trace *PacketTrace, packetData []byte, d *decoder) bool {
|
|
||||||
if !m.portDNATEnabled.Load() {
|
|
||||||
trace.AddResult(StageOutboundPortReverse, "Port DNAT not enabled", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 {
|
|
||||||
trace.AddResult(StageOutboundPortReverse, "Not IPv4, skipping port reverse", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(d.decoded) < 2 {
|
|
||||||
trace.AddResult(StageOutboundPortReverse, "No transport layer, skipping port reverse", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
srcIP := netip.AddrFrom4([4]byte{packetData[12], packetData[13], packetData[14], packetData[15]})
|
|
||||||
dstIP := netip.AddrFrom4([4]byte{packetData[16], packetData[17], packetData[18], packetData[19]})
|
|
||||||
|
|
||||||
var origPort uint16
|
|
||||||
transport := d.decoded[1]
|
|
||||||
switch transport {
|
|
||||||
case layers.LayerTypeTCP:
|
|
||||||
srcPort := uint16(d.tcp.SrcPort)
|
|
||||||
dstPort := uint16(d.tcp.DstPort)
|
|
||||||
conn, exists := m.tcpTracker.GetConnection(dstIP, dstPort, srcIP, srcPort)
|
|
||||||
if exists {
|
|
||||||
origPort = uint16(conn.DNATOrigPort.Load())
|
|
||||||
}
|
|
||||||
if origPort != 0 {
|
|
||||||
msg := fmt.Sprintf("TCP DNAT reverse (tracked connection): %s:%d -> %s:%d", srcIP, srcPort, srcIP, origPort)
|
|
||||||
trace.AddResult(StageOutboundPortReverse, msg, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
case layers.LayerTypeUDP:
|
|
||||||
srcPort := uint16(d.udp.SrcPort)
|
|
||||||
dstPort := uint16(d.udp.DstPort)
|
|
||||||
conn, exists := m.udpTracker.GetConnection(dstIP, dstPort, srcIP, srcPort)
|
|
||||||
if exists {
|
|
||||||
origPort = uint16(conn.DNATOrigPort.Load())
|
|
||||||
}
|
|
||||||
if origPort != 0 {
|
|
||||||
msg := fmt.Sprintf("UDP DNAT reverse (tracked connection): %s:%d -> %s:%d", srcIP, srcPort, srcIP, origPort)
|
|
||||||
trace.AddResult(StageOutboundPortReverse, msg, true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
trace.AddResult(StageOutboundPortReverse, "Not TCP/UDP, skipping port reverse", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
trace.AddResult(StageOutboundPortReverse, "No tracked connection for DNAT reverse", true)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) getDestPort(d *decoder) uint16 {
|
|
||||||
if len(d.decoded) < 2 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
switch d.decoded[1] {
|
|
||||||
case layers.LayerTypeTCP:
|
|
||||||
return uint16(d.tcp.DstPort)
|
|
||||||
case layers.LayerTypeUDP:
|
|
||||||
return uint16(d.udp.DstPort)
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
@@ -45,7 +44,7 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock, false, flowLogger, iface.DefaultMTU)
|
m, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
if !statefulMode {
|
if !statefulMode {
|
||||||
@@ -105,8 +104,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -129,8 +126,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -158,8 +153,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -186,8 +179,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -213,8 +204,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageRouteACL,
|
StageRouteACL,
|
||||||
@@ -239,8 +228,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageRouteACL,
|
StageRouteACL,
|
||||||
@@ -259,8 +246,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageRouteACL,
|
StageRouteACL,
|
||||||
@@ -279,8 +264,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
@@ -304,8 +287,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
},
|
},
|
||||||
@@ -320,8 +301,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageOutbound1to1NAT,
|
|
||||||
StageOutboundPortReverse,
|
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
},
|
},
|
||||||
expectedAllow: true,
|
expectedAllow: true,
|
||||||
@@ -340,8 +319,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -363,8 +340,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -387,8 +362,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -409,8 +382,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -435,8 +406,6 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
|
||||||
StageInbound1to1NAT,
|
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
package grpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
|
||||||
"google.golang.org/grpc/keepalive"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util/embeddedroots"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Backoff returns a backoff configuration for gRPC calls
|
|
||||||
func Backoff(ctx context.Context) backoff.BackOff {
|
|
||||||
b := backoff.NewExponentialBackOff()
|
|
||||||
b.MaxElapsedTime = 10 * time.Second
|
|
||||||
b.Clock = backoff.SystemClock
|
|
||||||
return backoff.WithContext(b, ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateConnection creates a gRPC client connection with the appropriate transport options.
|
|
||||||
// The component parameter specifies the WebSocket proxy component path (e.g., "/management", "/signal").
|
|
||||||
func CreateConnection(ctx context.Context, addr string, tlsEnabled bool, component string) (*grpc.ClientConn, error) {
|
|
||||||
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
|
|
||||||
// for js, the outer websocket layer takes care of tls
|
|
||||||
if tlsEnabled && runtime.GOOS != "js" {
|
|
||||||
certPool, err := x509.SystemCertPool()
|
|
||||||
if err != nil || certPool == nil {
|
|
||||||
log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
|
|
||||||
certPool = embeddedroots.Get()
|
|
||||||
}
|
|
||||||
|
|
||||||
transportOption = grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
|
|
||||||
RootCAs: certPool,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
connCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := grpc.DialContext(
|
|
||||||
connCtx,
|
|
||||||
addr,
|
|
||||||
transportOption,
|
|
||||||
WithCustomDialer(tlsEnabled, component),
|
|
||||||
grpc.WithBlock(),
|
|
||||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
|
||||||
Time: 30 * time.Second,
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("dial context: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package grpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os/user"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WithCustomDialer(_ bool, _ string) grpc.DialOption {
|
|
||||||
return grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
|
||||||
if runtime.GOOS == "linux" {
|
|
||||||
currentUser, err := user.Current()
|
|
||||||
if err != nil {
|
|
||||||
return nil, status.Errorf(codes.FailedPrecondition, "failed to get current user: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// the custom dialer requires root permissions which are not required for use cases run as non-root
|
|
||||||
if currentUser.Uid != "0" {
|
|
||||||
log.Debug("Not running as root, using standard dialer")
|
|
||||||
dialer := &net.Dialer{}
|
|
||||||
return dialer.DialContext(ctx, "tcp", addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := nbnet.NewDialer().DialContext(ctx, "tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
package grpc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util/wsproxy/client"
|
|
||||||
)
|
|
||||||
|
|
||||||
// WithCustomDialer returns a gRPC dial option that uses WebSocket transport for WASM/JS environments.
|
|
||||||
// The component parameter specifies the WebSocket proxy component path (e.g., "/management", "/signal").
|
|
||||||
func WithCustomDialer(tlsEnabled bool, component string) grpc.DialOption {
|
|
||||||
return client.WithWebSocketDialer(tlsEnabled, component)
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ package bind
|
|||||||
import (
|
import (
|
||||||
wireguard "golang.zx2c4.com/wireguard/conn"
|
wireguard "golang.zx2c4.com/wireguard/conn"
|
||||||
|
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
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)
|
// TODO: This is most likely obsolete since the control fns should be called by the wrapped udpconn (ice_bind.go)
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
package bind
|
package bind
|
||||||
|
|
||||||
import (
|
import wgConn "golang.zx2c4.com/wireguard/conn"
|
||||||
"net"
|
|
||||||
|
|
||||||
wgConn "golang.zx2c4.com/wireguard/conn"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Endpoint = wgConn.StdNetEndpoint
|
type Endpoint = wgConn.StdNetEndpoint
|
||||||
|
|
||||||
func EndpointToUDPAddr(e Endpoint) *net.UDPAddr {
|
|
||||||
return &net.UDPAddr{
|
|
||||||
IP: e.Addr().AsSlice(),
|
|
||||||
Port: int(e.Port()),
|
|
||||||
Zone: e.Addr().Zone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package bind
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrUDPMUXNotSupported = fmt.Errorf("UDPMUX is not supported in WASM")
|
|
||||||
)
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
//go:build !js
|
|
||||||
|
|
||||||
package bind
|
package bind
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -20,9 +17,14 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/udpmux"
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type RecvMessage struct {
|
||||||
|
Endpoint *Endpoint
|
||||||
|
Buffer []byte
|
||||||
|
}
|
||||||
|
|
||||||
type receiverCreator struct {
|
type receiverCreator struct {
|
||||||
iceBind *ICEBind
|
iceBind *ICEBind
|
||||||
}
|
}
|
||||||
@@ -40,38 +42,37 @@ func (rc receiverCreator) CreateIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UD
|
|||||||
// use the port because in the Send function the wgConn.Endpoint the port info is not exported.
|
// use the port because in the Send function the wgConn.Endpoint the port info is not exported.
|
||||||
type ICEBind struct {
|
type ICEBind struct {
|
||||||
*wgConn.StdNetBind
|
*wgConn.StdNetBind
|
||||||
|
RecvChan chan RecvMessage
|
||||||
|
|
||||||
transportNet transport.Net
|
transportNet transport.Net
|
||||||
filterFn udpmux.FilterFn
|
filterFn udpmux.FilterFn
|
||||||
address wgaddr.Address
|
endpoints map[netip.Addr]net.Conn
|
||||||
mtu uint16
|
endpointsMu sync.Mutex
|
||||||
|
|
||||||
endpoints map[netip.Addr]net.Conn
|
|
||||||
endpointsMu sync.Mutex
|
|
||||||
recvChan chan recvMessage
|
|
||||||
// every time when Close() is called (i.e. BindUpdate()) we need to close exit from the receiveRelayed and create a
|
// every time when Close() is called (i.e. BindUpdate()) we need to close exit from the receiveRelayed and create a
|
||||||
// new closed channel. With the closedChanMu we can safely close the channel and create a new one
|
// new closed channel. With the closedChanMu we can safely close the channel and create a new one
|
||||||
closedChan chan struct{}
|
closedChan chan struct{}
|
||||||
closedChanMu sync.RWMutex // protect the closeChan recreation from reading from it.
|
closedChanMu sync.RWMutex // protect the closeChan recreation from reading from it.
|
||||||
closed bool
|
closed bool
|
||||||
activityRecorder *ActivityRecorder
|
|
||||||
|
|
||||||
muUDPMux sync.Mutex
|
muUDPMux sync.Mutex
|
||||||
udpMux *udpmux.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
|
address wgaddr.Address
|
||||||
|
mtu uint16
|
||||||
|
activityRecorder *ActivityRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewICEBind(transportNet transport.Net, filterFn udpmux.FilterFn, address wgaddr.Address, mtu uint16) *ICEBind {
|
func NewICEBind(transportNet transport.Net, filterFn udpmux.FilterFn, address wgaddr.Address, mtu uint16) *ICEBind {
|
||||||
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
b, _ := wgConn.NewStdNetBind().(*wgConn.StdNetBind)
|
||||||
ib := &ICEBind{
|
ib := &ICEBind{
|
||||||
StdNetBind: b,
|
StdNetBind: b,
|
||||||
|
RecvChan: make(chan RecvMessage, 1),
|
||||||
transportNet: transportNet,
|
transportNet: transportNet,
|
||||||
filterFn: filterFn,
|
filterFn: filterFn,
|
||||||
address: address,
|
|
||||||
mtu: mtu,
|
|
||||||
endpoints: make(map[netip.Addr]net.Conn),
|
endpoints: make(map[netip.Addr]net.Conn),
|
||||||
recvChan: make(chan recvMessage, 1),
|
|
||||||
closedChan: make(chan struct{}),
|
closedChan: make(chan struct{}),
|
||||||
closed: true,
|
closed: true,
|
||||||
|
mtu: mtu,
|
||||||
|
address: address,
|
||||||
activityRecorder: NewActivityRecorder(),
|
activityRecorder: NewActivityRecorder(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +83,10 @@ func NewICEBind(transportNet transport.Net, filterFn udpmux.FilterFn, address wg
|
|||||||
return ib
|
return ib
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ICEBind) MTU() uint16 {
|
||||||
|
return s.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ICEBind) Open(uport uint16) ([]wgConn.ReceiveFunc, uint16, error) {
|
func (s *ICEBind) Open(uport uint16) ([]wgConn.ReceiveFunc, uint16, error) {
|
||||||
s.closed = false
|
s.closed = false
|
||||||
s.closedChanMu.Lock()
|
s.closedChanMu.Lock()
|
||||||
@@ -134,16 +139,6 @@ func (b *ICEBind) RemoveEndpoint(fakeIP netip.Addr) {
|
|||||||
delete(b.endpoints, fakeIP)
|
delete(b.endpoints, fakeIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *ICEBind) ReceiveFromEndpoint(ctx context.Context, ep *Endpoint, buf []byte) {
|
|
||||||
select {
|
|
||||||
case <-b.closedChan:
|
|
||||||
return
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case b.recvChan <- recvMessage{ep, buf}:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *ICEBind) Send(bufs [][]byte, ep wgConn.Endpoint) error {
|
func (b *ICEBind) Send(bufs [][]byte, ep wgConn.Endpoint) error {
|
||||||
b.endpointsMu.Lock()
|
b.endpointsMu.Lock()
|
||||||
conn, ok := b.endpoints[ep.DstIP()]
|
conn, ok := b.endpoints[ep.DstIP()]
|
||||||
@@ -276,7 +271,7 @@ func (c *ICEBind) receiveRelayed(buffs [][]byte, sizes []int, eps []wgConn.Endpo
|
|||||||
select {
|
select {
|
||||||
case <-c.closedChan:
|
case <-c.closedChan:
|
||||||
return 0, net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
case msg, ok := <-c.recvChan:
|
case msg, ok := <-c.RecvChan:
|
||||||
if !ok {
|
if !ok {
|
||||||
return 0, net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
package bind
|
|
||||||
|
|
||||||
type recvMessage struct {
|
|
||||||
Endpoint *Endpoint
|
|
||||||
Buffer []byte
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
package bind
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.zx2c4.com/wireguard/conn"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/udpmux"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RelayBindJS is a conn.Bind implementation for WebAssembly environments.
|
|
||||||
// Do not limit to build only js, because we want to be able to run tests
|
|
||||||
type RelayBindJS struct {
|
|
||||||
*conn.StdNetBind
|
|
||||||
|
|
||||||
recvChan chan recvMessage
|
|
||||||
endpoints map[netip.Addr]net.Conn
|
|
||||||
endpointsMu sync.Mutex
|
|
||||||
activityRecorder *ActivityRecorder
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRelayBindJS() *RelayBindJS {
|
|
||||||
return &RelayBindJS{
|
|
||||||
recvChan: make(chan recvMessage, 100),
|
|
||||||
endpoints: make(map[netip.Addr]net.Conn),
|
|
||||||
activityRecorder: NewActivityRecorder(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open creates a receive function for handling relay packets in WASM.
|
|
||||||
func (s *RelayBindJS) Open(uport uint16) ([]conn.ReceiveFunc, uint16, error) {
|
|
||||||
log.Debugf("Open: creating receive function for port %d", uport)
|
|
||||||
|
|
||||||
s.ctx, s.cancel = context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
receiveFn := func(bufs [][]byte, sizes []int, eps []conn.Endpoint) (int, error) {
|
|
||||||
select {
|
|
||||||
case <-s.ctx.Done():
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
case msg, ok := <-s.recvChan:
|
|
||||||
if !ok {
|
|
||||||
return 0, net.ErrClosed
|
|
||||||
}
|
|
||||||
copy(bufs[0], msg.Buffer)
|
|
||||||
sizes[0] = len(msg.Buffer)
|
|
||||||
eps[0] = conn.Endpoint(msg.Endpoint)
|
|
||||||
return 1, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Open: receive function created, returning port %d", uport)
|
|
||||||
return []conn.ReceiveFunc{receiveFn}, uport, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RelayBindJS) Close() error {
|
|
||||||
if s.cancel == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
log.Debugf("close RelayBindJS")
|
|
||||||
s.cancel()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RelayBindJS) ReceiveFromEndpoint(ctx context.Context, ep *Endpoint, buf []byte) {
|
|
||||||
select {
|
|
||||||
case <-s.ctx.Done():
|
|
||||||
return
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case s.recvChan <- recvMessage{ep, buf}:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send forwards packets through the relay connection for WASM.
|
|
||||||
func (s *RelayBindJS) Send(bufs [][]byte, ep conn.Endpoint) error {
|
|
||||||
if ep == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fakeIP := ep.DstIP()
|
|
||||||
|
|
||||||
s.endpointsMu.Lock()
|
|
||||||
relayConn, ok := s.endpoints[fakeIP]
|
|
||||||
s.endpointsMu.Unlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, buf := range bufs {
|
|
||||||
if _, err := relayConn.Write(buf); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *RelayBindJS) SetEndpoint(fakeIP netip.Addr, conn net.Conn) {
|
|
||||||
b.endpointsMu.Lock()
|
|
||||||
b.endpoints[fakeIP] = conn
|
|
||||||
b.endpointsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RelayBindJS) RemoveEndpoint(fakeIP netip.Addr) {
|
|
||||||
s.endpointsMu.Lock()
|
|
||||||
defer s.endpointsMu.Unlock()
|
|
||||||
|
|
||||||
delete(s.endpoints, fakeIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetICEMux returns the ICE UDPMux that was created and used by ICEBind
|
|
||||||
func (s *RelayBindJS) GetICEMux() (*udpmux.UniversalUDPMuxDefault, error) {
|
|
||||||
return nil, ErrUDPMUXNotSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *RelayBindJS) ActivityRecorder() *ActivityRecorder {
|
|
||||||
return s.activityRecorder
|
|
||||||
}
|
|
||||||
@@ -73,44 +73,6 @@ func (c *KernelConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *KernelConfigurer) RemoveEndpointAddress(peerKey string) error {
|
|
||||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the existing peer to preserve its allowed IPs
|
|
||||||
existingPeer, err := c.getPeer(c.deviceName, peerKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get peer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
removePeerCfg := wgtypes.PeerConfig{
|
|
||||||
PublicKey: peerKeyParsed,
|
|
||||||
Remove: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.configure(wgtypes.Config{Peers: []wgtypes.PeerConfig{removePeerCfg}}); err != nil {
|
|
||||||
return fmt.Errorf(`error removing peer %s from interface %s: %w`, peerKey, c.deviceName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
//Re-add the peer without the endpoint but same AllowedIPs
|
|
||||||
reAddPeerCfg := wgtypes.PeerConfig{
|
|
||||||
PublicKey: peerKeyParsed,
|
|
||||||
AllowedIPs: existingPeer.AllowedIPs,
|
|
||||||
ReplaceAllowedIPs: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.configure(wgtypes.Config{Peers: []wgtypes.PeerConfig{reAddPeerCfg}}); err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
`error re-adding peer %s to interface %s with allowed IPs %v: %w`,
|
|
||||||
peerKey, c.deviceName, existingPeer.AllowedIPs, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *KernelConfigurer) RemovePeer(peerKey string) error {
|
func (c *KernelConfigurer) RemovePeer(peerKey string) error {
|
||||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux || windows || freebsd || js || wasip1
|
//go:build linux || windows || freebsd
|
||||||
|
|
||||||
package configurer
|
package configurer
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows && !js
|
//go:build !windows
|
||||||
|
|
||||||
package configurer
|
package configurer
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package configurer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
type noopListener struct{}
|
|
||||||
|
|
||||||
func (n *noopListener) Accept() (net.Conn, error) {
|
|
||||||
return nil, net.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopListener) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *noopListener) Addr() net.Addr {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openUAPI(deviceName string) (net.Listener, error) {
|
|
||||||
return &noopListener{}, nil
|
|
||||||
}
|
|
||||||
@@ -17,8 +17,8 @@ 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"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
|
||||||
"github.com/netbirdio/netbird/monotime"
|
"github.com/netbirdio/netbird/monotime"
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -106,67 +106,6 @@ func (c *WGUSPConfigurer) UpdatePeer(peerKey string, allowedIps []netip.Prefix,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *WGUSPConfigurer) RemoveEndpointAddress(peerKey string) error {
|
|
||||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parse peer key: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcStr, err := c.device.IpcGet()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get IPC config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse current status to get allowed IPs for the peer
|
|
||||||
stats, err := parseStatus(c.deviceName, ipcStr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parse IPC config: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowedIPs []net.IPNet
|
|
||||||
found := false
|
|
||||||
for _, peer := range stats.Peers {
|
|
||||||
if peer.PublicKey == peerKey {
|
|
||||||
allowedIPs = peer.AllowedIPs
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("peer %s not found", peerKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove the peer from the WireGuard configuration
|
|
||||||
peer := wgtypes.PeerConfig{
|
|
||||||
PublicKey: peerKeyParsed,
|
|
||||||
Remove: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
config := wgtypes.Config{
|
|
||||||
Peers: []wgtypes.PeerConfig{peer},
|
|
||||||
}
|
|
||||||
if ipcErr := c.device.IpcSet(toWgUserspaceString(config)); ipcErr != nil {
|
|
||||||
return fmt.Errorf("failed to remove peer: %s", ipcErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the peer config
|
|
||||||
peer = wgtypes.PeerConfig{
|
|
||||||
PublicKey: peerKeyParsed,
|
|
||||||
ReplaceAllowedIPs: true,
|
|
||||||
AllowedIPs: allowedIPs,
|
|
||||||
}
|
|
||||||
|
|
||||||
config = wgtypes.Config{
|
|
||||||
Peers: []wgtypes.PeerConfig{peer},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.device.IpcSet(toWgUserspaceString(config)); err != nil {
|
|
||||||
return fmt.Errorf("remove endpoint address: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *WGUSPConfigurer) RemovePeer(peerKey string) error {
|
func (c *WGUSPConfigurer) RemovePeer(peerKey string) error {
|
||||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -470,7 +409,7 @@ func toBytes(s string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFwmark() int {
|
func getFwmark() int {
|
||||||
if nbnet.AdvancedRouting() && runtime.GOOS == "linux" {
|
if nbnet.AdvancedRouting() {
|
||||||
return nbnet.ControlPlaneMark
|
return nbnet.ControlPlaneMark
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -23,5 +23,4 @@ type WGTunDevice interface {
|
|||||||
FilteredDevice() *device.FilteredDevice
|
FilteredDevice() *device.FilteredDevice
|
||||||
Device() *wgdevice.Device
|
Device() *wgdevice.Device
|
||||||
GetNet() *netstack.Net
|
GetNet() *netstack.Net
|
||||||
GetICEBind() device.EndpointManager
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -20,12 +19,11 @@ import (
|
|||||||
|
|
||||||
// WGTunDevice ignore the WGTunDevice interface on Android because the creation of the tun device is different on this platform
|
// WGTunDevice ignore the WGTunDevice interface on Android because the creation of the tun device is different on this platform
|
||||||
type WGTunDevice struct {
|
type WGTunDevice struct {
|
||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
port int
|
port int
|
||||||
key string
|
key string
|
||||||
mtu uint16
|
mtu uint16
|
||||||
iceBind *bind.ICEBind
|
iceBind *bind.ICEBind
|
||||||
// todo: review if we can eliminate the TunAdapter
|
|
||||||
tunAdapter TunAdapter
|
tunAdapter TunAdapter
|
||||||
disableDNS bool
|
disableDNS bool
|
||||||
|
|
||||||
@@ -34,19 +32,17 @@ type WGTunDevice struct {
|
|||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *udpmux.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
renewableTun *RenewableTUN
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTunDevice(address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind, tunAdapter TunAdapter, disableDNS bool) *WGTunDevice {
|
func NewTunDevice(address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind, tunAdapter TunAdapter, disableDNS bool) *WGTunDevice {
|
||||||
return &WGTunDevice{
|
return &WGTunDevice{
|
||||||
address: address,
|
address: address,
|
||||||
port: port,
|
port: port,
|
||||||
key: key,
|
key: key,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
iceBind: iceBind,
|
iceBind: iceBind,
|
||||||
tunAdapter: tunAdapter,
|
tunAdapter: tunAdapter,
|
||||||
disableDNS: disableDNS,
|
disableDNS: disableDNS,
|
||||||
renewableTun: NewRenewableTUN(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,17 +65,14 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
unmonitoredTUN, name, err := tun.CreateUnmonitoredTUNFromFD(fd)
|
tunDevice, name, err := tun.CreateUnmonitoredTUNFromFD(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = unix.Close(fd)
|
_ = unix.Close(fd)
|
||||||
log.Errorf("failed to create Android interface: %s", err)
|
log.Errorf("failed to create Android interface: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.renewableTun.AddDevice(unmonitoredTUN)
|
|
||||||
|
|
||||||
t.name = name
|
t.name = name
|
||||||
t.filteredDevice = newDeviceFilter(t.renewableTun)
|
t.filteredDevice = newDeviceFilter(tunDevice)
|
||||||
|
|
||||||
log.Debugf("attaching to interface %v", name)
|
log.Debugf("attaching to interface %v", name)
|
||||||
t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] "))
|
t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] "))
|
||||||
@@ -111,23 +104,6 @@ func (t *WGTunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
|||||||
return udpMux, nil
|
return udpMux, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WGTunDevice) RenewTun(fd int) error {
|
|
||||||
if t.device == nil {
|
|
||||||
return fmt.Errorf("device not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
unmonitoredTUN, _, err := tun.CreateUnmonitoredTUNFromFD(fd)
|
|
||||||
if err != nil {
|
|
||||||
_ = unix.Close(fd)
|
|
||||||
log.Errorf("failed to renew Android interface: %s", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
t.renewableTun.AddDevice(unmonitoredTUN)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *WGTunDevice) UpdateAddr(addr wgaddr.Address) error {
|
func (t *WGTunDevice) UpdateAddr(addr wgaddr.Address) error {
|
||||||
// todo implement
|
// todo implement
|
||||||
return nil
|
return nil
|
||||||
@@ -174,11 +150,6 @@ func (t *WGTunDevice) GetNet() *netstack.Net {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetICEBind returns the ICEBind instance
|
|
||||||
func (t *WGTunDevice) GetICEBind() EndpointManager {
|
|
||||||
return t.iceBind
|
|
||||||
}
|
|
||||||
|
|
||||||
func routesToString(routes []string) string {
|
func routesToString(routes []string) string {
|
||||||
return strings.Join(routes, ";")
|
return strings.Join(routes, ";")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,8 +154,3 @@ func (t *TunDevice) assignAddr() error {
|
|||||||
func (t *TunDevice) GetNet() *netstack.Net {
|
func (t *TunDevice) GetNet() *netstack.Net {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetICEBind returns the ICEBind instance
|
|
||||||
func (t *TunDevice) GetICEBind() EndpointManager {
|
|
||||||
return t.iceBind
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -144,8 +144,3 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice {
|
|||||||
func (t *TunDevice) GetNet() *netstack.Net {
|
func (t *TunDevice) GetNet() *netstack.Net {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetICEBind returns the ICEBind instance
|
|
||||||
func (t *TunDevice) GetICEBind() EndpointManager {
|
|
||||||
return t.iceBind
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
"github.com/netbirdio/netbird/client/iface/udpmux"
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
|
||||||
"github.com/netbirdio/netbird/sharedsock"
|
"github.com/netbirdio/netbird/sharedsock"
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TunKernelDevice struct {
|
type TunKernelDevice struct {
|
||||||
@@ -101,8 +101,13 @@ func (t *TunKernelDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var udpConn net.PacketConn = rawSock
|
||||||
|
if !nbnet.AdvancedRouting() {
|
||||||
|
udpConn = nbnet.WrapPacketConn(rawSock)
|
||||||
|
}
|
||||||
|
|
||||||
bindParams := udpmux.UniversalUDPMuxParams{
|
bindParams := udpmux.UniversalUDPMuxParams{
|
||||||
UDPConn: nbnet.WrapPacketConn(rawSock),
|
UDPConn: udpConn,
|
||||||
Net: t.transportNet,
|
Net: t.transportNet,
|
||||||
FilterFn: t.filterFn,
|
FilterFn: t.filterFn,
|
||||||
WGAddress: t.address,
|
WGAddress: t.address,
|
||||||
@@ -179,8 +184,3 @@ func (t *TunKernelDevice) assignAddr() error {
|
|||||||
func (t *TunKernelDevice) GetNet() *netstack.Net {
|
func (t *TunKernelDevice) GetNet() *netstack.Net {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetICEBind returns nil for kernel mode devices
|
|
||||||
func (t *TunKernelDevice) GetICEBind() EndpointManager {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/conn"
|
|
||||||
"golang.zx2c4.com/wireguard/device"
|
"golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
@@ -14,16 +12,9 @@ import (
|
|||||||
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
"github.com/netbirdio/netbird/client/iface/udpmux"
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Bind interface {
|
|
||||||
conn.Bind
|
|
||||||
GetICEMux() (*udpmux.UniversalUDPMuxDefault, error)
|
|
||||||
ActivityRecorder() *bind.ActivityRecorder
|
|
||||||
EndpointManager
|
|
||||||
}
|
|
||||||
|
|
||||||
type TunNetstackDevice struct {
|
type TunNetstackDevice struct {
|
||||||
name string
|
name string
|
||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
@@ -31,7 +22,7 @@ type TunNetstackDevice struct {
|
|||||||
key string
|
key string
|
||||||
mtu uint16
|
mtu uint16
|
||||||
listenAddress string
|
listenAddress string
|
||||||
bind Bind
|
iceBind *bind.ICEBind
|
||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
@@ -42,7 +33,7 @@ type TunNetstackDevice struct {
|
|||||||
net *netstack.Net
|
net *netstack.Net
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetstackDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, bind Bind, listenAddress string) *TunNetstackDevice {
|
func NewNetstackDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, iceBind *bind.ICEBind, listenAddress string) *TunNetstackDevice {
|
||||||
return &TunNetstackDevice{
|
return &TunNetstackDevice{
|
||||||
name: name,
|
name: name,
|
||||||
address: address,
|
address: address,
|
||||||
@@ -50,7 +41,7 @@ func NewNetstackDevice(name string, address wgaddr.Address, wgPort int, key stri
|
|||||||
key: key,
|
key: key,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
listenAddress: listenAddress,
|
listenAddress: listenAddress,
|
||||||
bind: bind,
|
iceBind: iceBind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,11 +66,11 @@ func (t *TunNetstackDevice) create() (WGConfigurer, error) {
|
|||||||
|
|
||||||
t.device = device.NewDevice(
|
t.device = device.NewDevice(
|
||||||
t.filteredDevice,
|
t.filteredDevice,
|
||||||
t.bind,
|
t.iceBind,
|
||||||
device.NewLogger(wgLogLevel(), "[netbird] "),
|
device.NewLogger(wgLogLevel(), "[netbird] "),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.bind.ActivityRecorder())
|
t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder())
|
||||||
err = t.configurer.ConfigureInterface(t.key, t.port)
|
err = t.configurer.ConfigureInterface(t.key, t.port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tunIface.Close()
|
_ = tunIface.Close()
|
||||||
@@ -100,15 +91,11 @@ func (t *TunNetstackDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
udpMux, err := t.bind.GetICEMux()
|
udpMux, err := t.iceBind.GetICEMux()
|
||||||
if err != nil && !errors.Is(err, bind.ErrUDPMUXNotSupported) {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
t.udpMux = udpMux
|
||||||
if udpMux != nil {
|
|
||||||
t.udpMux = udpMux
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("netstack device is ready to use")
|
log.Debugf("netstack device is ready to use")
|
||||||
return udpMux, nil
|
return udpMux, nil
|
||||||
}
|
}
|
||||||
@@ -156,8 +143,3 @@ func (t *TunNetstackDevice) Device() *device.Device {
|
|||||||
func (t *TunNetstackDevice) GetNet() *netstack.Net {
|
func (t *TunNetstackDevice) GetNet() *netstack.Net {
|
||||||
return t.net
|
return t.net
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetICEBind returns the bind instance
|
|
||||||
func (t *TunNetstackDevice) GetICEBind() EndpointManager {
|
|
||||||
return t.bind
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,13 +2,6 @@
|
|||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func (t *TunNetstackDevice) Create(routes []string, dns string, searchDomains []string) (WGConfigurer, error) {
|
func (t *TunNetstackDevice) Create(routes []string, dns string, searchDomains []string) (WGConfigurer, error) {
|
||||||
return t.create()
|
return t.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunNetstackDevice) RenewTun(fd int) error {
|
|
||||||
// Doesn't make sense in Android for Netstack.
|
|
||||||
return fmt.Errorf("this function has not been implemented in Netstack for Android")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewNetstackDevice(t *testing.T) {
|
|
||||||
privateKey, _ := wgtypes.GeneratePrivateKey()
|
|
||||||
wgAddress, _ := wgaddr.ParseWGAddress("1.2.3.4/24")
|
|
||||||
|
|
||||||
relayBind := bind.NewRelayBindJS()
|
|
||||||
nsTun := NewNetstackDevice("wtx", wgAddress, 1234, privateKey.String(), 1500, relayBind, netstack.ListenAddr())
|
|
||||||
|
|
||||||
cfgr, err := nsTun.Create()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to create netstack device: %v", err)
|
|
||||||
}
|
|
||||||
if cfgr == nil {
|
|
||||||
t.Fatal("expected non-nil configurer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -146,8 +146,3 @@ func (t *USPDevice) assignAddr() error {
|
|||||||
func (t *USPDevice) GetNet() *netstack.Net {
|
func (t *USPDevice) GetNet() *netstack.Net {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetICEBind returns the ICEBind instance
|
|
||||||
func (t *USPDevice) GetICEBind() EndpointManager {
|
|
||||||
return t.iceBind
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -185,8 +185,3 @@ func (t *TunDevice) assignAddr() error {
|
|||||||
func (t *TunDevice) GetNet() *netstack.Net {
|
func (t *TunDevice) GetNet() *netstack.Net {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetICEBind returns the ICEBind instance
|
|
||||||
func (t *TunDevice) GetICEBind() EndpointManager {
|
|
||||||
return t.iceBind
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EndpointManager manages fake IP to connection mappings for userspace bind implementations.
|
|
||||||
// Implemented by bind.ICEBind and bind.RelayBindJS.
|
|
||||||
type EndpointManager interface {
|
|
||||||
SetEndpoint(fakeIP netip.Addr, conn net.Conn)
|
|
||||||
RemoveEndpoint(fakeIP netip.Addr)
|
|
||||||
}
|
|
||||||
@@ -21,5 +21,4 @@ type WGConfigurer interface {
|
|||||||
GetStats() (map[string]configurer.WGStats, error)
|
GetStats() (map[string]configurer.WGStats, error)
|
||||||
FullStats() (*configurer.Stats, error)
|
FullStats() (*configurer.Stats, error)
|
||||||
LastActivities() map[string]monotime.Time
|
LastActivities() map[string]monotime.Time
|
||||||
RemoveEndpointAddress(peerKey string) error
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
//go:build android
|
|
||||||
|
|
||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.zx2c4.com/wireguard/tun"
|
|
||||||
)
|
|
||||||
|
|
||||||
// closeAwareDevice wraps a tun.Device along with a flag
|
|
||||||
// indicating whether its Close method was called.
|
|
||||||
//
|
|
||||||
// It also redirects tun.Device's Events() to a separate goroutine
|
|
||||||
// and closes it when Close is called.
|
|
||||||
//
|
|
||||||
// The WaitGroup and CloseOnce fields are used to ensure that the
|
|
||||||
// goroutine is awaited and closed only once.
|
|
||||||
type closeAwareDevice struct {
|
|
||||||
isClosed atomic.Bool
|
|
||||||
tun.Device
|
|
||||||
closeEventCh chan struct{}
|
|
||||||
wg sync.WaitGroup
|
|
||||||
closeOnce sync.Once
|
|
||||||
}
|
|
||||||
|
|
||||||
func newClosableDevice(tunDevice tun.Device) *closeAwareDevice {
|
|
||||||
return &closeAwareDevice{
|
|
||||||
Device: tunDevice,
|
|
||||||
isClosed: atomic.Bool{},
|
|
||||||
closeEventCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// redirectEvents redirects the Events() method of the underlying tun.Device
|
|
||||||
// to the given channel (RenewableTUN's events channel).
|
|
||||||
func (c *closeAwareDevice) redirectEvents(out chan tun.Event) {
|
|
||||||
c.wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer c.wg.Done()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case ev, ok := <-c.Device.Events():
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ev == tun.EventDown {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case out <- ev:
|
|
||||||
case <-c.closeEventCh:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case <-c.closeEventCh:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close calls the underlying Device's Close method
|
|
||||||
// after setting isClosed to true.
|
|
||||||
func (c *closeAwareDevice) Close() (err error) {
|
|
||||||
c.closeOnce.Do(func() {
|
|
||||||
c.isClosed.Store(true)
|
|
||||||
close(c.closeEventCh)
|
|
||||||
err = c.Device.Close()
|
|
||||||
c.wg.Wait()
|
|
||||||
})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *closeAwareDevice) IsClosed() bool {
|
|
||||||
return c.isClosed.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
type RenewableTUN struct {
|
|
||||||
devices []*closeAwareDevice
|
|
||||||
mu sync.Mutex
|
|
||||||
cond *sync.Cond
|
|
||||||
events chan tun.Event
|
|
||||||
closed atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRenewableTUN() *RenewableTUN {
|
|
||||||
r := &RenewableTUN{
|
|
||||||
devices: make([]*closeAwareDevice, 0),
|
|
||||||
mu: sync.Mutex{},
|
|
||||||
events: make(chan tun.Event, 16),
|
|
||||||
}
|
|
||||||
r.cond = sync.NewCond(&r.mu)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) File() *os.File {
|
|
||||||
for {
|
|
||||||
dev := r.peekLast()
|
|
||||||
if dev == nil {
|
|
||||||
if !r.waitForDevice() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
file := dev.File()
|
|
||||||
|
|
||||||
if dev.IsClosed() {
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read reads from an underlying tun.Device kept in the r.devices slice.
|
|
||||||
// If no device is available, it waits for one to be added via AddDevice().
|
|
||||||
//
|
|
||||||
// On error, it retries reading from the newest device instead of returning the error
|
|
||||||
// if the device is closed; if not, it propagates the error.
|
|
||||||
func (r *RenewableTUN) Read(bufs [][]byte, sizes []int, offset int) (n int, err error) {
|
|
||||||
for {
|
|
||||||
dev := r.peekLast()
|
|
||||||
if dev == nil {
|
|
||||||
// wait until AddDevice() signals a new device via cond.Broadcast()
|
|
||||||
if !r.waitForDevice() { // returns false if the renewable TUN itself is closed
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = dev.Read(bufs, sizes, offset)
|
|
||||||
if err == nil {
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// swap in progress; retry on the newest instead of returning the error
|
|
||||||
if dev.IsClosed() {
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return n, err // propagate non-swap error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write writes to underlying tun.Device kept in the r.devices slice.
|
|
||||||
// If no device is available, it waits for one to be added via AddDevice().
|
|
||||||
//
|
|
||||||
// On error, it retries writing to the newest device instead of returning the error
|
|
||||||
// if the device is closed; if not, it propagates the error.
|
|
||||||
func (r *RenewableTUN) Write(bufs [][]byte, offset int) (int, error) {
|
|
||||||
for {
|
|
||||||
dev := r.peekLast()
|
|
||||||
if dev == nil {
|
|
||||||
if !r.waitForDevice() {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := dev.Write(bufs, offset)
|
|
||||||
if err == nil {
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if dev.IsClosed() {
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) MTU() (int, error) {
|
|
||||||
for {
|
|
||||||
dev := r.peekLast()
|
|
||||||
if dev == nil {
|
|
||||||
if !r.waitForDevice() {
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
mtu, err := dev.MTU()
|
|
||||||
if err == nil {
|
|
||||||
return mtu, nil
|
|
||||||
}
|
|
||||||
if dev.IsClosed() {
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) Name() (string, error) {
|
|
||||||
for {
|
|
||||||
dev := r.peekLast()
|
|
||||||
if dev == nil {
|
|
||||||
if !r.waitForDevice() {
|
|
||||||
return "", io.EOF
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name, err := dev.Name()
|
|
||||||
if err == nil {
|
|
||||||
return name, nil
|
|
||||||
}
|
|
||||||
if dev.IsClosed() {
|
|
||||||
time.Sleep(1 * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events returns a channel that is fed events from the underlying tun.Device's events channel
|
|
||||||
// once it is added.
|
|
||||||
func (r *RenewableTUN) Events() <-chan tun.Event {
|
|
||||||
return r.events
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) Close() error {
|
|
||||||
// Attempts to set the RenewableTUN closed flag to true.
|
|
||||||
// If it's already true, returns immediately.
|
|
||||||
if !r.closed.CompareAndSwap(false, true) {
|
|
||||||
return nil // already closed: idempotent
|
|
||||||
}
|
|
||||||
r.mu.Lock()
|
|
||||||
devices := r.devices
|
|
||||||
r.devices = nil
|
|
||||||
r.cond.Broadcast()
|
|
||||||
r.mu.Unlock()
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
|
|
||||||
log.Debugf("closing %d devices", len(devices))
|
|
||||||
for _, device := range devices {
|
|
||||||
if err := device.Close(); err != nil {
|
|
||||||
log.Debugf("error closing a device: %v", err)
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close(r.events)
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) BatchSize() int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) AddDevice(device tun.Device) {
|
|
||||||
r.mu.Lock()
|
|
||||||
if r.closed.Load() {
|
|
||||||
r.mu.Unlock()
|
|
||||||
_ = device.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var toClose *closeAwareDevice
|
|
||||||
if len(r.devices) > 0 {
|
|
||||||
toClose = r.devices[len(r.devices)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
cad := newClosableDevice(device)
|
|
||||||
cad.redirectEvents(r.events)
|
|
||||||
|
|
||||||
r.devices = []*closeAwareDevice{cad}
|
|
||||||
r.cond.Broadcast()
|
|
||||||
|
|
||||||
r.mu.Unlock()
|
|
||||||
|
|
||||||
if toClose != nil {
|
|
||||||
if err := toClose.Close(); err != nil {
|
|
||||||
log.Debugf("error closing last device: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) waitForDevice() bool {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
for len(r.devices) == 0 && !r.closed.Load() {
|
|
||||||
r.cond.Wait()
|
|
||||||
}
|
|
||||||
return !r.closed.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RenewableTUN) peekLast() *closeAwareDevice {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
|
|
||||||
if len(r.devices) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.devices[len(r.devices)-1]
|
|
||||||
}
|
|
||||||
@@ -21,6 +21,4 @@ type WGTunDevice interface {
|
|||||||
FilteredDevice() *device.FilteredDevice
|
FilteredDevice() *device.FilteredDevice
|
||||||
Device() *wgdevice.Device
|
Device() *wgdevice.Device
|
||||||
GetNet() *netstack.Net
|
GetNet() *netstack.Net
|
||||||
RenewTun(fd int) error
|
|
||||||
GetICEBind() device.EndpointManager
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,17 +80,6 @@ func (w *WGIface) GetProxy() wgproxy.Proxy {
|
|||||||
return w.wgProxyFactory.GetProxy()
|
return w.wgProxyFactory.GetProxy()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBind returns the EndpointManager userspace bind mode.
|
|
||||||
func (w *WGIface) GetBind() device.EndpointManager {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
|
|
||||||
if w.tun == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return w.tun.GetICEBind()
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUserspaceBind indicates whether this interfaces is userspace with bind.ICEBind
|
// IsUserspaceBind indicates whether this interfaces is userspace with bind.ICEBind
|
||||||
func (w *WGIface) IsUserspaceBind() bool {
|
func (w *WGIface) IsUserspaceBind() bool {
|
||||||
return w.userspaceBind
|
return w.userspaceBind
|
||||||
@@ -159,17 +148,6 @@ func (w *WGIface) UpdatePeer(peerKey string, allowedIps []netip.Prefix, keepAliv
|
|||||||
return w.configurer.UpdatePeer(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
|
return w.configurer.UpdatePeer(peerKey, allowedIps, keepAlive, endpoint, preSharedKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WGIface) RemoveEndpointAddress(peerKey string) error {
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
if w.configurer == nil {
|
|
||||||
return ErrIfaceNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Removing endpoint address: %s", peerKey)
|
|
||||||
return w.configurer.RemoveEndpointAddress(peerKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemovePeer removes a Wireguard Peer from the interface iface
|
// RemovePeer removes a Wireguard Peer from the interface iface
|
||||||
func (w *WGIface) RemovePeer(peerKey string) error {
|
func (w *WGIface) RemovePeer(peerKey string) error {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
|
|||||||
@@ -24,7 +24,3 @@ func (w *WGIface) Create() error {
|
|||||||
func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
|
func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
|
||||||
return fmt.Errorf("this function has not implemented on non mobile")
|
return fmt.Errorf("this function has not implemented on non mobile")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WGIface) RenewTun(fd int) error {
|
|
||||||
return fmt.Errorf("this function has not been implemented on non-android")
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user