mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-19 16:56:39 +00:00
Compare commits
184 Commits
handle-exi
...
refactor/n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
575b176371 | ||
|
|
b96dfb834e | ||
|
|
a654c286bf | ||
|
|
20c65cffc2 | ||
|
|
95cf5ab673 | ||
|
|
ed0c5a470a | ||
|
|
3a11175d34 | ||
|
|
d82176e9ea | ||
|
|
56ebd004d9 | ||
|
|
1ee575befe | ||
|
|
d3a34adcc9 | ||
|
|
bbb0910a2c | ||
|
|
ec035e5709 | ||
|
|
d7321c130b | ||
|
|
404cab90ba | ||
|
|
4545ab9a52 | ||
|
|
7f08983207 | ||
|
|
eddea14521 | ||
|
|
b9ef214ea5 | ||
|
|
709e24eb6f | ||
|
|
6654e2dbf7 | ||
|
|
d80d47a469 | ||
|
|
96f71ff1e1 | ||
|
|
2fe2af38d2 | ||
|
|
cd9a867ad0 | ||
|
|
0f9bfeff7c | ||
|
|
d0f8868e4d | ||
|
|
31e3928e08 | ||
|
|
1793ad8ff0 | ||
|
|
f5301230bf | ||
|
|
7e51803a58 | ||
|
|
4cab69a76c | ||
|
|
dace4ff30c | ||
|
|
429d7d6585 | ||
|
|
3cdb10cde7 | ||
|
|
069586b77f | ||
|
|
dac6fb16ba | ||
|
|
cd7dce7678 | ||
|
|
9bdabfbb2c | ||
|
|
2760c78bc2 | ||
|
|
af95aabb03 | ||
|
|
3abae0bd17 | ||
|
|
8252ff41db | ||
|
|
277aa2b7cc | ||
|
|
bb37dc89ce | ||
|
|
2882638c7f | ||
|
|
000e99e7f3 | ||
|
|
0d2e67983a | ||
|
|
5151f19d29 | ||
|
|
bedd3cabc9 | ||
|
|
9c7c15ba8b | ||
|
|
b0dcc9ff7b | ||
|
|
d35a845dbd | ||
|
|
11e2573747 | ||
|
|
4e03f708a4 | ||
|
|
6ac9e58911 | ||
|
|
c115434d53 | ||
|
|
3984f0ee08 | ||
|
|
2dc5e7eb7a | ||
|
|
75dc7f6517 | ||
|
|
409e2013cc | ||
|
|
654aa9581d | ||
|
|
f13c6ba1b8 | ||
|
|
fc2f23bd22 | ||
|
|
26f5aee4b9 | ||
|
|
8d47175cdb | ||
|
|
9021bb512b | ||
|
|
768332820e | ||
|
|
229c65ffa1 | ||
|
|
4d33567888 | ||
|
|
274711a37e | ||
|
|
53e24ae7f7 | ||
|
|
fbc02343e9 | ||
|
|
ffed4b38ef | ||
|
|
88467883fc | ||
|
|
954f40991f | ||
|
|
34341d95a9 | ||
|
|
e926ca34b5 | ||
|
|
5d1c61369d | ||
|
|
fd9e21a5f3 | ||
|
|
e7b5537dcc | ||
|
|
95794f53ce | ||
|
|
841bc7564a | ||
|
|
f20a1b3328 | ||
|
|
2ac0da6cac | ||
|
|
148b8b04b3 | ||
|
|
9a56883ffb | ||
|
|
806be13dd5 | ||
|
|
9bcd3ebed4 | ||
|
|
b85045e723 | ||
|
|
4d7e59f199 | ||
|
|
b5daec3b51 | ||
|
|
5e1a40c33f | ||
|
|
e8d301fdc9 | ||
|
|
17bab881f7 | ||
|
|
25ed58328a | ||
|
|
644ed4b934 | ||
|
|
58faa341d2 | ||
|
|
5853b5553c | ||
|
|
998fb30e1e | ||
|
|
e254b4cde5 | ||
|
|
ead1c618ba | ||
|
|
55126f990c | ||
|
|
90577682e4 | ||
|
|
dc30dcacce | ||
|
|
2c87fa6236 | ||
|
|
ec8d83ade4 | ||
|
|
90557da237 | ||
|
|
1d6209841e | ||
|
|
8f0e5708d5 | ||
|
|
5a9aa55121 | ||
|
|
6082c7cdcb | ||
|
|
3130cce72d | ||
|
|
bd23ab925e | ||
|
|
06eae13352 | ||
|
|
0c6f671a7c | ||
|
|
cf7f6c355f | ||
|
|
47e64d72db | ||
|
|
9e81e782e5 | ||
|
|
7aef0f67df | ||
|
|
dba7ef667d | ||
|
|
69d87343d2 | ||
|
|
5113c70943 | ||
|
|
ad8fcda67b | ||
|
|
d33f88df82 | ||
|
|
08fba9876b | ||
|
|
ca85aa9b8f | ||
|
|
786ca6fc79 | ||
|
|
dfebdf1444 | ||
|
|
a8dcff69c2 | ||
|
|
0ae2241573 | ||
|
|
050c05164a | ||
|
|
333908d06e | ||
|
|
bc6c5ece6e | ||
|
|
fd7b3ae21c | ||
|
|
abd7a84a46 | ||
|
|
f4b2bed1b9 | ||
|
|
2fb971e88a | ||
|
|
71e944fa57 | ||
|
|
d39fcfd62a | ||
|
|
21368b38d9 | ||
|
|
d817584f52 | ||
|
|
4d3dc3475d | ||
|
|
6fc50a438f | ||
|
|
149559a06b | ||
|
|
e14c6de203 | ||
|
|
d4c067f0af | ||
|
|
dbefa8bd9f | ||
|
|
4fd10b9447 | ||
|
|
aa595c3073 | ||
|
|
99bd34c02a | ||
|
|
7ce5507c05 | ||
|
|
0320bb7b35 | ||
|
|
f063866ce8 | ||
|
|
9f84165763 | ||
|
|
3488a516c9 | ||
|
|
5e273c121a | ||
|
|
968d95698e | ||
|
|
28bef26537 | ||
|
|
0d2845ea31 | ||
|
|
f425870c8e | ||
|
|
f9d64a06c2 | ||
|
|
86555c44f7 | ||
|
|
48792c64cd | ||
|
|
533d93eb17 | ||
|
|
9685411246 | ||
|
|
d00a226556 | ||
|
|
5d361b5421 | ||
|
|
a889c4108b | ||
|
|
12cad854b2 | ||
|
|
6a3846a8b7 | ||
|
|
7cd5dcae59 | ||
|
|
0e62325d46 | ||
|
|
b3056d0937 | ||
|
|
ab853ac2a5 | ||
|
|
e97f853909 | ||
|
|
70db8751d7 | ||
|
|
86a00ab4af | ||
|
|
3d4b502126 | ||
|
|
a4e8647aef | ||
|
|
160b811e21 | ||
|
|
5e607cf4e9 | ||
|
|
0fdb944058 | ||
|
|
ccbabd9e2a |
49
.github/workflows/docs-ack.yml
vendored
49
.github/workflows/docs-ack.yml
vendored
@@ -16,19 +16,29 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Read PR body
|
- name: Read PR body
|
||||||
id: body
|
id: body
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
BODY=$(jq -r '.pull_request.body // ""' "$GITHUB_EVENT_PATH")
|
set -euo pipefail
|
||||||
echo "body<<EOF" >> $GITHUB_OUTPUT
|
BODY_B64=$(jq -r '.pull_request.body // "" | @base64' "$GITHUB_EVENT_PATH")
|
||||||
echo "$BODY" >> $GITHUB_OUTPUT
|
{
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "body_b64=$BODY_B64"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Validate checkbox selection
|
- name: Validate checkbox selection
|
||||||
id: validate
|
id: validate
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BODY_B64: ${{ steps.body.outputs.body_b64 }}
|
||||||
run: |
|
run: |
|
||||||
body='${{ steps.body.outputs.body }}'
|
set -euo pipefail
|
||||||
|
if ! body="$(printf '%s' "$BODY_B64" | base64 -d)"; then
|
||||||
|
echo "::error::Failed to decode PR body from base64. Data may be corrupted or missing."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
added_checked=$(printf '%s' "$body" | grep -Ei '^[[:space:]]*-\s*\[x\]\s*I added/updated documentation' | wc -l | tr -d '[:space:]' || true)
|
||||||
|
noneed_checked=$(printf '%s' "$body" | grep -Ei '^[[:space:]]*-\s*\[x\]\s*Documentation is \*\*not needed\*\*' | wc -l | tr -d '[:space:]' || true)
|
||||||
|
|
||||||
added_checked=$(printf "%s" "$body" | grep -E '^- \[x\] I added/updated documentation' -i | wc -l | tr -d ' ')
|
|
||||||
noneed_checked=$(printf "%s" "$body" | grep -E '^- \[x\] Documentation is \*\*not needed\*\*' -i | wc -l | tr -d ' ')
|
|
||||||
|
|
||||||
if [ "$added_checked" -eq 1 ] && [ "$noneed_checked" -eq 1 ]; then
|
if [ "$added_checked" -eq 1 ] && [ "$noneed_checked" -eq 1 ]; then
|
||||||
echo "::error::Choose exactly one: either 'docs added' OR 'not needed'."
|
echo "::error::Choose exactly one: either 'docs added' OR 'not needed'."
|
||||||
@@ -41,30 +51,35 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$added_checked" -eq 1 ]; then
|
if [ "$added_checked" -eq 1 ]; then
|
||||||
echo "mode=added" >> $GITHUB_OUTPUT
|
echo "mode=added" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "mode=noneed" >> $GITHUB_OUTPUT
|
echo "mode=noneed" >> "$GITHUB_OUTPUT"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Extract docs PR URL (when 'docs added')
|
- name: Extract docs PR URL (when 'docs added')
|
||||||
if: steps.validate.outputs.mode == 'added'
|
if: steps.validate.outputs.mode == 'added'
|
||||||
id: extract
|
id: extract
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
BODY_B64: ${{ steps.body.outputs.body_b64 }}
|
||||||
run: |
|
run: |
|
||||||
body='${{ steps.body.outputs.body }}'
|
set -euo pipefail
|
||||||
|
body="$(printf '%s' "$BODY_B64" | base64 -d)"
|
||||||
|
|
||||||
# Strictly require HTTPS and that it's a PR in netbirdio/docs
|
# Strictly require HTTPS and that it's a PR in netbirdio/docs
|
||||||
# Examples accepted:
|
# e.g., https://github.com/netbirdio/docs/pull/1234
|
||||||
# https://github.com/netbirdio/docs/pull/1234
|
url="$(printf '%s' "$body" | grep -Eo 'https://github\.com/netbirdio/docs/pull/[0-9]+' | head -n1 || true)"
|
||||||
url=$(printf "%s" "$body" | grep -Eo 'https://github\.com/netbirdio/docs/pull/[0-9]+' | head -n1 || true)
|
|
||||||
|
|
||||||
if [ -z "$url" ]; then
|
if [ -z "${url:-}" ]; then
|
||||||
echo "::error::You checked 'docs added' but didn't include a valid HTTPS PR link to netbirdio/docs (e.g., https://github.com/netbirdio/docs/pull/1234)."
|
echo "::error::You checked 'docs added' but didn't include a valid HTTPS PR link to netbirdio/docs (e.g., https://github.com/netbirdio/docs/pull/1234)."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
pr_number=$(echo "$url" | sed -E 's#.*/pull/([0-9]+)$#\1#')
|
pr_number="$(printf '%s' "$url" | sed -E 's#.*/pull/([0-9]+)$#\1#')"
|
||||||
echo "url=$url" >> $GITHUB_OUTPUT
|
{
|
||||||
echo "pr_number=$pr_number" >> $GITHUB_OUTPUT
|
echo "url=$url"
|
||||||
|
echo "pr_number=$pr_number"
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Verify docs PR exists (and is open or merged)
|
- name: Verify docs PR exists (and is open or merged)
|
||||||
if: steps.validate.outputs.mode == 'added'
|
if: steps.validate.outputs.mode == 'added'
|
||||||
|
|||||||
3
.github/workflows/golang-test-freebsd.yml
vendored
3
.github/workflows/golang-test-freebsd.yml
vendored
@@ -25,8 +25,7 @@ jobs:
|
|||||||
release: "14.2"
|
release: "14.2"
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y curl pkgconf xorg
|
pkg install -y curl pkgconf xorg
|
||||||
LATEST_VERSION=$(curl -s https://go.dev/VERSION?m=text|head -n 1)
|
GO_TARBALL="go1.23.12.freebsd-amd64.tar.gz"
|
||||||
GO_TARBALL="$LATEST_VERSION.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"
|
||||||
|
|||||||
37
.github/workflows/golang-test-linux.yml
vendored
37
.github/workflows/golang-test-linux.yml
vendored
@@ -217,7 +217,7 @@ jobs:
|
|||||||
- arch: "386"
|
- arch: "386"
|
||||||
raceFlag: ""
|
raceFlag: ""
|
||||||
- arch: "amd64"
|
- arch: "amd64"
|
||||||
raceFlag: ""
|
raceFlag: "-race"
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
@@ -382,6 +382,32 @@ jobs:
|
|||||||
store: [ 'sqlite', 'postgres' ]
|
store: [ 'sqlite', 'postgres' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
|
- name: Create Docker network
|
||||||
|
run: docker network create promnet
|
||||||
|
|
||||||
|
- name: Start Prometheus Pushgateway
|
||||||
|
run: docker run -d --name pushgateway --network promnet -p 9091:9091 prom/pushgateway
|
||||||
|
|
||||||
|
- name: Start Prometheus (for Pushgateway forwarding)
|
||||||
|
run: |
|
||||||
|
echo '
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: "pushgateway"
|
||||||
|
static_configs:
|
||||||
|
- targets: ["pushgateway:9091"]
|
||||||
|
remote_write:
|
||||||
|
- url: ${{ secrets.GRAFANA_URL }}
|
||||||
|
basic_auth:
|
||||||
|
username: ${{ secrets.GRAFANA_USER }}
|
||||||
|
password: ${{ secrets.GRAFANA_API_KEY }}
|
||||||
|
' > prometheus.yml
|
||||||
|
|
||||||
|
docker run -d --name prometheus --network promnet \
|
||||||
|
-v $PWD/prometheus.yml:/etc/prometheus/prometheus.yml \
|
||||||
|
-p 9090:9090 \
|
||||||
|
prom/prometheus
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
@@ -428,9 +454,10 @@ jobs:
|
|||||||
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
CGO_ENABLED=1 GOARCH=${{ matrix.arch }} \
|
||||||
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
NETBIRD_STORE_ENGINE=${{ matrix.store }} \
|
||||||
CI=true \
|
CI=true \
|
||||||
|
GIT_BRANCH=${{ github.ref_name }} \
|
||||||
go test -tags devcert -run=^$ -bench=. \
|
go test -tags devcert -run=^$ -bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/... ./shared/management/... $(go list ./management/... ./shared/management/... | grep -v -e /management/server/http)
|
||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
@@ -521,7 +548,7 @@ jobs:
|
|||||||
-run=^$ \
|
-run=^$ \
|
||||||
-bench=. \
|
-bench=. \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE,GIT_BRANCH,GITHUB_RUN_ID' \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
name: "Management / Integration"
|
name: "Management / Integration"
|
||||||
@@ -571,4 +598,4 @@ jobs:
|
|||||||
CI=true \
|
CI=true \
|
||||||
go test -tags=integration \
|
go test -tags=integration \
|
||||||
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
-exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' \
|
||||||
-timeout 20m ./management/... ./shared/management/...
|
-timeout 20m ./management/server/http/...
|
||||||
|
|||||||
2
.github/workflows/golang-test-windows.yml
vendored
2
.github/workflows/golang-test-windows.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=${{ env.cache }}
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=${{ env.cache }}
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=${{ env.modcache }}
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe mod tidy
|
||||||
- run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' })" >> $env:GITHUB_ENV
|
- run: echo "files=$(go list ./... | ForEach-Object { $_ } | Where-Object { $_ -notmatch '/management' } | Where-Object { $_ -notmatch '/relay' } | Where-Object { $_ -notmatch '/signal' })" >> $env:GITHUB_ENV
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
|
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -tags=devcert -timeout 10m -p 1 ${{ env.files }} > test-out.txt 2>&1"
|
||||||
|
|||||||
2
.github/workflows/golangci-lint.yml
vendored
2
.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
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe,cros
|
||||||
skip: go.mod,go.sum
|
skip: go.mod,go.sum
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.22"
|
SIGN_PIPE_VER: "v0.0.23"
|
||||||
GORELEASER_VER: "v2.3.2"
|
GORELEASER_VER: "v2.3.2"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup MySQL privileges
|
||||||
|
if: matrix.store == 'mysql'
|
||||||
|
run: |
|
||||||
|
sleep 10
|
||||||
|
mysql -h 127.0.0.1 -u root -pmysqlroot -e "
|
||||||
|
GRANT SYSTEM_VARIABLES_ADMIN ON *.* TO 'netbird'@'%';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
"
|
||||||
|
|
||||||
- name: cp setup.env
|
- name: cp setup.env
|
||||||
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
||||||
|
|
||||||
|
|||||||
67
.github/workflows/wasm-build-validation.yml
vendored
Normal file
67
.github/workflows/wasm-build-validation.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
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: "1.23.x"
|
||||||
|
- 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: "1.23.x"
|
||||||
|
- 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 52428800 ]; then
|
||||||
|
echo "Wasm binary size (${SIZE_MB}MB) exceeds 50MB limit!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
0
.gitmodules
vendored
Normal file
0
.gitmodules
vendored
Normal file
@@ -2,6 +2,18 @@ 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
|
||||||
@@ -115,6 +127,11 @@ 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,3 +1,4 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -52,7 +53,7 @@
|
|||||||
|
|
||||||
### Open Source Network Security in a Single Platform
|
### Open Source Network Security in a Single Platform
|
||||||
|
|
||||||
<img width="1188" alt="centralized-network-management 1" src="https://github.com/user-attachments/assets/c28cc8e4-15d2-4d2f-bb97-a6433db39d56" />
|
https://github.com/user-attachments/assets/10cec749-bb56-4ab3-97af-4e38850108d2
|
||||||
|
|
||||||
### 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.0
|
FROM alpine:3.22.2
|
||||||
# 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="1"
|
NB_ENTRYPOINT_LOGIN_TIMEOUT="5"
|
||||||
|
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package android
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/formatter"
|
"github.com/netbirdio/netbird/formatter"
|
||||||
"github.com/netbirdio/netbird/util/net"
|
"github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionListener export internal Listener for mobile
|
// ConnectionListener export internal Listener for mobile
|
||||||
@@ -83,7 +84,8 @@ func NewClient(cfgFile string, androidSDKVersion int, deviceName string, uiVersi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run start the internal client. It is a blocker function
|
// Run start the internal client. It is a blocker function
|
||||||
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener) error {
|
func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
||||||
|
exportEnvList(envList)
|
||||||
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
||||||
ConfigPath: c.cfgFile,
|
ConfigPath: c.cfgFile,
|
||||||
})
|
})
|
||||||
@@ -118,7 +120,8 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
|
|||||||
|
|
||||||
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
||||||
// In this case make no sense handle registration steps.
|
// In this case make no sense handle registration steps.
|
||||||
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error {
|
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener, envList *EnvList) error {
|
||||||
|
exportEnvList(envList)
|
||||||
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
cfg, err := profilemanager.UpdateOrCreateConfig(profilemanager.ConfigInput{
|
||||||
ConfigPath: c.cfgFile,
|
ConfigPath: c.cfgFile,
|
||||||
})
|
})
|
||||||
@@ -249,3 +252,14 @@ func (c *Client) SetConnectionListener(listener ConnectionListener) {
|
|||||||
func (c *Client) RemoveConnectionListener() {
|
func (c *Client) RemoveConnectionListener() {
|
||||||
c.recorder.RemoveConnectionListener()
|
c.recorder.RemoveConnectionListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exportEnvList(list *EnvList) {
|
||||||
|
if list == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for k, v := range list.AllItems() {
|
||||||
|
if err := os.Setenv(k, v); err != nil {
|
||||||
|
log.Errorf("could not set env variable %s: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
32
client/android/env_list.go
Normal file
32
client/android/env_list.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package android
|
||||||
|
|
||||||
|
import "github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// EnvKeyNBForceRelay Exported for Android java client
|
||||||
|
EnvKeyNBForceRelay = peer.EnvKeyNBForceRelay
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvList wraps a Go map for export to Java
|
||||||
|
type EnvList struct {
|
||||||
|
data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEnvList creates a new EnvList
|
||||||
|
func NewEnvList() *EnvList {
|
||||||
|
return &EnvList{data: make(map[string]string)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put adds a key-value pair
|
||||||
|
func (el *EnvList) Put(key, value string) {
|
||||||
|
el.data[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value by key
|
||||||
|
func (el *EnvList) Get(key string) string {
|
||||||
|
return el.data[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (el *EnvList) AllItems() map[string]string {
|
||||||
|
return el.data
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ type ErrListener interface {
|
|||||||
// 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(string)
|
Open(string)
|
||||||
|
OnLoginSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth can register or login new client
|
// Auth can register or login new client
|
||||||
@@ -181,6 +182,11 @@ func (a *Auth) login(urlOpener URLOpener) 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ var (
|
|||||||
var debugCmd = &cobra.Command{
|
var debugCmd = &cobra.Command{
|
||||||
Use: "debug",
|
Use: "debug",
|
||||||
Short: "Debugging commands",
|
Short: "Debugging commands",
|
||||||
Long: "Provides commands for debugging and logging control within the NetBird daemon.",
|
Long: "Commands for debugging and logging within the NetBird daemon.",
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugBundleCmd = &cobra.Command{
|
var debugBundleCmd = &cobra.Command{
|
||||||
@@ -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{})
|
stat, err := client.Status(cmd.Context(), &proto.StatusRequest{ShouldRunProbes: true})
|
||||||
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,12 +303,18 @@ 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())
|
statusResp, err := getStatus(cmd.Context(), true)
|
||||||
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, "", ""),
|
nbstatus.ConvertToStatusOutputOverview(statusResp, anon, "", nil, nil, nil, "", profName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return statusOutputString
|
return statusOutputString
|
||||||
|
|||||||
8
client/cmd/debug_js.go
Normal file
8
client/cmd/debug_js.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -14,7 +14,8 @@ import (
|
|||||||
|
|
||||||
var downCmd = &cobra.Command{
|
var downCmd = &cobra.Command{
|
||||||
Use: "down",
|
Use: "down",
|
||||||
Short: "down netbird connections",
|
Short: "Disconnect from the NetBird network",
|
||||||
|
Long: "Disconnect the NetBird client from the network and management service. This will terminate all active connections with the remote peers.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ var downCmd = &cobra.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*7)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"os/user"
|
"os/user"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -31,7 +32,8 @@ func init() {
|
|||||||
|
|
||||||
var loginCmd = &cobra.Command{
|
var loginCmd = &cobra.Command{
|
||||||
Use: "login",
|
Use: "login",
|
||||||
Short: "login to the NetBird Management Service (first run)",
|
Short: "Log in to the NetBird network",
|
||||||
|
Long: "Log in to the NetBird network using a setup key or SSO",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := setEnvAndFlags(cmd); err != nil {
|
if err := setEnvAndFlags(cmd); err != nil {
|
||||||
return fmt.Errorf("set env and flags: %v", err)
|
return fmt.Errorf("set env and flags: %v", err)
|
||||||
@@ -226,7 +228,7 @@ func doForegroundLogin(ctx context.Context, cmd *cobra.Command, setupKey string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update host's static platform and system information
|
// update host's static platform and system information
|
||||||
system.UpdateStaticInfo()
|
system.UpdateStaticInfoAsync()
|
||||||
|
|
||||||
configFilePath, err := activeProf.FilePath()
|
configFilePath, err := activeProf.FilePath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -355,13 +357,21 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
|
|||||||
cmd.Println("")
|
cmd.Println("")
|
||||||
|
|
||||||
if !noBrowser {
|
if !noBrowser {
|
||||||
if err := open.Run(verificationURIComplete); err != nil {
|
if err := openBrowser(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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// openBrowser opens the URL in a browser, respecting the BROWSER environment variable.
|
||||||
|
func openBrowser(url string) error {
|
||||||
|
if browser := os.Getenv("BROWSER"); browser != "" {
|
||||||
|
return exec.Command(browser, url).Start()
|
||||||
|
}
|
||||||
|
return open.Run(url)
|
||||||
|
}
|
||||||
|
|
||||||
// isUnixRunningDesktop checks if a Linux OS is running desktop environment
|
// isUnixRunningDesktop checks if a Linux OS is running desktop environment
|
||||||
func isUnixRunningDesktop() bool {
|
func isUnixRunningDesktop() bool {
|
||||||
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import (
|
|||||||
var logoutCmd = &cobra.Command{
|
var logoutCmd = &cobra.Command{
|
||||||
Use: "deregister",
|
Use: "deregister",
|
||||||
Aliases: []string{"logout"},
|
Aliases: []string{"logout"},
|
||||||
Short: "deregister from the NetBird Management Service and delete peer",
|
Short: "Deregister from the NetBird management service and delete this peer",
|
||||||
|
Long: "This command will deregister the current peer from the NetBird management service and all associated configuration. Use with caution as this will remove the peer from the network.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ var appendFlag bool
|
|||||||
var networksCMD = &cobra.Command{
|
var networksCMD = &cobra.Command{
|
||||||
Use: "networks",
|
Use: "networks",
|
||||||
Aliases: []string{"routes"},
|
Aliases: []string{"routes"},
|
||||||
Short: "Manage networks",
|
Short: "Manage connections to NetBird Networks and Resources",
|
||||||
Long: `Commands to list, select, or deselect networks. Replaces the "routes" command.`,
|
Long: `Commands to list, select, or deselect networks. Replaces the "routes" command.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ import (
|
|||||||
|
|
||||||
var profileCmd = &cobra.Command{
|
var profileCmd = &cobra.Command{
|
||||||
Use: "profile",
|
Use: "profile",
|
||||||
Short: "manage NetBird profiles",
|
Short: "Manage NetBird client profiles",
|
||||||
Long: `Manage NetBird profiles, allowing you to list, switch, and remove profiles.`,
|
Long: `Commands to list, add, remove, and switch profiles. Profiles allow you to maintain different accounts in one client app.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileListCmd = &cobra.Command{
|
var profileListCmd = &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "list all profiles",
|
Short: "List all profiles",
|
||||||
Long: `List all available profiles in the NetBird client.`,
|
Long: `List all available profiles in the NetBird client.`,
|
||||||
Aliases: []string{"ls"},
|
Aliases: []string{"ls"},
|
||||||
RunE: listProfilesFunc,
|
RunE: listProfilesFunc,
|
||||||
@@ -30,7 +30,7 @@ var profileListCmd = &cobra.Command{
|
|||||||
|
|
||||||
var profileAddCmd = &cobra.Command{
|
var profileAddCmd = &cobra.Command{
|
||||||
Use: "add <profile_name>",
|
Use: "add <profile_name>",
|
||||||
Short: "add a new profile",
|
Short: "Add a new profile",
|
||||||
Long: `Add a new profile to the NetBird client. The profile name must be unique.`,
|
Long: `Add a new profile to the NetBird client. The profile name must be unique.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: addProfileFunc,
|
RunE: addProfileFunc,
|
||||||
@@ -38,16 +38,16 @@ var profileAddCmd = &cobra.Command{
|
|||||||
|
|
||||||
var profileRemoveCmd = &cobra.Command{
|
var profileRemoveCmd = &cobra.Command{
|
||||||
Use: "remove <profile_name>",
|
Use: "remove <profile_name>",
|
||||||
Short: "remove a profile",
|
Short: "Remove a profile",
|
||||||
Long: `Remove a profile from the NetBird client. The profile must not be active.`,
|
Long: `Remove a profile from the NetBird client. The profile must not be inactive.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: removeProfileFunc,
|
RunE: removeProfileFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
var profileSelectCmd = &cobra.Command{
|
var profileSelectCmd = &cobra.Command{
|
||||||
Use: "select <profile_name>",
|
Use: "select <profile_name>",
|
||||||
Short: "select a profile",
|
Short: "Select a profile",
|
||||||
Long: `Select a profile to be the active profile in the NetBird client. The profile must exist.`,
|
Long: `Make the specified profile active. This will switch the client to use the selected profile's configuration.`,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
RunE: selectProfileFunc,
|
RunE: selectProfileFunc,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const (
|
|||||||
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
||||||
dnsRouteIntervalFlag = "dns-router-interval"
|
dnsRouteIntervalFlag = "dns-router-interval"
|
||||||
enableLazyConnectionFlag = "enable-lazy-connection"
|
enableLazyConnectionFlag = "enable-lazy-connection"
|
||||||
|
mtuFlag = "mtu"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -72,7 +73,9 @@ var (
|
|||||||
anonymizeFlag bool
|
anonymizeFlag bool
|
||||||
dnsRouteInterval time.Duration
|
dnsRouteInterval time.Duration
|
||||||
lazyConnEnabled bool
|
lazyConnEnabled bool
|
||||||
|
mtu uint16
|
||||||
profilesDisabled bool
|
profilesDisabled bool
|
||||||
|
updateSettingsDisabled bool
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird",
|
Use: "netbird",
|
||||||
@@ -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*3)
|
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
return grpc.DialContext(
|
return grpc.DialContext(
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func TestSetFlagsFromEnvVars(t *testing.T) {
|
|||||||
cmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name")
|
cmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name")
|
||||||
cmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "Enable Rosenpass feature Rosenpass.")
|
cmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "Enable Rosenpass feature Rosenpass.")
|
||||||
cmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port")
|
cmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port")
|
||||||
|
cmd.PersistentFlags().Uint16Var(&mtu, mtuFlag, iface.DefaultMTU, "Set MTU (Maximum Transmission Unit) for the WireGuard interface")
|
||||||
|
|
||||||
t.Setenv("NB_EXTERNAL_IP_MAP", "abc,dec")
|
t.Setenv("NB_EXTERNAL_IP_MAP", "abc,dec")
|
||||||
t.Setenv("NB_INTERFACE_NAME", "test-name")
|
t.Setenv("NB_INTERFACE_NAME", "test-name")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
var serviceCmd = &cobra.Command{
|
var serviceCmd = &cobra.Command{
|
||||||
Use: "service",
|
Use: "service",
|
||||||
Short: "manages NetBird service",
|
Short: "Manage the NetBird daemon service",
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -42,7 +42,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd)
|
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd)
|
||||||
serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile.")
|
serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile. To persist this setting, use: netbird service install --disable-profiles")
|
||||||
|
serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||||
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
log.Info("starting NetBird service") //nolint
|
log.Info("starting NetBird service") //nolint
|
||||||
|
|
||||||
// Collect static system and platform information
|
// Collect static system and platform information
|
||||||
system.UpdateStaticInfo()
|
system.UpdateStaticInfoAsync()
|
||||||
|
|
||||||
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
||||||
p.serv = grpc.NewServer()
|
p.serv = grpc.NewServer()
|
||||||
@@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled)
|
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled)
|
||||||
if err := serverInstance.Start(); err != nil {
|
if err := serverInstance.Start(); err != nil {
|
||||||
log.Fatalf("failed to start daemon: %v", err)
|
log.Fatalf("failed to start daemon: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ func buildServiceArguments() []string {
|
|||||||
args = append(args, "--log-file", logFile)
|
args = append(args, "--log-file", logFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if profilesDisabled {
|
||||||
|
args = append(args, "--disable-profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateSettingsDisabled {
|
||||||
|
args = append(args, "--disable-update-settings")
|
||||||
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +107,7 @@ func createServiceConfigForInstall() (*service.Config, error) {
|
|||||||
|
|
||||||
var installCmd = &cobra.Command{
|
var installCmd = &cobra.Command{
|
||||||
Use: "install",
|
Use: "install",
|
||||||
Short: "installs NetBird service",
|
Short: "Install NetBird service",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if err := setupServiceCommand(cmd); err != nil {
|
if err := setupServiceCommand(cmd); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ var sshCmd = &cobra.Command{
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Short: "connect to a remote SSH server",
|
Short: "Connect to a remote SSH server",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
SetFlagsFromEnvVars(rootCmd)
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
SetFlagsFromEnvVars(cmd)
|
SetFlagsFromEnvVars(cmd)
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ var (
|
|||||||
|
|
||||||
var statusCmd = &cobra.Command{
|
var statusCmd = &cobra.Command{
|
||||||
Use: "status",
|
Use: "status",
|
||||||
Short: "status of the Netbird Service",
|
Short: "Display NetBird client status",
|
||||||
|
Long: "Display the current status of the NetBird client, including connection status, peer information, and network details.",
|
||||||
RunE: statusFunc,
|
RunE: statusFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,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)
|
resp, err := getStatus(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -120,7 +121,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
|
func getStatus(ctx context.Context, shouldRunProbes bool) (*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"+
|
||||||
@@ -129,7 +130,7 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true, ShouldRunProbes: true})
|
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true, ShouldRunProbes: shouldRunProbes})
|
||||||
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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,32 +9,33 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
|
||||||
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
|
||||||
"github.com/netbirdio/netbird/management/server/permissions"
|
|
||||||
"github.com/netbirdio/netbird/management/server/settings"
|
|
||||||
"github.com/netbirdio/netbird/management/server/store"
|
|
||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
|
||||||
"github.com/netbirdio/netbird/management/server/types"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/management-integrations/integrations"
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
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"
|
||||||
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
"github.com/netbirdio/netbird/management/internals/server/config"
|
||||||
mgmt "github.com/netbirdio/netbird/management/server"
|
mgmt "github.com/netbirdio/netbird/management/server"
|
||||||
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
"github.com/netbirdio/netbird/management/server/groups"
|
||||||
|
"github.com/netbirdio/netbird/management/server/integrations/port_forwarding"
|
||||||
|
"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/settings"
|
||||||
|
"github.com/netbirdio/netbird/management/server/store"
|
||||||
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
|
"github.com/netbirdio/netbird/management/server/types"
|
||||||
|
mgmtProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
sigProto "github.com/netbirdio/netbird/shared/signal/proto"
|
sigProto "github.com/netbirdio/netbird/shared/signal/proto"
|
||||||
sig "github.com/netbirdio/netbird/signal/server"
|
sig "github.com/netbirdio/netbird/signal/server"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func startTestingServices(t *testing.T) string {
|
func startTestingServices(t *testing.T) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
config := &types.Config{}
|
config := &config.Config{}
|
||||||
_, err := util.ReadJson("../testdata/management.json", config)
|
_, err := util.ReadJson("../testdata/management.json", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -69,7 +70,7 @@ func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
return s, lis
|
return s, lis
|
||||||
}
|
}
|
||||||
|
|
||||||
func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc.Server, net.Listener) {
|
func startManagement(t *testing.T, config *config.Config, testFile string) (*grpc.Server, net.Listener) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
lis, err := net.Listen("tcp", ":0")
|
lis, err := net.Listen("tcp", ":0")
|
||||||
@@ -88,15 +89,21 @@ func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
iv, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
|
||||||
|
|
||||||
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
|
||||||
require.NoError(t, err)
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
t.Cleanup(ctrl.Finish)
|
t.Cleanup(ctrl.Finish)
|
||||||
|
|
||||||
settingsMockManager := settings.NewMockManager(ctrl)
|
|
||||||
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
permissionsManagerMock := permissions.NewMockManager(ctrl)
|
||||||
|
peersmanager := peers.NewManager(store, permissionsManagerMock)
|
||||||
|
settingsManagerMock := settings.NewMockManager(ctrl)
|
||||||
|
|
||||||
|
iv, _ := integrations.NewIntegratedValidator(context.Background(), peersmanager, settingsManagerMock, eventStore)
|
||||||
|
|
||||||
|
metrics, err := telemetry.NewDefaultAppMetrics(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
settingsMockManager := settings.NewMockManager(ctrl)
|
||||||
|
groupsManager := groups.NewManagerMock()
|
||||||
|
|
||||||
settingsMockManager.EXPECT().
|
settingsMockManager.EXPECT().
|
||||||
GetSettings(gomock.Any(), gomock.Any(), gomock.Any()).
|
GetSettings(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
@@ -108,8 +115,8 @@ func startManagement(t *testing.T, config *types.Config, testFile string) (*grpc
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager)
|
secretsManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig, config.Relay, settingsMockManager, groupsManager)
|
||||||
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, nil, nil, &mgmt.MockIntegratedValidator{})
|
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, settingsMockManager, peersUpdateManager, secretsManager, nil, &manager.EphemeralManager{}, nil, &mgmt.MockIntegratedValidator{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -134,7 +141,7 @@ func startClientDaemon(
|
|||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
|
|
||||||
server := client.New(ctx,
|
server := client.New(ctx,
|
||||||
"", "", false)
|
"", "", false, false)
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ var (
|
|||||||
|
|
||||||
upCmd = &cobra.Command{
|
upCmd = &cobra.Command{
|
||||||
Use: "up",
|
Use: "up",
|
||||||
Short: "install, login and start NetBird client",
|
Short: "Connect to the NetBird network",
|
||||||
|
Long: "Connect to the NetBird network using the provided setup key or SSO auth. This command will bring up the WireGuard interface, connect to the management server, and establish peer-to-peer connections with other peers in the network if required.",
|
||||||
RunE: upFunc,
|
RunE: upFunc,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -62,6 +63,7 @@ func init() {
|
|||||||
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
|
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
|
||||||
upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name")
|
upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "WireGuard interface name")
|
||||||
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port")
|
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "WireGuard interface listening port")
|
||||||
|
upCmd.PersistentFlags().Uint16Var(&mtu, mtuFlag, iface.DefaultMTU, "Set MTU (Maximum Transmission Unit) for the WireGuard interface")
|
||||||
upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor,
|
upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor,
|
||||||
`Manage network monitoring. Defaults to true on Windows and macOS, false on Linux and FreeBSD. `+
|
`Manage network monitoring. Defaults to true on Windows and macOS, false on Linux and FreeBSD. `+
|
||||||
`E.g. --network-monitor=false to disable or --network-monitor=true to enable.`,
|
`E.g. --network-monitor=false to disable or --network-monitor=true to enable.`,
|
||||||
@@ -228,7 +230,9 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -356,6 +360,11 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro
|
|||||||
req.WireguardPort = &p
|
req.WireguardPort = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(mtuFlag).Changed {
|
||||||
|
m := int64(mtu)
|
||||||
|
req.Mtu = &m
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.Flag(networkMonitorFlag).Changed {
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
req.NetworkMonitor = &networkMonitor
|
req.NetworkMonitor = &networkMonitor
|
||||||
}
|
}
|
||||||
@@ -435,6 +444,13 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil
|
|||||||
ic.WireguardPort = &p
|
ic.WireguardPort = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(mtuFlag).Changed {
|
||||||
|
if err := iface.ValidateMTU(mtu); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ic.MTU = &mtu
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.Flag(networkMonitorFlag).Changed {
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
ic.NetworkMonitor = &networkMonitor
|
ic.NetworkMonitor = &networkMonitor
|
||||||
}
|
}
|
||||||
@@ -532,6 +548,14 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
|
|||||||
loginRequest.WireguardPort = &wp
|
loginRequest.WireguardPort = &wp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(mtuFlag).Changed {
|
||||||
|
if err := iface.ValidateMTU(mtu); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
m := int64(mtu)
|
||||||
|
loginRequest.Mtu = &m
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.Flag(networkMonitorFlag).Changed {
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
loginRequest.NetworkMonitor = &networkMonitor
|
loginRequest.NetworkMonitor = &networkMonitor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
versionCmd = &cobra.Command{
|
versionCmd = &cobra.Command{
|
||||||
Use: "version",
|
Use: "version",
|
||||||
Short: "prints NetBird version",
|
Short: "Print the NetBird's client application version",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
cmd.SetOut(cmd.OutOrStdout())
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
cmd.Println(version.NetbirdVersion())
|
cmd.Println(version.NetbirdVersion())
|
||||||
|
|||||||
@@ -23,23 +23,29 @@ import (
|
|||||||
|
|
||||||
var ErrClientAlreadyStarted = errors.New("client already started")
|
var ErrClientAlreadyStarted = errors.New("client already started")
|
||||||
var ErrClientNotStarted = errors.New("client not started")
|
var ErrClientNotStarted = errors.New("client not started")
|
||||||
|
var 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
|
||||||
@@ -58,8 +64,35 @@ type Options struct {
|
|||||||
DisableClientRoutes bool
|
DisableClientRoutes bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new netbird embedded client
|
// validateCredentials checks that exactly one credential type is provided
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
@@ -107,9 +140,14 @@ 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
|
||||||
}
|
}
|
||||||
@@ -126,7 +164,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, ""); err != nil {
|
if err := internal.Login(ctx, c.config, c.setupKey, c.jwtToken); err != nil {
|
||||||
return fmt.Errorf("login: %w", err)
|
return fmt.Errorf("login: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +173,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{}, 1)
|
run := make(chan struct{})
|
||||||
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 {
|
||||||
@@ -187,6 +225,16 @@ 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) {
|
||||||
@@ -211,7 +259,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenTCP listens on the given address in the netbird network
|
// 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()
|
||||||
@@ -232,7 +280,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()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
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/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -85,7 +85,7 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
) ([]firewall.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
chain := chainNameInputRules
|
chain := chainNameInputRules
|
||||||
|
|
||||||
ipsetName = transformIPsetName(ipsetName, sPort, dPort)
|
ipsetName = transformIPsetName(ipsetName, sPort, dPort, action)
|
||||||
specs := filterRuleSpecs(ip, string(protocol), sPort, dPort, action, ipsetName)
|
specs := filterRuleSpecs(ip, string(protocol), sPort, dPort, action, ipsetName)
|
||||||
|
|
||||||
mangleSpecs := slices.Clone(specs)
|
mangleSpecs := slices.Clone(specs)
|
||||||
@@ -135,7 +135,14 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
return nil, fmt.Errorf("rule already exists")
|
return nil, fmt.Errorf("rule already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.iptablesClient.Append(tableFilter, chain, specs...); err != nil {
|
// Insert DROP rules at the beginning, append ACCEPT rules at the end
|
||||||
|
if action == firewall.ActionDrop {
|
||||||
|
// Insert at the beginning of the chain (position 1)
|
||||||
|
err = m.iptablesClient.Insert(tableFilter, chain, 1, specs...)
|
||||||
|
} else {
|
||||||
|
err = m.iptablesClient.Append(tableFilter, chain, specs...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,17 +395,24 @@ func actionToStr(action firewall.Action) string {
|
|||||||
return "DROP"
|
return "DROP"
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port) string {
|
func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port, action firewall.Action) string {
|
||||||
switch {
|
if ipsetName == "" {
|
||||||
case ipsetName == "":
|
|
||||||
return ""
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
actionSuffix := ""
|
||||||
|
if action == firewall.ActionDrop {
|
||||||
|
actionSuffix = "-drop"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
case sPort != nil && dPort != nil:
|
case sPort != nil && dPort != nil:
|
||||||
return ipsetName + "-sport-dport"
|
return ipsetName + "-sport-dport" + actionSuffix
|
||||||
case sPort != nil:
|
case sPort != nil:
|
||||||
return ipsetName + "-sport"
|
return ipsetName + "-sport" + actionSuffix
|
||||||
case dPort != nil:
|
case dPort != nil:
|
||||||
return ipsetName + "-dport"
|
return ipsetName + "-dport" + actionSuffix
|
||||||
default:
|
default:
|
||||||
return ipsetName
|
return ipsetName + actionSuffix
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,6 +260,22 @@ 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"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package iptables
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
|
|
||||||
var ifaceMock = &iFaceMock{
|
var ifaceMock = &iFaceMock{
|
||||||
NameFunc: func() string {
|
NameFunc: func() string {
|
||||||
return "lo"
|
return "wg-test"
|
||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
@@ -109,10 +110,84 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIptablesManagerDenyRules(t *testing.T) {
|
||||||
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
manager, err := Create(ifaceMock)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := manager.Close(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("add deny rule", func(t *testing.T) {
|
||||||
|
ip := netip.MustParseAddr("10.20.0.3")
|
||||||
|
port := &fw.Port{Values: []uint16{22}}
|
||||||
|
|
||||||
|
rule, err := manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionDrop, "deny-ssh")
|
||||||
|
require.NoError(t, err, "failed to add deny rule")
|
||||||
|
require.NotEmpty(t, rule, "deny rule should not be empty")
|
||||||
|
|
||||||
|
// Verify the rule was added by checking iptables
|
||||||
|
for _, r := range rule {
|
||||||
|
rr := r.(*Rule)
|
||||||
|
checkRuleSpecs(t, ipv4Client, rr.chain, true, rr.specs...)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("deny rule precedence test", func(t *testing.T) {
|
||||||
|
ip := netip.MustParseAddr("10.20.0.4")
|
||||||
|
port := &fw.Port{Values: []uint16{80}}
|
||||||
|
|
||||||
|
// Add accept rule first
|
||||||
|
_, err := manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "accept-http")
|
||||||
|
require.NoError(t, err, "failed to add accept rule")
|
||||||
|
|
||||||
|
// Add deny rule second for same IP/port - this should take precedence
|
||||||
|
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionDrop, "deny-http")
|
||||||
|
require.NoError(t, err, "failed to add deny rule")
|
||||||
|
|
||||||
|
// Inspect the actual iptables rules to verify deny rule comes before accept rule
|
||||||
|
rules, err := ipv4Client.List("filter", chainNameInputRules)
|
||||||
|
require.NoError(t, err, "failed to list iptables rules")
|
||||||
|
|
||||||
|
// Debug: print all rules
|
||||||
|
t.Logf("All iptables rules in chain %s:", chainNameInputRules)
|
||||||
|
for i, rule := range rules {
|
||||||
|
t.Logf(" [%d] %s", i, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
var denyRuleIndex, acceptRuleIndex int = -1, -1
|
||||||
|
for i, rule := range rules {
|
||||||
|
if strings.Contains(rule, "DROP") {
|
||||||
|
t.Logf("Found DROP rule at index %d: %s", i, rule)
|
||||||
|
if strings.Contains(rule, "deny-http") && strings.Contains(rule, "80") {
|
||||||
|
denyRuleIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(rule, "ACCEPT") {
|
||||||
|
t.Logf("Found ACCEPT rule at index %d: %s", i, rule)
|
||||||
|
if strings.Contains(rule, "accept-http") && strings.Contains(rule, "80") {
|
||||||
|
acceptRuleIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEqual(t, -1, denyRuleIndex, "deny rule should exist in iptables")
|
||||||
|
require.NotEqual(t, -1, acceptRuleIndex, "accept rule should exist in iptables")
|
||||||
|
require.Less(t, denyRuleIndex, acceptRuleIndex,
|
||||||
|
"deny rule should come before accept rule in iptables chain (deny at index %d, accept at index %d)",
|
||||||
|
denyRuleIndex, acceptRuleIndex)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestIptablesManagerIPSet(t *testing.T) {
|
func TestIptablesManagerIPSet(t *testing.T) {
|
||||||
mock := &iFaceMock{
|
mock := &iFaceMock{
|
||||||
NameFunc: func() string {
|
NameFunc: func() string {
|
||||||
return "lo"
|
return "wg-test"
|
||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
@@ -176,7 +251,7 @@ func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName strin
|
|||||||
func TestIptablesCreatePerformance(t *testing.T) {
|
func TestIptablesCreatePerformance(t *testing.T) {
|
||||||
mock := &iFaceMock{
|
mock := &iFaceMock{
|
||||||
NameFunc: func() string {
|
NameFunc: func() string {
|
||||||
return "lo"
|
return "wg-test"
|
||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
|
|||||||
@@ -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/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants needed to manage and create iptable rules
|
// constants needed to manage and create iptable rules
|
||||||
@@ -880,6 +880,54 @@ 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
|
||||||
|
|||||||
@@ -14,7 +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"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func isIptablesSupported() bool {
|
func isIptablesSupported() bool {
|
||||||
|
|||||||
@@ -151,14 +151,20 @@ type Manager interface {
|
|||||||
|
|
||||||
DisableRouting() error
|
DisableRouting() error
|
||||||
|
|
||||||
// AddDNATRule adds a DNAT rule
|
// AddDNATRule adds outbound DNAT rule for forwarding external traffic to the NetBird network.
|
||||||
AddDNATRule(ForwardRule) (Rule, error)
|
AddDNATRule(ForwardRule) (Rule, error)
|
||||||
|
|
||||||
// DeleteDNATRule deletes a DNAT rule
|
// DeleteDNATRule deletes the outbound 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/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -341,30 +341,38 @@ func (m *AclManager) addIOFiltering(
|
|||||||
userData := []byte(ruleId)
|
userData := []byte(ruleId)
|
||||||
|
|
||||||
chain := m.chainInputRules
|
chain := m.chainInputRules
|
||||||
nftRule := m.rConn.AddRule(&nftables.Rule{
|
rule := &nftables.Rule{
|
||||||
Table: m.workTable,
|
Table: m.workTable,
|
||||||
Chain: chain,
|
Chain: chain,
|
||||||
Exprs: mainExpressions,
|
Exprs: mainExpressions,
|
||||||
UserData: userData,
|
UserData: userData,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Insert DROP rules at the beginning, append ACCEPT rules at the end
|
||||||
|
var nftRule *nftables.Rule
|
||||||
|
if action == firewall.ActionDrop {
|
||||||
|
nftRule = m.rConn.InsertRule(rule)
|
||||||
|
} else {
|
||||||
|
nftRule = m.rConn.AddRule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
if err := m.rConn.Flush(); err != nil {
|
||||||
return nil, fmt.Errorf(flushError, err)
|
return nil, fmt.Errorf(flushError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule := &Rule{
|
ruleStruct := &Rule{
|
||||||
nftRule: nftRule,
|
nftRule: nftRule,
|
||||||
mangleRule: m.createPreroutingRule(expressions, userData),
|
mangleRule: m.createPreroutingRule(expressions, userData),
|
||||||
nftSet: ipset,
|
nftSet: ipset,
|
||||||
ruleID: ruleId,
|
ruleID: ruleId,
|
||||||
ip: ip,
|
ip: ip,
|
||||||
}
|
}
|
||||||
m.rules[ruleId] = rule
|
m.rules[ruleId] = ruleStruct
|
||||||
if ipset != nil {
|
if ipset != nil {
|
||||||
m.ipsetStore.AddReferenceToIpset(ipset.Name)
|
m.ipsetStore.AddReferenceToIpset(ipset.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rule, nil
|
return ruleStruct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AclManager) createPreroutingRule(expressions []expr.Any, userData []byte) *nftables.Rule {
|
func (m *AclManager) createPreroutingRule(expressions []expr.Any, userData []byte) *nftables.Rule {
|
||||||
|
|||||||
@@ -376,6 +376,22 @@ 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 {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package nftables
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -20,7 +21,7 @@ import (
|
|||||||
|
|
||||||
var ifaceMock = &iFaceMock{
|
var ifaceMock = &iFaceMock{
|
||||||
NameFunc: func() string {
|
NameFunc: func() string {
|
||||||
return "lo"
|
return "wg-test"
|
||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
@@ -103,9 +104,8 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
Kind: expr.VerdictAccept,
|
Kind: expr.VerdictAccept,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1)
|
// Since DROP rules are inserted at position 0, the DROP rule comes first
|
||||||
|
expectedDropExprs := []expr.Any{
|
||||||
expectedExprs2 := []expr.Any{
|
|
||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
@@ -141,7 +141,12 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
},
|
},
|
||||||
&expr.Verdict{Kind: expr.VerdictDrop},
|
&expr.Verdict{Kind: expr.VerdictDrop},
|
||||||
}
|
}
|
||||||
require.ElementsMatch(t, rules[1].Exprs, expectedExprs2, "expected the same expressions")
|
|
||||||
|
// Compare DROP rule at position 0 (inserted first due to InsertRule)
|
||||||
|
compareExprsIgnoringCounters(t, rules[0].Exprs, expectedDropExprs)
|
||||||
|
|
||||||
|
// Compare connection tracking rule at position 1 (pushed down by DROP rule insertion)
|
||||||
|
compareExprsIgnoringCounters(t, rules[1].Exprs, expectedExprs1)
|
||||||
|
|
||||||
for _, r := range rule {
|
for _, r := range rule {
|
||||||
err = manager.DeletePeerRule(r)
|
err = manager.DeletePeerRule(r)
|
||||||
@@ -160,10 +165,90 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
require.NoError(t, err, "failed to reset")
|
require.NoError(t, err, "failed to reset")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNftablesManagerRuleOrder(t *testing.T) {
|
||||||
|
// This test verifies rule insertion order in nftables peer ACLs
|
||||||
|
// We add accept rule first, then deny rule to test ordering behavior
|
||||||
|
manager, err := Create(ifaceMock)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, manager.Init(nil))
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = manager.Close(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
ip := netip.MustParseAddr("100.96.0.2").Unmap()
|
||||||
|
testClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
// Add accept rule first
|
||||||
|
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "accept-http")
|
||||||
|
require.NoError(t, err, "failed to add accept rule")
|
||||||
|
|
||||||
|
// Add deny rule second for the same traffic
|
||||||
|
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop, "deny-http")
|
||||||
|
require.NoError(t, err, "failed to add deny rule")
|
||||||
|
|
||||||
|
err = manager.Flush()
|
||||||
|
require.NoError(t, err, "failed to flush")
|
||||||
|
|
||||||
|
rules, err := testClient.GetRules(manager.aclManager.workTable, manager.aclManager.chainInputRules)
|
||||||
|
require.NoError(t, err, "failed to get rules")
|
||||||
|
|
||||||
|
t.Logf("Found %d rules in nftables chain", len(rules))
|
||||||
|
|
||||||
|
// Find the accept and deny rules and verify deny comes before accept
|
||||||
|
var acceptRuleIndex, denyRuleIndex int = -1, -1
|
||||||
|
for i, rule := range rules {
|
||||||
|
hasAcceptHTTPSet := false
|
||||||
|
hasDenyHTTPSet := false
|
||||||
|
hasPort80 := false
|
||||||
|
var action string
|
||||||
|
|
||||||
|
for _, e := range rule.Exprs {
|
||||||
|
// Check for set lookup
|
||||||
|
if lookup, ok := e.(*expr.Lookup); ok {
|
||||||
|
if lookup.SetName == "accept-http" {
|
||||||
|
hasAcceptHTTPSet = true
|
||||||
|
} else if lookup.SetName == "deny-http" {
|
||||||
|
hasDenyHTTPSet = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for port 80
|
||||||
|
if cmp, ok := e.(*expr.Cmp); ok {
|
||||||
|
if cmp.Op == expr.CmpOpEq && len(cmp.Data) == 2 && binary.BigEndian.Uint16(cmp.Data) == 80 {
|
||||||
|
hasPort80 = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for verdict
|
||||||
|
if verdict, ok := e.(*expr.Verdict); ok {
|
||||||
|
if verdict.Kind == expr.VerdictAccept {
|
||||||
|
action = "ACCEPT"
|
||||||
|
} else if verdict.Kind == expr.VerdictDrop {
|
||||||
|
action = "DROP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasAcceptHTTPSet && hasPort80 && action == "ACCEPT" {
|
||||||
|
t.Logf("Rule [%d]: accept-http set + Port 80 + ACCEPT", i)
|
||||||
|
acceptRuleIndex = i
|
||||||
|
} else if hasDenyHTTPSet && hasPort80 && action == "DROP" {
|
||||||
|
t.Logf("Rule [%d]: deny-http set + Port 80 + DROP", i)
|
||||||
|
denyRuleIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEqual(t, -1, acceptRuleIndex, "accept rule should exist in nftables")
|
||||||
|
require.NotEqual(t, -1, denyRuleIndex, "deny rule should exist in nftables")
|
||||||
|
require.Less(t, denyRuleIndex, acceptRuleIndex,
|
||||||
|
"deny rule should come before accept rule in nftables chain (deny at index %d, accept at index %d)",
|
||||||
|
denyRuleIndex, acceptRuleIndex)
|
||||||
|
}
|
||||||
|
|
||||||
func TestNFtablesCreatePerformance(t *testing.T) {
|
func TestNFtablesCreatePerformance(t *testing.T) {
|
||||||
mock := &iFaceMock{
|
mock := &iFaceMock{
|
||||||
NameFunc: func() string {
|
NameFunc: func() string {
|
||||||
return "lo"
|
return "wg-test"
|
||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
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/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -1350,6 +1350,103 @@ 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,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
|||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
m.outgoingRules = make(map[netip.Addr]RuleSet)
|
m.outgoingRules = make(map[netip.Addr]RuleSet)
|
||||||
|
m.incomingDenyRules = make(map[netip.Addr]RuleSet)
|
||||||
m.incomingRules = make(map[netip.Addr]RuleSet)
|
m.incomingRules = make(map[netip.Addr]RuleSet)
|
||||||
|
|
||||||
if m.udpTracker != nil {
|
if m.udpTracker != nil {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (m *Manager) Close(*statemanager.Manager) error {
|
|||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
m.outgoingRules = make(map[netip.Addr]RuleSet)
|
m.outgoingRules = make(map[netip.Addr]RuleSet)
|
||||||
|
m.incomingDenyRules = make(map[netip.Addr]RuleSet)
|
||||||
m.incomingRules = make(map[netip.Addr]RuleSet)
|
m.incomingRules = make(map[netip.Addr]RuleSet)
|
||||||
|
|
||||||
if m.udpTracker != nil {
|
if m.udpTracker != nil {
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ 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, bool) {
|
func (t *TCPTracker) updateIfExists(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, direction nftypes.Direction, size int) (ConnKey, uint16, bool) {
|
||||||
key := ConnKey{
|
key := ConnKey{
|
||||||
SrcIP: srcIP,
|
SrcIP: srcIP,
|
||||||
DstIP: dstIP,
|
DstIP: dstIP,
|
||||||
@@ -171,28 +171,30 @@ 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, true
|
return key, uint16(conn.DNATOrigPort.Load()), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, false
|
return key, 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackOutbound records an outbound TCP connection
|
// TrackOutbound records an outbound TCP connection and returns the original port if DNAT reversal is needed
|
||||||
func (t *TCPTracker) TrackOutbound(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, size int) {
|
func (t *TCPTracker) TrackOutbound(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, size int) uint16 {
|
||||||
if _, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, flags, nftypes.Egress, size); !exists {
|
if _, origPort, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, flags, nftypes.Egress, size); exists {
|
||||||
// if (inverted direction) conn is not tracked, track this direction
|
return origPort
|
||||||
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) {
|
func (t *TCPTracker) TrackInbound(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, ruleID []byte, size int, dnatOrigPort uint16) {
|
||||||
t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Ingress, ruleID, size)
|
t.track(srcIP, dstIP, srcPort, dstPort, flags, nftypes.Ingress, ruleID, size, dnatOrigPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func (t *TCPTracker) track(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, flags uint8, direction nftypes.Direction, ruleID []byte, size int, origPort uint16) {
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -210,8 +212,13 @@ 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))
|
||||||
|
|
||||||
t.logger.Trace2("New %s TCP connection: %s", direction, key)
|
if origPort != 0 {
|
||||||
|
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()
|
||||||
@@ -449,6 +456,21 @@ 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)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPSyn, nil, 100, 0)
|
||||||
|
|
||||||
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)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100, 0)
|
||||||
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)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPPush|TCPAck, nil, 1000, 0)
|
||||||
|
|
||||||
// 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)
|
tracker.TrackInbound(clientIP, serverIP, clientPort, serverPort, TCPAck, nil, 100, 0)
|
||||||
|
|
||||||
// Verify state and counters
|
// Verify state and counters
|
||||||
require.Equal(t, TCPStateEstablished, conn.GetState())
|
require.Equal(t, TCPStateEstablished, conn.GetState())
|
||||||
|
|||||||
@@ -58,20 +58,23 @@ func NewUDPTracker(timeout time.Duration, logger *nblog.Logger, flowLogger nftyp
|
|||||||
return tracker
|
return tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrackOutbound records an outbound UDP connection
|
// TrackOutbound records an outbound UDP connection and returns the original port if DNAT reversal is needed
|
||||||
func (t *UDPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, size int) {
|
func (t *UDPTracker) TrackOutbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, size int) uint16 {
|
||||||
if _, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, nftypes.Egress, size); !exists {
|
_, origPort, exists := t.updateIfExists(dstIP, srcIP, dstPort, srcPort, nftypes.Egress, size)
|
||||||
// if (inverted direction) conn is not tracked, track this direction
|
if exists {
|
||||||
t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Egress, nil, size)
|
return origPort
|
||||||
}
|
}
|
||||||
|
// 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) {
|
func (t *UDPTracker) TrackInbound(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, ruleID []byte, size int, dnatOrigPort uint16) {
|
||||||
t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Ingress, ruleID, size)
|
t.track(srcIP, dstIP, srcPort, dstPort, nftypes.Ingress, ruleID, size, dnatOrigPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *UDPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, size int) (ConnKey, bool) {
|
func (t *UDPTracker) updateIfExists(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, size int) (ConnKey, uint16, bool) {
|
||||||
key := ConnKey{
|
key := ConnKey{
|
||||||
SrcIP: srcIP,
|
SrcIP: srcIP,
|
||||||
DstIP: dstIP,
|
DstIP: dstIP,
|
||||||
@@ -86,15 +89,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, true
|
return key, uint16(conn.DNATOrigPort.Load()), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return key, false
|
return key, 0, 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) {
|
func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, dstPort uint16, direction nftypes.Direction, ruleID []byte, size int, origPort uint16) {
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -109,6 +112,7 @@ 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)
|
||||||
|
|
||||||
@@ -116,7 +120,11 @@ func (t *UDPTracker) track(srcIP netip.Addr, dstIP netip.Addr, srcPort uint16, d
|
|||||||
t.connections[key] = conn
|
t.connections[key] = conn
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
|
|
||||||
t.logger.Trace2("New %s UDP connection: %s", direction, key)
|
if origPort != 0 {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,14 +70,13 @@ func (r RouteRules) Sort() {
|
|||||||
|
|
||||||
// Manager userspace firewall manager
|
// Manager userspace firewall manager
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
// outgoingRules is used for hooks only
|
outgoingRules map[netip.Addr]RuleSet
|
||||||
outgoingRules map[netip.Addr]RuleSet
|
incomingDenyRules map[netip.Addr]RuleSet
|
||||||
// incomingRules is used for filtering and hooks
|
incomingRules map[netip.Addr]RuleSet
|
||||||
incomingRules map[netip.Addr]RuleSet
|
routeRules RouteRules
|
||||||
routeRules RouteRules
|
decoders sync.Pool
|
||||||
decoders sync.Pool
|
wgIface common.IFaceMapper
|
||||||
wgIface common.IFaceMapper
|
nativeFirewall firewall.Manager
|
||||||
nativeFirewall firewall.Manager
|
|
||||||
|
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
|
||||||
@@ -110,6 +109,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
// decoder for packages
|
// decoder for packages
|
||||||
@@ -123,6 +126,8 @@ 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
|
||||||
@@ -186,6 +191,7 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
|
|||||||
},
|
},
|
||||||
nativeFirewall: nativeFirewall,
|
nativeFirewall: nativeFirewall,
|
||||||
outgoingRules: make(map[netip.Addr]RuleSet),
|
outgoingRules: make(map[netip.Addr]RuleSet),
|
||||||
|
incomingDenyRules: make(map[netip.Addr]RuleSet),
|
||||||
incomingRules: make(map[netip.Addr]RuleSet),
|
incomingRules: make(map[netip.Addr]RuleSet),
|
||||||
wgIface: iface,
|
wgIface: iface,
|
||||||
localipmanager: newLocalIPManager(),
|
localipmanager: newLocalIPManager(),
|
||||||
@@ -196,6 +202,7 @@ 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{},
|
||||||
}
|
}
|
||||||
m.routingEnabled.Store(false)
|
m.routingEnabled.Store(false)
|
||||||
|
|
||||||
@@ -417,10 +424,17 @@ func (m *Manager) AddPeerFiltering(
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
if _, ok := m.incomingRules[r.ip]; !ok {
|
var targetMap map[netip.Addr]RuleSet
|
||||||
m.incomingRules[r.ip] = make(RuleSet)
|
if r.drop {
|
||||||
|
targetMap = m.incomingDenyRules
|
||||||
|
} else {
|
||||||
|
targetMap = m.incomingRules
|
||||||
}
|
}
|
||||||
m.incomingRules[r.ip][r.id] = r
|
|
||||||
|
if _, ok := targetMap[r.ip]; !ok {
|
||||||
|
targetMap[r.ip] = make(RuleSet)
|
||||||
|
}
|
||||||
|
targetMap[r.ip][r.id] = r
|
||||||
m.mutex.Unlock()
|
m.mutex.Unlock()
|
||||||
return []firewall.Rule{&r}, nil
|
return []firewall.Rule{&r}, nil
|
||||||
}
|
}
|
||||||
@@ -507,10 +521,24 @@ func (m *Manager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
|
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := m.incomingRules[r.ip][r.id]; !ok {
|
var sourceMap map[netip.Addr]RuleSet
|
||||||
|
if r.drop {
|
||||||
|
sourceMap = m.incomingDenyRules
|
||||||
|
} else {
|
||||||
|
sourceMap = m.incomingRules
|
||||||
|
}
|
||||||
|
|
||||||
|
if ruleset, ok := sourceMap[r.ip]; ok {
|
||||||
|
if _, exists := ruleset[r.id]; !exists {
|
||||||
|
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
|
||||||
|
}
|
||||||
|
delete(ruleset, r.id)
|
||||||
|
if len(ruleset) == 0 {
|
||||||
|
delete(sourceMap, r.ip)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
|
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
|
||||||
}
|
}
|
||||||
delete(m.incomingRules[r.ip], r.id)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -572,7 +600,7 @@ func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterOutBound filters outgoing packets
|
// FilterOutbound filters outgoing packets
|
||||||
func (m *Manager) FilterOutbound(packetData []byte, size int) bool {
|
func (m *Manager) FilterOutbound(packetData []byte, size int) bool {
|
||||||
return m.filterOutbound(packetData, size)
|
return m.filterOutbound(packetData, size)
|
||||||
}
|
}
|
||||||
@@ -609,7 +637,7 @@ func (m *Manager) filterOutbound(packetData []byte, size int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
m.trackOutbound(d, srcIP, dstIP, size)
|
m.trackOutbound(d, srcIP, dstIP, packetData, size)
|
||||||
m.translateOutboundDNAT(packetData, d)
|
m.translateOutboundDNAT(packetData, d)
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -653,14 +681,26 @@ func getTCPFlags(tcp *layers.TCP) uint8 {
|
|||||||
return flags
|
return flags
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) trackOutbound(d *decoder, srcIP, dstIP netip.Addr, size int) {
|
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:
|
||||||
m.udpTracker.TrackOutbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), size)
|
origPort := 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)
|
||||||
m.tcpTracker.TrackOutbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, size)
|
origPort := 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)
|
||||||
}
|
}
|
||||||
@@ -670,13 +710,15 @@ 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)
|
m.udpTracker.TrackInbound(srcIP, dstIP, uint16(d.udp.SrcPort), uint16(d.udp.DstPort), ruleID, size, d.dnatOrigPort)
|
||||||
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)
|
m.tcpTracker.TrackInbound(srcIP, dstIP, uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort), flags, ruleID, size, d.dnatOrigPort)
|
||||||
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
|
||||||
@@ -738,10 +780,20 @@ 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)
|
||||||
@@ -761,7 +813,7 @@ func (m *Manager) filterInbound(packetData []byte, size int) bool {
|
|||||||
// handleLocalTraffic handles local traffic.
|
// handleLocalTraffic handles local traffic.
|
||||||
// If it returns true, the packet should be dropped.
|
// If it returns true, the packet should be dropped.
|
||||||
func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packetData []byte, size int) bool {
|
func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packetData []byte, size int) bool {
|
||||||
ruleID, blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d)
|
ruleID, blocked := m.peerACLsBlock(srcIP, d, packetData)
|
||||||
if blocked {
|
if blocked {
|
||||||
_, pnum := getProtocolFromPacket(d)
|
_, pnum := getProtocolFromPacket(d)
|
||||||
srcPort, dstPort := getPortsFromPacket(d)
|
srcPort, dstPort := getPortsFromPacket(d)
|
||||||
@@ -971,26 +1023,28 @@ func (m *Manager) isSpecialICMP(d *decoder) bool {
|
|||||||
icmpType == layers.ICMPv4TypeTimeExceeded
|
icmpType == layers.ICMPv4TypeTimeExceeded
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) peerACLsBlock(srcIP netip.Addr, packetData []byte, rules map[netip.Addr]RuleSet, d *decoder) ([]byte, bool) {
|
func (m *Manager) peerACLsBlock(srcIP netip.Addr, d *decoder, packetData []byte) ([]byte, bool) {
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
if m.isSpecialICMP(d) {
|
if m.isSpecialICMP(d) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if mgmtId, filter, ok := validateRule(srcIP, packetData, rules[srcIP], d); ok {
|
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingDenyRules[srcIP], d); ok {
|
||||||
return mgmtId, filter
|
return mgmtId, filter
|
||||||
}
|
}
|
||||||
|
|
||||||
if mgmtId, filter, ok := validateRule(srcIP, packetData, rules[netip.IPv4Unspecified()], d); ok {
|
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingRules[srcIP], d); ok {
|
||||||
|
return mgmtId, filter
|
||||||
|
}
|
||||||
|
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingRules[netip.IPv4Unspecified()], d); ok {
|
||||||
|
return mgmtId, filter
|
||||||
|
}
|
||||||
|
if mgmtId, filter, ok := validateRule(srcIP, packetData, m.incomingRules[netip.IPv6Unspecified()], d); ok {
|
||||||
return mgmtId, filter
|
return mgmtId, filter
|
||||||
}
|
}
|
||||||
|
|
||||||
if mgmtId, filter, ok := validateRule(srcIP, packetData, rules[netip.IPv6Unspecified()], d); ok {
|
|
||||||
return mgmtId, filter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default policy: DROP ALL
|
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1013,6 +1067,7 @@ func portsMatch(rulePort *firewall.Port, packetPort uint16) bool {
|
|||||||
|
|
||||||
func validateRule(ip netip.Addr, packetData []byte, rules map[string]PeerRule, d *decoder) ([]byte, bool, bool) {
|
func validateRule(ip netip.Addr, packetData []byte, rules map[string]PeerRule, d *decoder) ([]byte, bool, bool) {
|
||||||
payloadLayer := d.decoded[1]
|
payloadLayer := d.decoded[1]
|
||||||
|
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if rule.matchByIP && ip.Compare(rule.ip) != 0 {
|
if rule.matchByIP && ip.Compare(rule.ip) != 0 {
|
||||||
continue
|
continue
|
||||||
@@ -1045,6 +1100,7 @@ func validateRule(ip netip.Addr, packetData []byte, rules map[string]PeerRule, d
|
|||||||
return rule.mgmtId, rule.drop, true
|
return rule.mgmtId, rule.drop, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, false, false
|
return nil, false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,6 +1172,7 @@ func (m *Manager) AddUDPPacketHook(in bool, ip netip.Addr, dPort uint16, hook fu
|
|||||||
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
if in {
|
if in {
|
||||||
|
// Incoming UDP hooks are stored in allow rules map
|
||||||
if _, ok := m.incomingRules[r.ip]; !ok {
|
if _, ok := m.incomingRules[r.ip]; !ok {
|
||||||
m.incomingRules[r.ip] = make(map[string]PeerRule)
|
m.incomingRules[r.ip] = make(map[string]PeerRule)
|
||||||
}
|
}
|
||||||
@@ -1136,6 +1193,7 @@ func (m *Manager) RemovePacketHook(hookID string) error {
|
|||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
// Check incoming hooks (stored in allow rules)
|
||||||
for _, arr := range m.incomingRules {
|
for _, arr := range m.incomingRules {
|
||||||
for _, r := range arr {
|
for _, r := range arr {
|
||||||
if r.id == hookID {
|
if r.id == hookID {
|
||||||
@@ -1144,6 +1202,7 @@ func (m *Manager) RemovePacketHook(hookID string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Check outgoing hooks
|
||||||
for _, arr := range m.outgoingRules {
|
for _, arr := range m.outgoingRules {
|
||||||
for _, r := range arr {
|
for _, r := range arr {
|
||||||
if r.id == hookID {
|
if r.id == hookID {
|
||||||
|
|||||||
@@ -458,6 +458,31 @@ func TestPeerACLFiltering(t *testing.T) {
|
|||||||
ruleAction: fw.ActionDrop,
|
ruleAction: fw.ActionDrop,
|
||||||
shouldBeBlocked: true,
|
shouldBeBlocked: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Peer ACL - Drop rule should override accept all rule",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 22,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{22}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Peer ACL - Drop all traffic from specific IP",
|
||||||
|
srcIP: "100.10.0.99",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 80,
|
||||||
|
ruleIP: "100.10.0.99",
|
||||||
|
ruleProto: fw.ProtocolALL,
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Implicit DROP (no rules)", func(t *testing.T) {
|
t.Run("Implicit DROP (no rules)", func(t *testing.T) {
|
||||||
@@ -468,13 +493,11 @@ func TestPeerACLFiltering(t *testing.T) {
|
|||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
|
||||||
if tc.ruleAction == fw.ActionDrop {
|
if tc.ruleAction == fw.ActionDrop {
|
||||||
// add general accept rule to test drop rule
|
// add general accept rule for the same IP to test drop rule precedence
|
||||||
// TODO: this only works because 0.0.0.0 is tested last, we need to implement order
|
|
||||||
rules, err := manager.AddPeerFiltering(
|
rules, err := manager.AddPeerFiltering(
|
||||||
nil,
|
nil,
|
||||||
net.ParseIP("0.0.0.0"),
|
net.ParseIP(tc.ruleIP),
|
||||||
fw.ProtocolALL,
|
fw.ProtocolALL,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
|
|||||||
@@ -136,9 +136,22 @@ func TestManagerDeleteRule(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check rules exist in appropriate maps
|
||||||
for _, r := range rule2 {
|
for _, r := range rule2 {
|
||||||
if _, ok := m.incomingRules[ip][r.ID()]; !ok {
|
peerRule, ok := r.(*PeerRule)
|
||||||
t.Errorf("rule2 is not in the incomingRules")
|
if !ok {
|
||||||
|
t.Errorf("rule should be a PeerRule")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check if rule exists in deny or allow maps based on action
|
||||||
|
var found bool
|
||||||
|
if peerRule.drop {
|
||||||
|
_, found = m.incomingDenyRules[ip][r.ID()]
|
||||||
|
} else {
|
||||||
|
_, found = m.incomingRules[ip][r.ID()]
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("rule2 is not in the expected rules map")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,9 +163,22 @@ func TestManagerDeleteRule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check rules are removed from appropriate maps
|
||||||
for _, r := range rule2 {
|
for _, r := range rule2 {
|
||||||
if _, ok := m.incomingRules[ip][r.ID()]; ok {
|
peerRule, ok := r.(*PeerRule)
|
||||||
t.Errorf("rule2 is not in the incomingRules")
|
if !ok {
|
||||||
|
t.Errorf("rule should be a PeerRule")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Check if rule is removed from deny or allow maps based on action
|
||||||
|
var found bool
|
||||||
|
if peerRule.drop {
|
||||||
|
_, found = m.incomingDenyRules[ip][r.ID()]
|
||||||
|
} else {
|
||||||
|
_, found = m.incomingRules[ip][r.ID()]
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
t.Errorf("rule2 should be removed from the rules map")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,16 +222,17 @@ func TestAddUDPPacketHook(t *testing.T) {
|
|||||||
|
|
||||||
var addedRule PeerRule
|
var addedRule PeerRule
|
||||||
if tt.in {
|
if tt.in {
|
||||||
|
// Incoming UDP hooks are stored in allow rules map
|
||||||
if len(manager.incomingRules[tt.ip]) != 1 {
|
if len(manager.incomingRules[tt.ip]) != 1 {
|
||||||
t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules))
|
t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules[tt.ip]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, rule := range manager.incomingRules[tt.ip] {
|
for _, rule := range manager.incomingRules[tt.ip] {
|
||||||
addedRule = rule
|
addedRule = rule
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if len(manager.outgoingRules) != 1 {
|
if len(manager.outgoingRules[tt.ip]) != 1 {
|
||||||
t.Errorf("expected 1 outgoing rule, got %d", len(manager.outgoingRules))
|
t.Errorf("expected 1 outgoing rule, got %d", len(manager.outgoingRules[tt.ip]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, rule := range manager.outgoingRules[tt.ip] {
|
for _, rule := range manager.outgoingRules[tt.ip] {
|
||||||
@@ -261,8 +288,8 @@ func TestManagerReset(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.outgoingRules) != 0 || len(m.incomingRules) != 0 {
|
if len(m.outgoingRules) != 0 || len(m.incomingRules) != 0 || len(m.incomingDenyRules) != 0 {
|
||||||
t.Errorf("rules is not empty")
|
t.Errorf("rules are not empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ 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
|
||||||
@@ -94,7 +96,6 @@ 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 {
|
||||||
@@ -185,6 +186,15 @@ 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 {
|
||||||
@@ -239,6 +249,16 @@ 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")
|
||||||
@@ -260,6 +280,12 @@ 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++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,6 +309,10 @@ 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...)
|
||||||
@@ -390,4 +420,4 @@ func (l *Logger) Stop(ctx context.Context) error {
|
|||||||
case <-done:
|
case <-done:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ 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"
|
||||||
@@ -13,6 +15,21 @@ 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
|
||||||
@@ -52,6 +69,7 @@ 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
|
||||||
@@ -89,11 +107,21 @@ 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),
|
||||||
@@ -101,11 +129,13 @@ 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)
|
||||||
@@ -113,19 +143,25 @@ 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() || !translatedAddr.IsValid() {
|
if !originalAddr.IsValid() {
|
||||||
return fmt.Errorf("invalid IP addresses")
|
return fmt.Errorf("invalid original IP address")
|
||||||
|
}
|
||||||
|
if !translatedAddr.IsValid() {
|
||||||
|
return fmt.Errorf("invalid translated IP address")
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.localipmanager.IsLocalIP(translatedAddr) {
|
if m.localipmanager.IsLocalIP(translatedAddr) {
|
||||||
@@ -135,7 +171,6 @@ 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()
|
||||||
@@ -151,7 +186,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()
|
||||||
@@ -169,7 +204,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
|
||||||
@@ -181,7 +216,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
|
||||||
@@ -193,16 +228,12 @@ 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)
|
||||||
@@ -210,8 +241,8 @@ func (m *Manager) translateOutboundDNAT(packetData []byte, d *decoder) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.rewritePacketDestination(packetData, d, translatedIP); err != nil {
|
if err := m.rewritePacketIP(packetData, d, translatedIP, destinationIPOffset); 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,16 +250,12 @@ 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)
|
||||||
@@ -236,8 +263,8 @@ func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.rewritePacketSource(packetData, d, originalIP); err != nil {
|
if err := m.rewritePacketIP(packetData, d, originalIP, sourceIPOffset); 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,21 +272,21 @@ func (m *Manager) translateInboundReverse(packetData []byte, d *decoder) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// rewritePacketDestination replaces destination IP in the packet
|
// rewritePacketIP replaces an IP address (source or destination) in the packet and updates checksums.
|
||||||
func (m *Manager) rewritePacketDestination(packetData []byte, d *decoder, newIP netip.Addr) error {
|
func (m *Manager) rewritePacketIP(packetData []byte, d *decoder, newIP netip.Addr, ipOffset int) error {
|
||||||
if len(packetData) < 20 || d.decoded[0] != layers.LayerTypeIPv4 || !newIP.Is4() {
|
if !newIP.Is4() {
|
||||||
return ErrIPv4Only
|
return ErrIPv4Only
|
||||||
}
|
}
|
||||||
|
|
||||||
var oldDst [4]byte
|
var oldIP [4]byte
|
||||||
copy(oldDst[:], packetData[16:20])
|
copy(oldIP[:], packetData[ipOffset:ipOffset+4])
|
||||||
newDst := newIP.As4()
|
newIPBytes := newIP.As4()
|
||||||
|
|
||||||
copy(packetData[16:20], newDst[:])
|
copy(packetData[ipOffset:ipOffset+4], newIPBytes[:])
|
||||||
|
|
||||||
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 fmt.Errorf("invalid IP header length")
|
return errInvalidIPHeaderLength
|
||||||
}
|
}
|
||||||
|
|
||||||
binary.BigEndian.PutUint16(packetData[10:12], 0)
|
binary.BigEndian.PutUint16(packetData[10:12], 0)
|
||||||
@@ -269,44 +296,9 @@ func (m *Manager) rewritePacketDestination(packetData []byte, d *decoder, newIP
|
|||||||
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, oldDst[:], newDst[:])
|
m.updateTCPChecksum(packetData, ipHeaderLen, oldIP[:], newIPBytes[:])
|
||||||
case layers.LayerTypeUDP:
|
case layers.LayerTypeUDP:
|
||||||
m.updateUDPChecksum(packetData, ipHeaderLen, oldDst[:], newDst[:])
|
m.updateUDPChecksum(packetData, ipHeaderLen, oldIP[:], newIPBytes[:])
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -315,6 +307,7 @@ func (m *Manager) rewritePacketSource(packetData []byte, d *decoder, newIP netip
|
|||||||
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 {
|
||||||
@@ -327,6 +320,7 @@ 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 {
|
||||||
@@ -344,6 +338,7 @@ 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 {
|
||||||
@@ -356,7 +351,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)
|
||||||
|
|
||||||
@@ -391,7 +386,7 @@ func incrementalUpdate(oldChecksum uint16, oldBytes, newBytes []byte) uint16 {
|
|||||||
return ^uint16(sum)
|
return ^uint16(sum)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDNATRule adds a DNAT rule (delegates to native firewall for port forwarding)
|
// AddDNATRule adds outbound DNAT rule for forwarding external traffic to NetBird network.
|
||||||
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
|
||||||
@@ -399,10 +394,184 @@ func (m *Manager) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error)
|
|||||||
return m.nativeFirewall.AddDNATRule(rule)
|
return m.nativeFirewall.AddDNATRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteDNATRule deletes a DNAT rule (delegates to native firewall)
|
// DeleteDNATRule deletes outbound DNAT rule.
|
||||||
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -414,3 +414,127 @@ 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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -143,3 +144,111 @@ 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)
|
||||||
|
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)
|
||||||
|
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,25 +16,33 @@ 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",
|
||||||
StageConntrack: "Connection Tracking",
|
StageInboundPortDNAT: "Inbound Port DNAT",
|
||||||
StagePeerACL: "Peer ACL",
|
StageInbound1to1NAT: "Inbound 1:1 NAT",
|
||||||
StageRouting: "Routing",
|
StageConntrack: "Connection Tracking",
|
||||||
StageRouteACL: "Route ACL",
|
StagePeerACL: "Peer ACL",
|
||||||
StageForwarding: "Forwarding",
|
StageRouting: "Routing",
|
||||||
StageCompleted: "Completed",
|
StageRouteACL: "Route ACL",
|
||||||
|
StageForwarding: "Forwarding",
|
||||||
|
StageCompleted: "Completed",
|
||||||
|
StageOutbound1to1NAT: "Outbound 1:1 NAT",
|
||||||
|
StageOutboundPortReverse: "Outbound DNAT Reverse",
|
||||||
}[s]
|
}[s]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +269,10 @@ 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
|
||||||
}
|
}
|
||||||
@@ -314,7 +326,7 @@ func (m *Manager) buildConntrackStateMessage(d *decoder) string {
|
|||||||
func (m *Manager) handleLocalDelivery(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP netip.Addr) bool {
|
func (m *Manager) handleLocalDelivery(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP netip.Addr) bool {
|
||||||
trace.AddResult(StageRouting, "Packet destined for local delivery", true)
|
trace.AddResult(StageRouting, "Packet destined for local delivery", true)
|
||||||
|
|
||||||
ruleId, blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d)
|
ruleId, blocked := m.peerACLsBlock(srcIP, d, packetData)
|
||||||
|
|
||||||
strRuleId := "<no id>"
|
strRuleId := "<no id>"
|
||||||
if ruleId != nil {
|
if ruleId != nil {
|
||||||
@@ -400,7 +412,16 @@ 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 {
|
||||||
// will create or update the connection state
|
d := m.decoders.Get().(*decoder)
|
||||||
|
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)
|
||||||
@@ -409,3 +430,199 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -126,6 +128,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -153,6 +157,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -179,6 +185,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -204,6 +212,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageRouteACL,
|
StageRouteACL,
|
||||||
@@ -228,6 +238,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageRouteACL,
|
StageRouteACL,
|
||||||
@@ -246,6 +258,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageRouteACL,
|
StageRouteACL,
|
||||||
@@ -264,6 +278,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
@@ -287,6 +303,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
},
|
},
|
||||||
@@ -301,6 +319,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageOutbound1to1NAT,
|
||||||
|
StageOutboundPortReverse,
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
},
|
},
|
||||||
expectedAllow: true,
|
expectedAllow: true,
|
||||||
@@ -319,6 +339,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -340,6 +362,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -362,6 +386,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -382,6 +408,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageConntrack,
|
StageConntrack,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
@@ -406,6 +434,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
|
StageInboundPortDNAT,
|
||||||
|
StageInbound1to1NAT,
|
||||||
StageRouting,
|
StageRouting,
|
||||||
StagePeerACL,
|
StagePeerACL,
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
|
|||||||
@@ -4,15 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os/user"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -21,35 +15,9 @@ import (
|
|||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util/embeddedroots"
|
"github.com/netbirdio/netbird/util/embeddedroots"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func WithCustomDialer() grpc.DialOption {
|
// Backoff returns a backoff configuration for gRPC calls
|
||||||
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 {
|
|
||||||
log.Errorf("Failed to dial: %s", err)
|
|
||||||
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// grpcDialBackoff is the backoff mechanism for the grpc calls
|
|
||||||
func Backoff(ctx context.Context) backoff.BackOff {
|
func Backoff(ctx context.Context) backoff.BackOff {
|
||||||
b := backoff.NewExponentialBackOff()
|
b := backoff.NewExponentialBackOff()
|
||||||
b.MaxElapsedTime = 10 * time.Second
|
b.MaxElapsedTime = 10 * time.Second
|
||||||
@@ -57,9 +25,12 @@ func Backoff(ctx context.Context) backoff.BackOff {
|
|||||||
return backoff.WithContext(b, ctx)
|
return backoff.WithContext(b, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
|
// 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())
|
transportOption := grpc.WithTransportCredentials(insecure.NewCredentials())
|
||||||
if tlsEnabled {
|
// for js, the outer websocket layer takes care of tls
|
||||||
|
if tlsEnabled && runtime.GOOS != "js" {
|
||||||
certPool, err := x509.SystemCertPool()
|
certPool, err := x509.SystemCertPool()
|
||||||
if err != nil || certPool == nil {
|
if err != nil || certPool == nil {
|
||||||
log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
|
log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
|
||||||
@@ -71,14 +42,14 @@ func CreateConnection(addr string, tlsEnabled bool) (*grpc.ClientConn, error) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
connCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
connCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
conn, err := grpc.DialContext(
|
conn, err := grpc.DialContext(
|
||||||
connCtx,
|
connCtx,
|
||||||
addr,
|
addr,
|
||||||
transportOption,
|
transportOption,
|
||||||
WithCustomDialer(),
|
WithCustomDialer(tlsEnabled, component),
|
||||||
grpc.WithBlock(),
|
grpc.WithBlock(),
|
||||||
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||||
Time: 30 * time.Second,
|
Time: 30 * time.Second,
|
||||||
44
client/grpc/dialer_generic.go
Normal file
44
client/grpc/dialer_generic.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
//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(tlsEnabled bool, component 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 {
|
||||||
|
log.Errorf("Failed to dial: %s", err)
|
||||||
|
return nil, fmt.Errorf("nbnet.NewDialer().DialContext: %w", err)
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
13
client/grpc/dialer_js.go
Normal file
13
client/grpc/dialer_js.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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/util/net"
|
nbnet "github.com/netbirdio/netbird/client/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,5 +1,17 @@
|
|||||||
package bind
|
package bind
|
||||||
|
|
||||||
import wgConn "golang.zx2c4.com/wireguard/conn"
|
import (
|
||||||
|
"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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
7
client/iface/bind/error.go
Normal file
7
client/iface/bind/error.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUDPMUXNotSupported = fmt.Errorf("UDPMUX is not supported in WASM")
|
||||||
|
)
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
package bind
|
package bind
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -8,22 +11,18 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/pion/stun/v2"
|
"github.com/pion/stun/v3"
|
||||||
"github.com/pion/transport/v3"
|
"github.com/pion/transport/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/net/ipv4"
|
"golang.org/x/net/ipv4"
|
||||||
"golang.org/x/net/ipv6"
|
"golang.org/x/net/ipv6"
|
||||||
wgConn "golang.zx2c4.com/wireguard/conn"
|
wgConn "golang.zx2c4.com/wireguard/conn"
|
||||||
|
|
||||||
|
"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/util/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecvMessage struct {
|
|
||||||
Endpoint *Endpoint
|
|
||||||
Buffer []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
type receiverCreator struct {
|
type receiverCreator struct {
|
||||||
iceBind *ICEBind
|
iceBind *ICEBind
|
||||||
}
|
}
|
||||||
@@ -41,35 +40,38 @@ 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 FilterFn
|
filterFn udpmux.FilterFn
|
||||||
endpoints map[netip.Addr]net.Conn
|
address wgaddr.Address
|
||||||
endpointsMu sync.Mutex
|
mtu uint16
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
muUDPMux sync.Mutex
|
|
||||||
udpMux *UniversalUDPMuxDefault
|
|
||||||
address wgaddr.Address
|
|
||||||
activityRecorder *ActivityRecorder
|
activityRecorder *ActivityRecorder
|
||||||
|
|
||||||
|
muUDPMux sync.Mutex
|
||||||
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewICEBind(transportNet transport.Net, filterFn FilterFn, address wgaddr.Address) *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,
|
||||||
address: address,
|
|
||||||
activityRecorder: NewActivityRecorder(),
|
activityRecorder: NewActivityRecorder(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ func (s *ICEBind) ActivityRecorder() *ActivityRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetICEMux returns the ICE UDPMux that was created and used by ICEBind
|
// GetICEMux returns the ICE UDPMux that was created and used by ICEBind
|
||||||
func (s *ICEBind) GetICEMux() (*UniversalUDPMuxDefault, error) {
|
func (s *ICEBind) GetICEMux() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
s.muUDPMux.Lock()
|
s.muUDPMux.Lock()
|
||||||
defer s.muUDPMux.Unlock()
|
defer s.muUDPMux.Unlock()
|
||||||
if s.udpMux == nil {
|
if s.udpMux == nil {
|
||||||
@@ -132,6 +134,16 @@ 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()]
|
||||||
@@ -152,12 +164,13 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r
|
|||||||
s.muUDPMux.Lock()
|
s.muUDPMux.Lock()
|
||||||
defer s.muUDPMux.Unlock()
|
defer s.muUDPMux.Unlock()
|
||||||
|
|
||||||
s.udpMux = NewUniversalUDPMuxDefault(
|
s.udpMux = udpmux.NewUniversalUDPMuxDefault(
|
||||||
UniversalUDPMuxParams{
|
udpmux.UniversalUDPMuxParams{
|
||||||
UDPConn: nbnet.WrapPacketConn(conn),
|
UDPConn: nbnet.WrapPacketConn(conn),
|
||||||
Net: s.transportNet,
|
Net: s.transportNet,
|
||||||
FilterFn: s.filterFn,
|
FilterFn: s.filterFn,
|
||||||
WGAddress: s.address,
|
WGAddress: s.address,
|
||||||
|
MTU: s.mtu,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return func(bufs [][]byte, sizes []int, eps []wgConn.Endpoint) (n int, err error) {
|
return func(bufs [][]byte, sizes []int, eps []wgConn.Endpoint) (n int, err error) {
|
||||||
@@ -263,7 +276,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
|
||||||
}
|
}
|
||||||
|
|||||||
6
client/iface/bind/recv_msg.go
Normal file
6
client/iface/bind/recv_msg.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
type recvMessage struct {
|
||||||
|
Endpoint *Endpoint
|
||||||
|
Buffer []byte
|
||||||
|
}
|
||||||
125
client/iface/bind/relay_bind.go
Normal file
125
client/iface/bind/relay_bind.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
//go:build ios
|
|
||||||
|
|
||||||
package bind
|
|
||||||
|
|
||||||
func (m *UDPMuxDefault) notifyAddressRemoval(addr string) {
|
|
||||||
// iOS doesn't support nbnet hooks, so this is a no-op
|
|
||||||
}
|
|
||||||
9
client/iface/bufsize/bufsize.go
Normal file
9
client/iface/bufsize/bufsize.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package bufsize
|
||||||
|
|
||||||
|
const (
|
||||||
|
// WGBufferOverhead represents the additional buffer space needed beyond MTU
|
||||||
|
// for WireGuard packet encapsulation (WG header + UDP + IP + safety margin)
|
||||||
|
// Original hardcoded buffers were 1500, default MTU is 1280, so overhead = 220
|
||||||
|
// TODO: Calculate this properly based on actual protocol overhead instead of using hardcoded difference
|
||||||
|
WGBufferOverhead = 220
|
||||||
|
)
|
||||||
@@ -73,6 +73,44 @@ 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
|
//go:build linux || windows || freebsd || js || wasip1
|
||||||
|
|
||||||
package configurer
|
package configurer
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !windows
|
//go:build !windows && !js
|
||||||
|
|
||||||
package configurer
|
package configurer
|
||||||
|
|
||||||
|
|||||||
23
client/iface/configurer/uapi_js.go
Normal file
23
client/iface/configurer/uapi_js.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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,6 +106,67 @@ 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 {
|
||||||
@@ -394,6 +455,13 @@ func toLastHandshake(stringVar string) (time.Time, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, fmt.Errorf("parse handshake sec: %w", err)
|
return time.Time{}, fmt.Errorf("parse handshake sec: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If sec is 0 (Unix epoch), return zero time instead
|
||||||
|
// This indicates no handshake has occurred
|
||||||
|
if sec == 0 {
|
||||||
|
return time.Time{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
return time.Unix(sec, 0), nil
|
return time.Unix(sec, 0), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +470,7 @@ func toBytes(s string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFwmark() int {
|
func getFwmark() int {
|
||||||
if nbnet.AdvancedRouting() {
|
if nbnet.AdvancedRouting() && runtime.GOOS == "linux" {
|
||||||
return nbnet.ControlPlaneMark
|
return nbnet.ControlPlaneMark
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -7,19 +7,21 @@ import (
|
|||||||
|
|
||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WGTunDevice interface {
|
type WGTunDevice interface {
|
||||||
Create() (device.WGConfigurer, error)
|
Create() (device.WGConfigurer, error)
|
||||||
Up() (*bind.UniversalUDPMuxDefault, error)
|
Up() (*udpmux.UniversalUDPMuxDefault, error)
|
||||||
UpdateAddr(address wgaddr.Address) error
|
UpdateAddr(address wgaddr.Address) error
|
||||||
WgAddress() wgaddr.Address
|
WgAddress() wgaddr.Address
|
||||||
|
MTU() uint16
|
||||||
DeviceName() string
|
DeviceName() string
|
||||||
Close() error
|
Close() error
|
||||||
FilteredDevice() *device.FilteredDevice
|
FilteredDevice() *device.FilteredDevice
|
||||||
Device() *wgdevice.Device
|
Device() *wgdevice.Device
|
||||||
GetNet() *netstack.Net
|
GetNet() *netstack.Net
|
||||||
|
GetICEBind() device.EndpointManager
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ type WGTunDevice struct {
|
|||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
port int
|
port int
|
||||||
key string
|
key string
|
||||||
mtu int
|
mtu uint16
|
||||||
iceBind *bind.ICEBind
|
iceBind *bind.ICEBind
|
||||||
tunAdapter TunAdapter
|
tunAdapter TunAdapter
|
||||||
disableDNS bool
|
disableDNS bool
|
||||||
@@ -29,11 +30,11 @@ type WGTunDevice struct {
|
|||||||
name string
|
name string
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTunDevice(address wgaddr.Address, port int, key string, mtu int, 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,
|
||||||
@@ -58,7 +59,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string
|
|||||||
searchDomainsToString = ""
|
searchDomainsToString = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
fd, err := t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, dns, searchDomainsToString, routesString)
|
fd, err := t.tunAdapter.ConfigureInterface(t.address.String(), int(t.mtu), dns, searchDomainsToString, routesString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to create Android interface: %s", err)
|
log.Errorf("failed to create Android interface: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -88,7 +89,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string
|
|||||||
}
|
}
|
||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
func (t *WGTunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *WGTunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -137,6 +138,10 @@ func (t *WGTunDevice) WgAddress() wgaddr.Address {
|
|||||||
return t.address
|
return t.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *WGTunDevice) MTU() uint16 {
|
||||||
|
return t.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (t *WGTunDevice) FilteredDevice() *FilteredDevice {
|
func (t *WGTunDevice) FilteredDevice() *FilteredDevice {
|
||||||
return t.filteredDevice
|
return t.filteredDevice
|
||||||
}
|
}
|
||||||
@@ -145,6 +150,11 @@ 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, ";")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,16 +22,16 @@ type TunDevice struct {
|
|||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
port int
|
port int
|
||||||
key string
|
key string
|
||||||
mtu int
|
mtu uint16
|
||||||
iceBind *bind.ICEBind
|
iceBind *bind.ICEBind
|
||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu int, iceBind *bind.ICEBind) *TunDevice {
|
func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind) *TunDevice {
|
||||||
return &TunDevice{
|
return &TunDevice{
|
||||||
name: name,
|
name: name,
|
||||||
address: address,
|
address: address,
|
||||||
@@ -42,7 +43,7 @@ func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunDevice) Create() (WGConfigurer, error) {
|
func (t *TunDevice) Create() (WGConfigurer, error) {
|
||||||
tunDevice, err := tun.CreateTUN(t.name, t.mtu)
|
tunDevice, err := tun.CreateTUN(t.name, int(t.mtu))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating tun device: %s", err)
|
return nil, fmt.Errorf("error creating tun device: %s", err)
|
||||||
}
|
}
|
||||||
@@ -71,7 +72,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -111,6 +112,10 @@ func (t *TunDevice) WgAddress() wgaddr.Address {
|
|||||||
return t.address
|
return t.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TunDevice) MTU() uint16 {
|
||||||
|
return t.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TunDevice) DeviceName() string {
|
func (t *TunDevice) DeviceName() string {
|
||||||
return t.name
|
return t.name
|
||||||
}
|
}
|
||||||
@@ -149,3 +154,8 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,21 +23,23 @@ type TunDevice struct {
|
|||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
port int
|
port int
|
||||||
key string
|
key string
|
||||||
|
mtu uint16
|
||||||
iceBind *bind.ICEBind
|
iceBind *bind.ICEBind
|
||||||
tunFd int
|
tunFd int
|
||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTunDevice(name string, address wgaddr.Address, port int, key string, iceBind *bind.ICEBind, tunFd int) *TunDevice {
|
func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind, tunFd int) *TunDevice {
|
||||||
return &TunDevice{
|
return &TunDevice{
|
||||||
name: name,
|
name: name,
|
||||||
address: address,
|
address: address,
|
||||||
port: port,
|
port: port,
|
||||||
key: key,
|
key: key,
|
||||||
|
mtu: mtu,
|
||||||
iceBind: iceBind,
|
iceBind: iceBind,
|
||||||
tunFd: tunFd,
|
tunFd: tunFd,
|
||||||
}
|
}
|
||||||
@@ -81,7 +84,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -125,6 +128,10 @@ func (t *TunDevice) WgAddress() wgaddr.Address {
|
|||||||
return t.address
|
return t.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TunDevice) MTU() uint16 {
|
||||||
|
return t.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TunDevice) UpdateAddr(_ wgaddr.Address) error {
|
func (t *TunDevice) UpdateAddr(_ wgaddr.Address) error {
|
||||||
// todo implement
|
// todo implement
|
||||||
return nil
|
return nil
|
||||||
@@ -137,3 +144,8 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/device"
|
"golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"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/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 {
|
||||||
@@ -24,19 +24,19 @@ type TunKernelDevice struct {
|
|||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
wgPort int
|
wgPort int
|
||||||
key string
|
key string
|
||||||
mtu int
|
mtu uint16
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
transportNet transport.Net
|
transportNet transport.Net
|
||||||
|
|
||||||
link *wgLink
|
link *wgLink
|
||||||
udpMuxConn net.PacketConn
|
udpMuxConn net.PacketConn
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
|
|
||||||
filterFn bind.FilterFn
|
filterFn udpmux.FilterFn
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu int, transportNet transport.Net) *TunKernelDevice {
|
func NewKernelDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, transportNet transport.Net) *TunKernelDevice {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &TunKernelDevice{
|
return &TunKernelDevice{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -66,7 +66,7 @@ func (t *TunKernelDevice) Create() (WGConfigurer, error) {
|
|||||||
// TODO: do a MTU discovery
|
// TODO: do a MTU discovery
|
||||||
log.Debugf("setting MTU: %d interface: %s", t.mtu, t.name)
|
log.Debugf("setting MTU: %d interface: %s", t.mtu, t.name)
|
||||||
|
|
||||||
if err := link.setMTU(t.mtu); err != nil {
|
if err := link.setMTU(int(t.mtu)); err != nil {
|
||||||
return nil, fmt.Errorf("set mtu: %w", err)
|
return nil, fmt.Errorf("set mtu: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ func (t *TunKernelDevice) Create() (WGConfigurer, error) {
|
|||||||
return configurer, nil
|
return configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunKernelDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
if t.udpMux != nil {
|
if t.udpMux != nil {
|
||||||
return t.udpMux, nil
|
return t.udpMux, nil
|
||||||
}
|
}
|
||||||
@@ -96,23 +96,19 @@ func (t *TunKernelDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
rawSock, err := sharedsock.Listen(t.wgPort, sharedsock.NewIncomingSTUNFilter())
|
rawSock, err := sharedsock.Listen(t.wgPort, sharedsock.NewIncomingSTUNFilter(), t.mtu)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var udpConn net.PacketConn = rawSock
|
bindParams := udpmux.UniversalUDPMuxParams{
|
||||||
if !nbnet.AdvancedRouting() {
|
UDPConn: nbnet.WrapPacketConn(rawSock),
|
||||||
udpConn = nbnet.WrapPacketConn(rawSock)
|
|
||||||
}
|
|
||||||
|
|
||||||
bindParams := bind.UniversalUDPMuxParams{
|
|
||||||
UDPConn: udpConn,
|
|
||||||
Net: t.transportNet,
|
Net: t.transportNet,
|
||||||
FilterFn: t.filterFn,
|
FilterFn: t.filterFn,
|
||||||
WGAddress: t.address,
|
WGAddress: t.address,
|
||||||
|
MTU: t.mtu,
|
||||||
}
|
}
|
||||||
mux := bind.NewUniversalUDPMuxDefault(bindParams)
|
mux := udpmux.NewUniversalUDPMuxDefault(bindParams)
|
||||||
go mux.ReadFromConn(t.ctx)
|
go mux.ReadFromConn(t.ctx)
|
||||||
t.udpMuxConn = rawSock
|
t.udpMuxConn = rawSock
|
||||||
t.udpMux = mux
|
t.udpMux = mux
|
||||||
@@ -158,6 +154,10 @@ func (t *TunKernelDevice) WgAddress() wgaddr.Address {
|
|||||||
return t.address
|
return t.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TunKernelDevice) MTU() uint16 {
|
||||||
|
return t.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TunKernelDevice) DeviceName() string {
|
func (t *TunKernelDevice) DeviceName() string {
|
||||||
return t.name
|
return t.name
|
||||||
}
|
}
|
||||||
@@ -179,3 +179,8 @@ 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,41 +1,48 @@
|
|||||||
//go:build !android
|
|
||||||
// +build !android
|
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/client/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
|
||||||
port int
|
port int
|
||||||
key string
|
key string
|
||||||
mtu int
|
mtu uint16
|
||||||
listenAddress string
|
listenAddress string
|
||||||
iceBind *bind.ICEBind
|
bind Bind
|
||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
nsTun *nbnetstack.NetStackTun
|
nsTun *nbnetstack.NetStackTun
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
|
|
||||||
net *netstack.Net
|
net *netstack.Net
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetstackDevice(name string, address wgaddr.Address, wgPort int, key string, mtu int, iceBind *bind.ICEBind, listenAddress string) *TunNetstackDevice {
|
func NewNetstackDevice(name string, address wgaddr.Address, wgPort int, key string, mtu uint16, bind Bind, listenAddress string) *TunNetstackDevice {
|
||||||
return &TunNetstackDevice{
|
return &TunNetstackDevice{
|
||||||
name: name,
|
name: name,
|
||||||
address: address,
|
address: address,
|
||||||
@@ -43,11 +50,11 @@ func NewNetstackDevice(name string, address wgaddr.Address, wgPort int, key stri
|
|||||||
key: key,
|
key: key,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
listenAddress: listenAddress,
|
listenAddress: listenAddress,
|
||||||
iceBind: iceBind,
|
bind: bind,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
|
func (t *TunNetstackDevice) create() (WGConfigurer, error) {
|
||||||
log.Info("create nbnetstack tun interface")
|
log.Info("create nbnetstack tun interface")
|
||||||
|
|
||||||
// TODO: get from service listener runtime IP
|
// TODO: get from service listener runtime IP
|
||||||
@@ -57,7 +64,7 @@ func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("netstack using address: %s", t.address.IP)
|
log.Debugf("netstack using address: %s", t.address.IP)
|
||||||
t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, t.mtu)
|
t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, int(t.mtu))
|
||||||
log.Debugf("netstack using dns address: %s", dnsAddr)
|
log.Debugf("netstack using dns address: %s", dnsAddr)
|
||||||
tunIface, net, err := t.nsTun.Create()
|
tunIface, net, err := t.nsTun.Create()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -68,11 +75,11 @@ func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
|
|||||||
|
|
||||||
t.device = device.NewDevice(
|
t.device = device.NewDevice(
|
||||||
t.filteredDevice,
|
t.filteredDevice,
|
||||||
t.iceBind,
|
t.bind,
|
||||||
device.NewLogger(wgLogLevel(), "[netbird] "),
|
device.NewLogger(wgLogLevel(), "[netbird] "),
|
||||||
)
|
)
|
||||||
|
|
||||||
t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.iceBind.ActivityRecorder())
|
t.configurer = configurer.NewUSPConfigurer(t.device, t.name, t.bind.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()
|
||||||
@@ -83,7 +90,7 @@ func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunNetstackDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunNetstackDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
if t.device == nil {
|
if t.device == nil {
|
||||||
return nil, fmt.Errorf("device is not ready yet")
|
return nil, fmt.Errorf("device is not ready yet")
|
||||||
}
|
}
|
||||||
@@ -93,11 +100,15 @@ func (t *TunNetstackDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
udpMux, err := t.iceBind.GetICEMux()
|
udpMux, err := t.bind.GetICEMux()
|
||||||
if err != nil {
|
if err != nil && !errors.Is(err, bind.ErrUDPMUXNotSupported) {
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -125,6 +136,10 @@ func (t *TunNetstackDevice) WgAddress() wgaddr.Address {
|
|||||||
return t.address
|
return t.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TunNetstackDevice) MTU() uint16 {
|
||||||
|
return t.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TunNetstackDevice) DeviceName() string {
|
func (t *TunNetstackDevice) DeviceName() string {
|
||||||
return t.name
|
return t.name
|
||||||
}
|
}
|
||||||
@@ -141,3 +156,8 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
7
client/iface/device/device_netstack_android.go
Normal file
7
client/iface/device/device_netstack_android.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package device
|
||||||
|
|
||||||
|
func (t *TunNetstackDevice) Create(routes []string, dns string, searchDomains []string) (WGConfigurer, error) {
|
||||||
|
return t.create()
|
||||||
|
}
|
||||||
7
client/iface/device/device_netstack_generic.go
Normal file
7
client/iface/device/device_netstack_generic.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package device
|
||||||
|
|
||||||
|
func (t *TunNetstackDevice) Create() (WGConfigurer, error) {
|
||||||
|
return t.create()
|
||||||
|
}
|
||||||
27
client/iface/device/device_netstack_test.go
Normal file
27
client/iface/device/device_netstack_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,16 +21,16 @@ type USPDevice struct {
|
|||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
port int
|
port int
|
||||||
key string
|
key string
|
||||||
mtu int
|
mtu uint16
|
||||||
iceBind *bind.ICEBind
|
iceBind *bind.ICEBind
|
||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUSPDevice(name string, address wgaddr.Address, port int, key string, mtu int, iceBind *bind.ICEBind) *USPDevice {
|
func NewUSPDevice(name string, address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind) *USPDevice {
|
||||||
log.Infof("using userspace bind mode")
|
log.Infof("using userspace bind mode")
|
||||||
|
|
||||||
return &USPDevice{
|
return &USPDevice{
|
||||||
@@ -44,9 +45,9 @@ func NewUSPDevice(name string, address wgaddr.Address, port int, key string, mtu
|
|||||||
|
|
||||||
func (t *USPDevice) Create() (WGConfigurer, error) {
|
func (t *USPDevice) Create() (WGConfigurer, error) {
|
||||||
log.Info("create tun interface")
|
log.Info("create tun interface")
|
||||||
tunIface, err := tun.CreateTUN(t.name, t.mtu)
|
tunIface, err := tun.CreateTUN(t.name, int(t.mtu))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to create tun interface (%s, %d): %s", t.name, t.mtu, err)
|
log.Debugf("failed to create tun interface (%s, %d): %s", t.name, int(t.mtu), err)
|
||||||
return nil, fmt.Errorf("error creating tun device: %s", err)
|
return nil, fmt.Errorf("error creating tun device: %s", err)
|
||||||
}
|
}
|
||||||
t.filteredDevice = newDeviceFilter(tunIface)
|
t.filteredDevice = newDeviceFilter(tunIface)
|
||||||
@@ -74,7 +75,7 @@ func (t *USPDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *USPDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *USPDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
if t.device == nil {
|
if t.device == nil {
|
||||||
return nil, fmt.Errorf("device is not ready yet")
|
return nil, fmt.Errorf("device is not ready yet")
|
||||||
}
|
}
|
||||||
@@ -118,6 +119,10 @@ func (t *USPDevice) WgAddress() wgaddr.Address {
|
|||||||
return t.address
|
return t.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *USPDevice) MTU() uint16 {
|
||||||
|
return t.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (t *USPDevice) DeviceName() string {
|
func (t *USPDevice) DeviceName() string {
|
||||||
return t.name
|
return t.name
|
||||||
}
|
}
|
||||||
@@ -141,3 +146,8 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,17 +24,17 @@ type TunDevice struct {
|
|||||||
address wgaddr.Address
|
address wgaddr.Address
|
||||||
port int
|
port int
|
||||||
key string
|
key string
|
||||||
mtu int
|
mtu uint16
|
||||||
iceBind *bind.ICEBind
|
iceBind *bind.ICEBind
|
||||||
|
|
||||||
device *device.Device
|
device *device.Device
|
||||||
nativeTunDevice *tun.NativeTun
|
nativeTunDevice *tun.NativeTun
|
||||||
filteredDevice *FilteredDevice
|
filteredDevice *FilteredDevice
|
||||||
udpMux *bind.UniversalUDPMuxDefault
|
udpMux *udpmux.UniversalUDPMuxDefault
|
||||||
configurer WGConfigurer
|
configurer WGConfigurer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu int, iceBind *bind.ICEBind) *TunDevice {
|
func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind) *TunDevice {
|
||||||
return &TunDevice{
|
return &TunDevice{
|
||||||
name: name,
|
name: name,
|
||||||
address: address,
|
address: address,
|
||||||
@@ -59,7 +60,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
log.Info("create tun interface")
|
log.Info("create tun interface")
|
||||||
tunDevice, err := tun.CreateTUNWithRequestedGUID(t.name, &guid, t.mtu)
|
tunDevice, err := tun.CreateTUNWithRequestedGUID(t.name, &guid, int(t.mtu))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating tun device: %s", err)
|
return nil, fmt.Errorf("error creating tun device: %s", err)
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
return t.configurer, nil
|
return t.configurer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TunDevice) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
err := t.device.Up()
|
err := t.device.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -144,6 +145,10 @@ func (t *TunDevice) WgAddress() wgaddr.Address {
|
|||||||
return t.address
|
return t.address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *TunDevice) MTU() uint16 {
|
||||||
|
return t.mtu
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TunDevice) DeviceName() string {
|
func (t *TunDevice) DeviceName() string {
|
||||||
return t.name
|
return t.name
|
||||||
}
|
}
|
||||||
@@ -180,3 +185,8 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
13
client/iface/device/endpoint_manager.go
Normal file
13
client/iface/device/endpoint_manager.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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,4 +21,5 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,21 @@ import (
|
|||||||
|
|
||||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WGTunDevice interface {
|
type WGTunDevice interface {
|
||||||
Create(routes []string, dns string, searchDomains []string) (device.WGConfigurer, error)
|
Create(routes []string, dns string, searchDomains []string) (device.WGConfigurer, error)
|
||||||
Up() (*bind.UniversalUDPMuxDefault, error)
|
Up() (*udpmux.UniversalUDPMuxDefault, error)
|
||||||
UpdateAddr(address wgaddr.Address) error
|
UpdateAddr(address wgaddr.Address) error
|
||||||
WgAddress() wgaddr.Address
|
WgAddress() wgaddr.Address
|
||||||
|
MTU() uint16
|
||||||
DeviceName() string
|
DeviceName() string
|
||||||
Close() error
|
Close() error
|
||||||
FilteredDevice() *device.FilteredDevice
|
FilteredDevice() *device.FilteredDevice
|
||||||
Device() *wgdevice.Device
|
Device() *wgdevice.Device
|
||||||
GetNet() *netstack.Net
|
GetNet() *netstack.Net
|
||||||
|
GetICEBind() device.EndpointManager
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import (
|
|||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/errors"
|
"github.com/netbirdio/netbird/client/errors"
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
||||||
"github.com/netbirdio/netbird/monotime"
|
"github.com/netbirdio/netbird/monotime"
|
||||||
@@ -26,6 +26,8 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultMTU = 1280
|
DefaultMTU = 1280
|
||||||
|
MinMTU = 576
|
||||||
|
MaxMTU = 8192
|
||||||
DefaultWgPort = 51820
|
DefaultWgPort = 51820
|
||||||
WgInterfaceDefault = configurer.WgInterfaceDefault
|
WgInterfaceDefault = configurer.WgInterfaceDefault
|
||||||
)
|
)
|
||||||
@@ -35,6 +37,17 @@ var (
|
|||||||
ErrIfaceNotFound = fmt.Errorf("wireguard interface not found")
|
ErrIfaceNotFound = fmt.Errorf("wireguard interface not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ValidateMTU validates that MTU is within acceptable range
|
||||||
|
func ValidateMTU(mtu uint16) error {
|
||||||
|
if mtu < MinMTU {
|
||||||
|
return fmt.Errorf("MTU %d below minimum (%d bytes)", mtu, MinMTU)
|
||||||
|
}
|
||||||
|
if mtu > MaxMTU {
|
||||||
|
return fmt.Errorf("MTU %d exceeds maximum supported size (%d bytes)", mtu, MaxMTU)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type wgProxyFactory interface {
|
type wgProxyFactory interface {
|
||||||
GetProxy() wgproxy.Proxy
|
GetProxy() wgproxy.Proxy
|
||||||
Free() error
|
Free() error
|
||||||
@@ -45,10 +58,10 @@ type WGIFaceOpts struct {
|
|||||||
Address string
|
Address string
|
||||||
WGPort int
|
WGPort int
|
||||||
WGPrivKey string
|
WGPrivKey string
|
||||||
MTU int
|
MTU uint16
|
||||||
MobileArgs *device.MobileIFaceArguments
|
MobileArgs *device.MobileIFaceArguments
|
||||||
TransportNet transport.Net
|
TransportNet transport.Net
|
||||||
FilterFn bind.FilterFn
|
FilterFn udpmux.FilterFn
|
||||||
DisableDNS bool
|
DisableDNS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +80,17 @@ 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
|
||||||
@@ -82,6 +106,10 @@ func (w *WGIface) Address() wgaddr.Address {
|
|||||||
return w.tun.WgAddress()
|
return w.tun.WgAddress()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *WGIface) MTU() uint16 {
|
||||||
|
return w.tun.MTU()
|
||||||
|
}
|
||||||
|
|
||||||
// ToInterface returns the net.Interface for the Wireguard interface
|
// ToInterface returns the net.Interface for the Wireguard interface
|
||||||
func (r *WGIface) ToInterface() *net.Interface {
|
func (r *WGIface) ToInterface() *net.Interface {
|
||||||
name := r.tun.DeviceName()
|
name := r.tun.DeviceName()
|
||||||
@@ -97,7 +125,7 @@ func (r *WGIface) ToInterface() *net.Interface {
|
|||||||
|
|
||||||
// Up configures a Wireguard interface
|
// Up configures a Wireguard interface
|
||||||
// The interface must exist before calling this method (e.g. call interface.Create() before)
|
// The interface must exist before calling this method (e.g. call interface.Create() before)
|
||||||
func (w *WGIface) Up() (*bind.UniversalUDPMuxDefault, error) {
|
func (w *WGIface) Up() (*udpmux.UniversalUDPMuxDefault, error) {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
@@ -131,6 +159,17 @@ 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()
|
||||||
|
|||||||
6
client/iface/iface_destroy_js.go
Normal file
6
client/iface/iface_destroy_js.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package iface
|
||||||
|
|
||||||
|
// Destroy is a no-op on WASM
|
||||||
|
func (w *WGIface) Destroy() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package iface
|
|||||||
import (
|
import (
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
||||||
)
|
)
|
||||||
@@ -14,12 +15,21 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
|
|
||||||
|
if netstack.IsEnabled() {
|
||||||
|
wgIFace := &WGIface{
|
||||||
|
userspaceBind: true,
|
||||||
|
tun: device.NewNetstackDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr()),
|
||||||
|
wgProxyFactory: wgproxy.NewUSPFactory(iceBind, opts.MTU),
|
||||||
|
}
|
||||||
|
return wgIFace, nil
|
||||||
|
}
|
||||||
|
|
||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
tun: device.NewTunDevice(wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunAdapter, opts.DisableDNS),
|
tun: device.NewTunDevice(wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunAdapter, opts.DisableDNS),
|
||||||
wgProxyFactory: wgproxy.NewUSPFactory(iceBind),
|
wgProxyFactory: wgproxy.NewUSPFactory(iceBind, opts.MTU),
|
||||||
}
|
}
|
||||||
return wgIFace, nil
|
return wgIFace, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
|
|
||||||
var tun WGTunDevice
|
var tun WGTunDevice
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
@@ -29,7 +29,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
tun: tun,
|
tun: tun,
|
||||||
wgProxyFactory: wgproxy.NewUSPFactory(iceBind),
|
wgProxyFactory: wgproxy.NewUSPFactory(iceBind, opts.MTU),
|
||||||
}
|
}
|
||||||
return wgIFace, nil
|
return wgIFace, nil
|
||||||
}
|
}
|
||||||
|
|||||||
41
client/iface/iface_new_freebsd.go
Normal file
41
client/iface/iface_new_freebsd.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build freebsd
|
||||||
|
|
||||||
|
package iface
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewWGIFace Creates a new WireGuard interface instance
|
||||||
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
|
wgAddress, err := wgaddr.ParseWGAddress(opts.Address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wgIFace := &WGIface{}
|
||||||
|
|
||||||
|
if netstack.IsEnabled() {
|
||||||
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
|
wgIFace.tun = device.NewNetstackDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr())
|
||||||
|
wgIFace.userspaceBind = true
|
||||||
|
wgIFace.wgProxyFactory = wgproxy.NewUSPFactory(iceBind, opts.MTU)
|
||||||
|
return wgIFace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.ModuleTunIsLoaded() {
|
||||||
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
|
wgIFace.tun = device.NewUSPDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind)
|
||||||
|
wgIFace.userspaceBind = true
|
||||||
|
wgIFace.wgProxyFactory = wgproxy.NewUSPFactory(iceBind, opts.MTU)
|
||||||
|
return wgIFace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("couldn't check or load tun module")
|
||||||
|
}
|
||||||
@@ -16,12 +16,12 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
|
|
||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
tun: device.NewTunDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, iceBind, opts.MobileArgs.TunFd),
|
tun: device.NewTunDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, opts.MobileArgs.TunFd),
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
wgProxyFactory: wgproxy.NewUSPFactory(iceBind),
|
wgProxyFactory: wgproxy.NewUSPFactory(iceBind, opts.MTU),
|
||||||
}
|
}
|
||||||
return wgIFace, nil
|
return wgIFace, nil
|
||||||
}
|
}
|
||||||
|
|||||||
27
client/iface/iface_new_js.go
Normal file
27
client/iface/iface_new_js.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package iface
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/wgproxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewWGIFace creates a new WireGuard interface for WASM (always uses netstack mode)
|
||||||
|
func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
||||||
|
wgAddress, err := wgaddr.ParseWGAddress(opts.Address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
relayBind := bind.NewRelayBindJS()
|
||||||
|
|
||||||
|
wgIface := &WGIface{
|
||||||
|
tun: device.NewNetstackDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, relayBind, netstack.ListenAddr()),
|
||||||
|
userspaceBind: true,
|
||||||
|
wgProxyFactory: wgproxy.NewUSPFactory(relayBind, opts.MTU),
|
||||||
|
}
|
||||||
|
|
||||||
|
return wgIface, nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build (linux && !android) || freebsd
|
//go:build linux && !android
|
||||||
|
|
||||||
package iface
|
package iface
|
||||||
|
|
||||||
@@ -22,23 +22,23 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
wgIFace := &WGIface{}
|
wgIFace := &WGIface{}
|
||||||
|
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
wgIFace.tun = device.NewNetstackDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr())
|
wgIFace.tun = device.NewNetstackDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind, netstack.ListenAddr())
|
||||||
wgIFace.userspaceBind = true
|
wgIFace.userspaceBind = true
|
||||||
wgIFace.wgProxyFactory = wgproxy.NewUSPFactory(iceBind)
|
wgIFace.wgProxyFactory = wgproxy.NewUSPFactory(iceBind, opts.MTU)
|
||||||
return wgIFace, nil
|
return wgIFace, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if device.WireGuardModuleIsLoaded() {
|
if device.WireGuardModuleIsLoaded() {
|
||||||
wgIFace.tun = device.NewKernelDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, opts.TransportNet)
|
wgIFace.tun = device.NewKernelDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, opts.TransportNet)
|
||||||
wgIFace.wgProxyFactory = wgproxy.NewKernelFactory(opts.WGPort)
|
wgIFace.wgProxyFactory = wgproxy.NewKernelFactory(opts.WGPort, opts.MTU)
|
||||||
return wgIFace, nil
|
return wgIFace, nil
|
||||||
}
|
}
|
||||||
if device.ModuleTunIsLoaded() {
|
if device.ModuleTunIsLoaded() {
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
wgIFace.tun = device.NewUSPDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind)
|
wgIFace.tun = device.NewUSPDevice(opts.IFaceName, wgAddress, opts.WGPort, opts.WGPrivKey, opts.MTU, iceBind)
|
||||||
wgIFace.userspaceBind = true
|
wgIFace.userspaceBind = true
|
||||||
wgIFace.wgProxyFactory = wgproxy.NewUSPFactory(iceBind)
|
wgIFace.wgProxyFactory = wgproxy.NewUSPFactory(iceBind, opts.MTU)
|
||||||
return wgIFace, nil
|
return wgIFace, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress)
|
iceBind := bind.NewICEBind(opts.TransportNet, opts.FilterFn, wgAddress, opts.MTU)
|
||||||
|
|
||||||
var tun WGTunDevice
|
var tun WGTunDevice
|
||||||
if netstack.IsEnabled() {
|
if netstack.IsEnabled() {
|
||||||
@@ -26,7 +26,7 @@ func NewWGIFace(opts WGIFaceOpts) (*WGIface, error) {
|
|||||||
wgIFace := &WGIface{
|
wgIFace := &WGIface{
|
||||||
userspaceBind: true,
|
userspaceBind: true,
|
||||||
tun: tun,
|
tun: tun,
|
||||||
wgProxyFactory: wgproxy.NewUSPFactory(iceBind),
|
wgProxyFactory: wgproxy.NewUSPFactory(iceBind, opts.MTU),
|
||||||
}
|
}
|
||||||
return wgIFace, nil
|
return wgIFace, nil
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
package netstack
|
package netstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
12
client/iface/netstack/env_js.go
Normal file
12
client/iface/netstack/env_js.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package netstack
|
||||||
|
|
||||||
|
const EnvUseNetstackMode = "NB_USE_NETSTACK_MODE"
|
||||||
|
|
||||||
|
// IsEnabled always returns true for js since it's the only mode available
|
||||||
|
func IsEnabled() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListenAddr() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package bind
|
package udpmux
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Most of this code was copied from https://github.com/pion/ice and modified to fulfill NetBird's requirements
|
Most of this code was copied from https://github.com/pion/ice and modified to fulfill NetBird's requirements
|
||||||
@@ -16,11 +16,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type udpMuxedConnParams struct {
|
type udpMuxedConnParams struct {
|
||||||
Mux *UDPMuxDefault
|
Mux *SingleSocketUDPMux
|
||||||
AddrPool *sync.Pool
|
AddrPool *sync.Pool
|
||||||
Key string
|
Key string
|
||||||
LocalAddr net.Addr
|
LocalAddr net.Addr
|
||||||
Logger logging.LeveledLogger
|
Logger logging.LeveledLogger
|
||||||
|
CandidateID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag
|
// udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag
|
||||||
@@ -119,6 +120,10 @@ func (c *udpMuxedConn) Close() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *udpMuxedConn) GetCandidateID() string {
|
||||||
|
return c.params.CandidateID
|
||||||
|
}
|
||||||
|
|
||||||
func (c *udpMuxedConn) isClosed() bool {
|
func (c *udpMuxedConn) isClosed() bool {
|
||||||
select {
|
select {
|
||||||
case <-c.closedChan:
|
case <-c.closedChan:
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user