mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-22 18:26:41 +00:00
Compare commits
108 Commits
v0.42.0
...
add-ns-pun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5eddf70b | ||
|
|
0f050e5fe1 | ||
|
|
0f7c7f1da2 | ||
|
|
b56f61bf1b | ||
|
|
64f111923e | ||
|
|
122a89c02b | ||
|
|
c6cceba381 | ||
|
|
6c0cdb6ed1 | ||
|
|
84354951d3 | ||
|
|
55957a1960 | ||
|
|
df82a45d99 | ||
|
|
9424b88db2 | ||
|
|
609654eee7 | ||
|
|
b604c66140 | ||
|
|
ea4d13e96d | ||
|
|
87148c503f | ||
|
|
0cd36baf67 | ||
|
|
06980e7fa0 | ||
|
|
1ce4ee0cef | ||
|
|
f367925496 | ||
|
|
616b19c064 | ||
|
|
af27aaf9af | ||
|
|
35287f8241 | ||
|
|
07b220d91b | ||
|
|
41cd4952f1 | ||
|
|
f16f0c7831 | ||
|
|
aa07b3b87b | ||
|
|
2bef214cc0 | ||
|
|
cfb2d82352 | ||
|
|
684501fd35 | ||
|
|
0492c1724a | ||
|
|
6f436e57b5 | ||
|
|
a0d28f9851 | ||
|
|
cdd27a9fe5 | ||
|
|
5523040acd | ||
|
|
670446d42e | ||
|
|
273160c682 | ||
|
|
5bed6777d5 | ||
|
|
a0482ebc7b | ||
|
|
1d6c360aec | ||
|
|
f04e7c3f06 | ||
|
|
2a89d6e47a | ||
|
|
3d89cd43c2 | ||
|
|
0eeda712d0 | ||
|
|
3e3268db5f | ||
|
|
31f0879e71 | ||
|
|
f25b5bb987 | ||
|
|
24f932b2ce | ||
|
|
c03435061c | ||
|
|
8e948739f1 | ||
|
|
9b53cad752 | ||
|
|
802a18167c | ||
|
|
e9108ffe6c | ||
|
|
e806d9de38 | ||
|
|
daa8380df9 | ||
|
|
4785f23fc4 | ||
|
|
1d4cfb83e7 | ||
|
|
207fa059d2 | ||
|
|
cbcdad7814 | ||
|
|
701c13807a | ||
|
|
99f8dc7748 | ||
|
|
f1de8e6eb0 | ||
|
|
b2a10780af | ||
|
|
43ae79d848 | ||
|
|
e520b64c6d | ||
|
|
92c91bbdd8 | ||
|
|
adf494e1ac | ||
|
|
2158461121 | ||
|
|
0cd4b601c3 | ||
|
|
ee1cec47b3 | ||
|
|
efb0edfc4c | ||
|
|
20f59ddecb | ||
|
|
2f34e984b0 | ||
|
|
d5b52e86b6 | ||
|
|
cad2fe1f39 | ||
|
|
fcd2c15a37 | ||
|
|
ebda0fc538 | ||
|
|
ac135ab11d | ||
|
|
25faf9283d | ||
|
|
59faaa99f6 | ||
|
|
9762b39f29 | ||
|
|
ffdd115ded | ||
|
|
055df9854c | ||
|
|
12f883badf | ||
|
|
2abb92b0d4 | ||
|
|
01c3719c5d | ||
|
|
7b64953eed | ||
|
|
9bc7d788f0 | ||
|
|
b5419ef11a | ||
|
|
d5081cef90 | ||
|
|
488e619ec7 | ||
|
|
d2b42c8f68 | ||
|
|
2f44fe2e23 | ||
|
|
d8dc107bee | ||
|
|
3fa915e271 | ||
|
|
47c3afe561 | ||
|
|
84bfecdd37 | ||
|
|
3cf87b6846 | ||
|
|
4fe4c2054d | ||
|
|
38ada44a0e | ||
|
|
dbf81a145e | ||
|
|
39483f8ca8 | ||
|
|
c0eaea938e | ||
|
|
ef8b8a2891 | ||
|
|
2817f62c13 | ||
|
|
4a9049566a | ||
|
|
85f92f8321 | ||
|
|
714beb6e3b |
15
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
15
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
@@ -37,16 +37,21 @@ If yes, which one?
|
|||||||
|
|
||||||
**Debug output**
|
**Debug output**
|
||||||
|
|
||||||
To help us resolve the problem, please attach the following debug output
|
To help us resolve the problem, please attach the following anonymized status output
|
||||||
|
|
||||||
netbird status -dA
|
netbird status -dA
|
||||||
|
|
||||||
As well as the file created by
|
Create and upload a debug bundle, and share the returned file key:
|
||||||
|
|
||||||
|
netbird debug for 1m -AS -U
|
||||||
|
|
||||||
|
*Uploaded files are automatically deleted after 30 days.*
|
||||||
|
|
||||||
|
|
||||||
|
Alternatively, create the file only and attach it here manually:
|
||||||
|
|
||||||
netbird debug for 1m -AS
|
netbird debug for 1m -AS
|
||||||
|
|
||||||
|
|
||||||
We advise reviewing the anonymized output for any remaining personal information.
|
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
|
||||||
@@ -57,8 +62,10 @@ If applicable, add screenshots to help explain your problem.
|
|||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
||||||
**Have you tried these troubleshooting steps?**
|
**Have you tried these troubleshooting steps?**
|
||||||
|
- [ ] Reviewed [client troubleshooting](https://docs.netbird.io/how-to/troubleshooting-client) (if applicable)
|
||||||
- [ ] Checked for newer NetBird versions
|
- [ ] Checked for newer NetBird versions
|
||||||
- [ ] Searched for similar issues on GitHub (including closed ones)
|
- [ ] Searched for similar issues on GitHub (including closed ones)
|
||||||
- [ ] Restarted the NetBird client
|
- [ ] Restarted the NetBird client
|
||||||
- [ ] Disabled other VPN software
|
- [ ] Disabled other VPN software
|
||||||
- [ ] Checked firewall settings
|
- [ ] Checked firewall settings
|
||||||
|
|
||||||
|
|||||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -13,3 +13,5 @@
|
|||||||
- [ ] It is a refactor
|
- [ ] It is a refactor
|
||||||
- [ ] Created tests that fail without the change (if possible)
|
- [ ] Created tests that fail without the change (if possible)
|
||||||
- [ ] Extended the README / documentation, if necessary
|
- [ ] Extended the README / documentation, if necessary
|
||||||
|
|
||||||
|
> By submitting this pull request, you confirm that you have read and agree to the terms of the [Contributor License Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).
|
||||||
|
|||||||
207
.github/workflows/golang-test-linux.yml
vendored
207
.github/workflows/golang-test-linux.yml
vendored
@@ -146,6 +146,65 @@ jobs:
|
|||||||
- name: Test
|
- name: Test
|
||||||
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay)
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} CI=true go test -tags devcert -exec 'sudo' -timeout 10m -p 1 $(go list ./... | grep -v -e /management -e /signal -e /relay)
|
||||||
|
|
||||||
|
test_client_on_docker:
|
||||||
|
name: "Client (Docker) / Unit"
|
||||||
|
needs: [build-cache]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.23.x"
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get Go environment
|
||||||
|
id: go-env
|
||||||
|
run: |
|
||||||
|
echo "cache_dir=$(go env GOCACHE)" >> $GITHUB_OUTPUT
|
||||||
|
echo "modcache_dir=$(go env GOMODCACHE)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
id: cache-restore
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ steps.go-env.outputs.cache_dir }}
|
||||||
|
${{ steps.go-env.outputs.modcache_dir }}
|
||||||
|
key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
|
- name: Run tests in container
|
||||||
|
env:
|
||||||
|
HOST_GOCACHE: ${{ steps.go-env.outputs.cache_dir }}
|
||||||
|
HOST_GOMODCACHE: ${{ steps.go-env.outputs.modcache_dir }}
|
||||||
|
run: |
|
||||||
|
CONTAINER_GOCACHE="/root/.cache/go-build"
|
||||||
|
CONTAINER_GOMODCACHE="/go/pkg/mod"
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
--cap-add=NET_ADMIN \
|
||||||
|
--privileged \
|
||||||
|
-v $PWD:/app \
|
||||||
|
-w /app \
|
||||||
|
-v "${HOST_GOCACHE}:${CONTAINER_GOCACHE}" \
|
||||||
|
-v "${HOST_GOMODCACHE}:${CONTAINER_GOMODCACHE}" \
|
||||||
|
-e CGO_ENABLED=1 \
|
||||||
|
-e CI=true \
|
||||||
|
-e DOCKER_CI=true \
|
||||||
|
-e GOARCH=${GOARCH_TARGET} \
|
||||||
|
-e GOCACHE=${CONTAINER_GOCACHE} \
|
||||||
|
-e GOMODCACHE=${CONTAINER_GOMODCACHE} \
|
||||||
|
golang:1.23-alpine \
|
||||||
|
sh -c ' \
|
||||||
|
apk update; apk add --no-cache \
|
||||||
|
ca-certificates iptables ip6tables dbus dbus-dev libpcap-dev build-base; \
|
||||||
|
go test -buildvcs=false -tags devcert -v -timeout 10m -p 1 $(go list -buildvcs=false ./... | grep -v -e /management -e /signal -e /relay -e /client/ui -e /upload-server)
|
||||||
|
'
|
||||||
|
|
||||||
test_relay:
|
test_relay:
|
||||||
name: "Relay / Unit"
|
name: "Relay / Unit"
|
||||||
needs: [build-cache]
|
needs: [build-cache]
|
||||||
@@ -164,6 +223,10 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
@@ -179,13 +242,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gotest-cache-
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
|
||||||
|
|
||||||
- name: Install 32-bit libpcap
|
|
||||||
if: matrix.arch == '386'
|
|
||||||
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
@@ -217,6 +273,10 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
|
run: sudo apt update && sudo apt install -y gcc-multilib g++-multilib libc6-dev-i386
|
||||||
|
|
||||||
- name: Get Go environment
|
- name: Get Go environment
|
||||||
run: |
|
run: |
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
||||||
@@ -232,13 +292,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gotest-cache-
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
|
||||||
|
|
||||||
- name: Install 32-bit libpcap
|
|
||||||
if: matrix.arch == '386'
|
|
||||||
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
@@ -286,13 +339,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gotest-cache-
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
|
||||||
|
|
||||||
- name: Install 32-bit libpcap
|
|
||||||
if: matrix.arch == '386'
|
|
||||||
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
@@ -354,13 +400,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gotest-cache-
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
|
||||||
|
|
||||||
- name: Install 32-bit libpcap
|
|
||||||
if: matrix.arch == '386'
|
|
||||||
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
@@ -385,7 +424,7 @@ jobs:
|
|||||||
CI=true \
|
CI=true \
|
||||||
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' \
|
||||||
-timeout 20m ./...
|
-timeout 20m ./management/...
|
||||||
|
|
||||||
api_benchmark:
|
api_benchmark:
|
||||||
name: "Management / Benchmark (API)"
|
name: "Management / Benchmark (API)"
|
||||||
@@ -449,13 +488,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gotest-cache-
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
|
||||||
|
|
||||||
- name: Install 32-bit libpcap
|
|
||||||
if: matrix.arch == '386'
|
|
||||||
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
@@ -520,13 +552,6 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gotest-cache-
|
${{ runner.os }}-gotest-cache-
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
|
||||||
|
|
||||||
- name: Install 32-bit libpcap
|
|
||||||
if: matrix.arch == '386'
|
|
||||||
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
@@ -541,99 +566,3 @@ jobs:
|
|||||||
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/...
|
-timeout 20m ./management/...
|
||||||
|
|
||||||
test_client_on_docker:
|
|
||||||
name: "Client (Docker) / Unit"
|
|
||||||
needs: [ build-cache ]
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "1.23.x"
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Get Go environment
|
|
||||||
run: |
|
|
||||||
echo "cache=$(go env GOCACHE)" >> $GITHUB_ENV
|
|
||||||
echo "modcache=$(go env.GOMODCACHE)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Cache Go modules
|
|
||||||
uses: actions/cache/restore@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
${{ env.cache }}
|
|
||||||
${{ env.modcache }}
|
|
||||||
key: ${{ runner.os }}-gotest-cache-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gotest-cache-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
|
||||||
|
|
||||||
- name: Install modules
|
|
||||||
run: go mod tidy
|
|
||||||
|
|
||||||
- name: Check git status
|
|
||||||
run: git --no-pager diff --exit-code
|
|
||||||
|
|
||||||
- name: Generate Shared Sock Test bin
|
|
||||||
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
|
|
||||||
|
|
||||||
- name: Generate RouteManager Test bin
|
|
||||||
run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager
|
|
||||||
|
|
||||||
- name: Generate SystemOps Test bin (static via Alpine)
|
|
||||||
run: |
|
|
||||||
docker run --rm -v $PWD:/app -w /app \
|
|
||||||
alpine:latest \
|
|
||||||
sh -c "
|
|
||||||
apk add --no-cache go gcc musl-dev libpcap-dev dbus-dev && \
|
|
||||||
adduser -D -u $(id -u) builder && \
|
|
||||||
su builder -c '\
|
|
||||||
cd /app && \
|
|
||||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 \
|
|
||||||
go test -c -o /app/systemops-testing.bin \
|
|
||||||
-tags netgo \
|
|
||||||
-ldflags=\"-w -extldflags \\\"-static -ldbus-1 -lpcap\\\"\" \
|
|
||||||
./client/internal/routemanager/systemops \
|
|
||||||
'
|
|
||||||
"
|
|
||||||
|
|
||||||
- name: Generate nftables Manager Test bin
|
|
||||||
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
|
|
||||||
|
|
||||||
- name: Generate Engine Test bin
|
|
||||||
run: CGO_ENABLED=1 go test -c -o engine-testing.bin ./client/internal
|
|
||||||
|
|
||||||
- name: Generate Peer Test bin
|
|
||||||
run: CGO_ENABLED=0 go test -c -o peer-testing.bin ./client/internal/peer/
|
|
||||||
|
|
||||||
- run: chmod +x *testing.bin
|
|
||||||
|
|
||||||
- name: Run Shared Sock tests in docker
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/sharedsock --entrypoint /ci/sharedsock-testing.bin gcr.io/distroless/base:debug -test.timeout 5m -test.parallel 1
|
|
||||||
|
|
||||||
- name: Run Iface tests in docker
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/netbird -v /tmp/cache:/tmp/cache -v /tmp/modcache:/tmp/modcache -w /netbird -e GOCACHE=/tmp/cache -e GOMODCACHE=/tmp/modcache -e CGO_ENABLED=0 golang:1.23-alpine go test -test.timeout 5m -test.parallel 1 ./client/iface/...
|
|
||||||
|
|
||||||
- name: Run RouteManager tests in docker
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /ci/routemanager-testing.bin gcr.io/distroless/base:debug -test.timeout 5m -test.parallel 1
|
|
||||||
|
|
||||||
- name: Run SystemOps tests in docker
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager/systemops --entrypoint /ci/systemops-testing.bin gcr.io/distroless/base:debug -test.timeout 5m -test.parallel 1
|
|
||||||
|
|
||||||
- name: Run nftables Manager tests in docker
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /ci/nftablesmanager-testing.bin gcr.io/distroless/base:debug -test.timeout 5m -test.parallel 1
|
|
||||||
|
|
||||||
- name: Run Engine tests in docker with file store
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="jsonfile" --entrypoint /ci/engine-testing.bin gcr.io/distroless/base:debug -test.timeout 5m -test.parallel 1
|
|
||||||
|
|
||||||
- name: Run Engine tests in docker with sqlite store
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="sqlite" --entrypoint /ci/engine-testing.bin gcr.io/distroless/base:debug -test.timeout 5m -test.parallel 1
|
|
||||||
|
|
||||||
- name: Run Peer tests in docker
|
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /ci/peer-testing.bin gcr.io/distroless/base:debug -test.timeout 5m -test.parallel 1
|
|
||||||
|
|||||||
1
.github/workflows/golangci-lint.yml
vendored
1
.github/workflows/golangci-lint.yml
vendored
@@ -21,7 +21,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe
|
ignore_words_list: erro,clienta,hastable,iif,groupd,testin,groupe
|
||||||
skip: go.mod,go.sum
|
skip: go.mod,go.sum
|
||||||
only_warn: 1
|
|
||||||
golangci:
|
golangci:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|||||||
@@ -172,13 +172,14 @@ jobs:
|
|||||||
grep "NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN" docker-compose.yml
|
grep "NETBIRD_STORE_ENGINE_MYSQL_DSN=$NETBIRD_STORE_ENGINE_MYSQL_DSN" docker-compose.yml
|
||||||
grep NETBIRD_STORE_ENGINE_POSTGRES_DSN docker-compose.yml | egrep "$NETBIRD_STORE_ENGINE_POSTGRES_DSN"
|
grep NETBIRD_STORE_ENGINE_POSTGRES_DSN docker-compose.yml | egrep "$NETBIRD_STORE_ENGINE_POSTGRES_DSN"
|
||||||
# check relay values
|
# check relay values
|
||||||
grep "NB_EXPOSED_ADDRESS=$CI_NETBIRD_DOMAIN:33445" docker-compose.yml
|
grep "NB_EXPOSED_ADDRESS=rels://$CI_NETBIRD_DOMAIN:33445" docker-compose.yml
|
||||||
grep "NB_LISTEN_ADDRESS=:33445" docker-compose.yml
|
grep "NB_LISTEN_ADDRESS=:33445" docker-compose.yml
|
||||||
grep '33445:33445' docker-compose.yml
|
grep '33445:33445' docker-compose.yml
|
||||||
grep -A 10 'relay:' docker-compose.yml | egrep 'NB_AUTH_SECRET=.+$'
|
grep -A 10 'relay:' docker-compose.yml | egrep 'NB_AUTH_SECRET=.+$'
|
||||||
grep -A 7 Relay management.json | grep "rel://$CI_NETBIRD_DOMAIN:33445"
|
grep -A 7 Relay management.json | grep "rels://$CI_NETBIRD_DOMAIN:33445"
|
||||||
grep -A 7 Relay management.json | egrep '"Secret": ".+"'
|
grep -A 7 Relay management.json | egrep '"Secret": ".+"'
|
||||||
grep DisablePromptLogin management.json | grep 'true'
|
grep DisablePromptLogin management.json | grep 'true'
|
||||||
|
grep LoginFlag management.json | grep 0
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|||||||
@@ -96,6 +96,20 @@ builds:
|
|||||||
- -s -w -X github.com/netbirdio/netbird/version.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
- -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 }}"
|
mod_timestamp: "{{ .CommitTimestamp }}"
|
||||||
|
|
||||||
|
- id: netbird-upload
|
||||||
|
dir: upload-server
|
||||||
|
env: [CGO_ENABLED=0]
|
||||||
|
binary: netbird-upload
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
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 }}"
|
||||||
|
|
||||||
universal_binaries:
|
universal_binaries:
|
||||||
- id: netbird
|
- id: netbird
|
||||||
|
|
||||||
@@ -409,6 +423,52 @@ dockers:
|
|||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- "--label=maintainer=dev@netbird.io"
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-upload
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: upload-server/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-upload
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: upload-server/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-upload
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: upload-server/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
docker_manifests:
|
docker_manifests:
|
||||||
- name_template: netbirdio/netbird:{{ .Version }}
|
- name_template: netbirdio/netbird:{{ .Version }}
|
||||||
image_templates:
|
image_templates:
|
||||||
@@ -475,7 +535,17 @@ docker_manifests:
|
|||||||
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
||||||
- netbirdio/management:{{ .Version }}-debug-arm
|
- netbirdio/management:{{ .Version }}-debug-arm
|
||||||
- netbirdio/management:{{ .Version }}-debug-amd64
|
- netbirdio/management:{{ .Version }}-debug-amd64
|
||||||
|
- name_template: netbirdio/upload:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm
|
||||||
|
- netbirdio/upload:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/upload:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/upload:{{ .Version }}-arm
|
||||||
|
- netbirdio/upload:{{ .Version }}-amd64
|
||||||
brews:
|
brews:
|
||||||
- ids:
|
- ids:
|
||||||
- default
|
- default
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-31rofwmxc-27akKd0Le0vyRpBcwXkP0g">
|
<a href="https://docs.netbird.io/slack-url">
|
||||||
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
||||||
</a>
|
</a>
|
||||||
<br>
|
<br>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<br/>
|
<br/>
|
||||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||||
<br/>
|
<br/>
|
||||||
Join our <a href="https://join.slack.com/t/netbirdio/shared_invite/zt-31rofwmxc-27akKd0Le0vyRpBcwXkP0g">Slack channel</a>
|
Join our <a href="https://docs.netbird.io/slack-url">Slack channel</a>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
</strong>
|
</strong>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
FROM alpine:3.21.3
|
FROM alpine:3.21.3
|
||||||
RUN apk add --no-cache ca-certificates iptables ip6tables
|
# iproute2: busybox doesn't display ip rules properly
|
||||||
|
RUN apk add --no-cache ca-certificates ip6tables iproute2 iptables
|
||||||
ENV NB_FOREGROUND_MODE=true
|
ENV NB_FOREGROUND_MODE=true
|
||||||
ENTRYPOINT [ "/usr/local/bin/netbird","up"]
|
ENTRYPOINT [ "/usr/local/bin/netbird","up"]
|
||||||
COPY netbird /usr/local/bin/netbird
|
COPY netbird /usr/local/bin/netbird
|
||||||
|
|||||||
@@ -69,6 +69,22 @@ func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr {
|
|||||||
return a.ipAnonymizer[ip]
|
return a.ipAnonymizer[ip]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeUDPAddr(addr net.UDPAddr) net.UDPAddr {
|
||||||
|
// Convert IP to netip.Addr
|
||||||
|
ip, ok := netip.AddrFromSlice(addr.IP)
|
||||||
|
if !ok {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
anonIP := a.AnonymizeIP(ip)
|
||||||
|
|
||||||
|
return net.UDPAddr{
|
||||||
|
IP: anonIP.AsSlice(),
|
||||||
|
Port: addr.Port,
|
||||||
|
Zone: addr.Zone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs
|
// isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs
|
||||||
func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool {
|
func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool {
|
||||||
if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 {
|
if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 {
|
||||||
|
|||||||
@@ -87,16 +87,27 @@ func debugBundle(cmd *cobra.Command, _ []string) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
client := proto.NewDaemonServiceClient(conn)
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
|
request := &proto.DebugBundleRequest{
|
||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
Status: getStatusOutput(cmd, anonymizeFlag),
|
Status: getStatusOutput(cmd, anonymizeFlag),
|
||||||
SystemInfo: debugSystemInfoFlag,
|
SystemInfo: debugSystemInfoFlag,
|
||||||
})
|
}
|
||||||
|
if debugUploadBundle {
|
||||||
|
request.UploadURL = debugUploadBundleURL
|
||||||
|
}
|
||||||
|
resp, err := client.DebugBundle(cmd.Context(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
cmd.Printf("Local file:\n%s\n", resp.GetPath())
|
||||||
|
|
||||||
cmd.Println(resp.GetPath())
|
if resp.GetUploadFailureReason() != "" {
|
||||||
|
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugUploadBundle {
|
||||||
|
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -211,23 +222,19 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration)
|
headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration)
|
||||||
statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag))
|
statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd, anonymizeFlag))
|
||||||
|
request := &proto.DebugBundleRequest{
|
||||||
resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
|
|
||||||
Anonymize: anonymizeFlag,
|
Anonymize: anonymizeFlag,
|
||||||
Status: statusOutput,
|
Status: statusOutput,
|
||||||
SystemInfo: debugSystemInfoFlag,
|
SystemInfo: debugSystemInfoFlag,
|
||||||
})
|
}
|
||||||
|
if debugUploadBundle {
|
||||||
|
request.UploadURL = debugUploadBundleURL
|
||||||
|
}
|
||||||
|
resp, err := client.DebugBundle(cmd.Context(), request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable network map persistence after creating the debug bundle
|
|
||||||
if _, err := client.SetNetworkMapPersistence(cmd.Context(), &proto.SetNetworkMapPersistenceRequest{
|
|
||||||
Enabled: false,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("failed to disable network map persistence: %v", status.Convert(err).Message())
|
|
||||||
}
|
|
||||||
|
|
||||||
if stateWasDown {
|
if stateWasDown {
|
||||||
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
||||||
@@ -242,7 +249,15 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
|
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println(resp.GetPath())
|
cmd.Printf("Local file:\n%s\n", resp.GetPath())
|
||||||
|
|
||||||
|
if resp.GetUploadFailureReason() != "" {
|
||||||
|
return fmt.Errorf("upload failed: %s", resp.GetUploadFailureReason())
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugUploadBundle {
|
||||||
|
cmd.Printf("Upload file key:\n%s\n", resp.GetUploadedKey())
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -55,6 +56,9 @@ var loginCmd = &cobra.Command{
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update host's static platform and system information
|
||||||
|
system.UpdateStaticInfo()
|
||||||
|
|
||||||
ic := internal.ConfigInput{
|
ic := internal.ConfigInput{
|
||||||
ManagementURL: managementURL,
|
ManagementURL: managementURL,
|
||||||
AdminURL: adminURL,
|
AdminURL: adminURL,
|
||||||
@@ -95,11 +99,11 @@ var loginCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: providedSetupKey,
|
SetupKey: providedSetupKey,
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
IsLinuxDesktopClient: isLinuxRunningDesktop(),
|
IsUnixDesktopClient: isUnixRunningDesktop(),
|
||||||
Hostname: hostName,
|
Hostname: hostName,
|
||||||
DnsLabels: dnsLabelsReq,
|
DnsLabels: dnsLabelsReq,
|
||||||
}
|
}
|
||||||
|
|
||||||
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
@@ -192,7 +196,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isLinuxRunningDesktop())
|
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isUnixRunningDesktop())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -240,7 +244,10 @@ func openURL(cmd *cobra.Command, verificationURIComplete, userCode string, noBro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isLinuxRunningDesktop checks if a Linux OS is running desktop environment
|
// isUnixRunningDesktop checks if a Linux OS is running desktop environment
|
||||||
func isLinuxRunningDesktop() bool {
|
func isUnixRunningDesktop() bool {
|
||||||
|
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
|
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,23 +22,26 @@ import (
|
|||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/upload-server/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
externalIPMapFlag = "external-ip-map"
|
externalIPMapFlag = "external-ip-map"
|
||||||
dnsResolverAddress = "dns-resolver-address"
|
dnsResolverAddress = "dns-resolver-address"
|
||||||
enableRosenpassFlag = "enable-rosenpass"
|
enableRosenpassFlag = "enable-rosenpass"
|
||||||
rosenpassPermissiveFlag = "rosenpass-permissive"
|
rosenpassPermissiveFlag = "rosenpass-permissive"
|
||||||
preSharedKeyFlag = "preshared-key"
|
preSharedKeyFlag = "preshared-key"
|
||||||
interfaceNameFlag = "interface-name"
|
interfaceNameFlag = "interface-name"
|
||||||
wireguardPortFlag = "wireguard-port"
|
wireguardPortFlag = "wireguard-port"
|
||||||
networkMonitorFlag = "network-monitor"
|
networkMonitorFlag = "network-monitor"
|
||||||
disableAutoConnectFlag = "disable-auto-connect"
|
disableAutoConnectFlag = "disable-auto-connect"
|
||||||
serverSSHAllowedFlag = "allow-server-ssh"
|
serverSSHAllowedFlag = "allow-server-ssh"
|
||||||
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
||||||
dnsRouteIntervalFlag = "dns-router-interval"
|
dnsRouteIntervalFlag = "dns-router-interval"
|
||||||
systemInfoFlag = "system-info"
|
systemInfoFlag = "system-info"
|
||||||
blockLANAccessFlag = "block-lan-access"
|
enableLazyConnectionFlag = "enable-lazy-connection"
|
||||||
|
uploadBundle = "upload-bundle"
|
||||||
|
uploadBundleURL = "upload-bundle-url"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -74,7 +77,9 @@ var (
|
|||||||
anonymizeFlag bool
|
anonymizeFlag bool
|
||||||
debugSystemInfoFlag bool
|
debugSystemInfoFlag bool
|
||||||
dnsRouteInterval time.Duration
|
dnsRouteInterval time.Duration
|
||||||
blockLANAccess bool
|
debugUploadBundle bool
|
||||||
|
debugUploadBundleURL string
|
||||||
|
lazyConnEnabled bool
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird",
|
Use: "netbird",
|
||||||
@@ -179,8 +184,11 @@ func init() {
|
|||||||
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
|
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
|
||||||
upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted")
|
upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted")
|
||||||
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
|
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
|
||||||
|
upCmd.PersistentFlags().BoolVar(&lazyConnEnabled, enableLazyConnectionFlag, false, "[Experimental] Enable the lazy connection feature. If enabled, the client will establish connections on-demand.")
|
||||||
|
|
||||||
debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle")
|
debugCmd.PersistentFlags().BoolVarP(&debugSystemInfoFlag, systemInfoFlag, "S", true, "Adds system information to the debug bundle")
|
||||||
|
debugCmd.PersistentFlags().BoolVarP(&debugUploadBundle, uploadBundle, "U", false, fmt.Sprintf("Uploads the debug bundle to a server from URL defined by %s", uploadBundleURL))
|
||||||
|
debugCmd.PersistentFlags().StringVar(&debugUploadBundleURL, uploadBundleURL, types.DefaultBundleURL, "Service URL to get an URL to upload the debug bundle")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupCloseHandler handles SIGTERM signal and exits with success
|
// SetupCloseHandler handles SIGTERM signal and exits with success
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
@@ -27,12 +28,19 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newSVCConfig() *service.Config {
|
func newSVCConfig() *service.Config {
|
||||||
return &service.Config{
|
config := &service.Config{
|
||||||
Name: serviceName,
|
Name: serviceName,
|
||||||
DisplayName: "Netbird",
|
DisplayName: "Netbird",
|
||||||
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
|
Description: "Netbird mesh network client",
|
||||||
Option: make(service.KeyValue),
|
Option: make(service.KeyValue),
|
||||||
|
EnvVars: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
config.EnvVars["SYSTEMD_UNIT"] = serviceName
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSVC(prg *program, conf *service.Config) (service.Service, error) {
|
func newSVC(prg *program, conf *service.Config) (service.Service, error) {
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/server"
|
"github.com/netbirdio/netbird/client/server"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *program) Start(svc service.Service) error {
|
func (p *program) Start(svc service.Service) error {
|
||||||
// Start should not block. Do the actual work async.
|
// Start should not block. Do the actual work async.
|
||||||
log.Info("starting Netbird service") //nolint
|
log.Info("starting Netbird service") //nolint
|
||||||
|
|
||||||
|
// Collect static system and platform information
|
||||||
|
system.UpdateStaticInfo()
|
||||||
|
|
||||||
// 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()
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ var installCmd = &cobra.Command{
|
|||||||
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
|
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url", managementURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
if logFile != "console" {
|
if logFile != "" {
|
||||||
svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
|
svcConfig.Arguments = append(svcConfig.Arguments, "--log-file", logFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func init() {
|
|||||||
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
|
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
|
||||||
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
||||||
statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
|
statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
|
||||||
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
func statusFunc(cmd *cobra.Command, args []string) error {
|
func statusFunc(cmd *cobra.Command, args []string) error {
|
||||||
@@ -127,12 +127,12 @@ func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
|
|||||||
|
|
||||||
func parseFilters() error {
|
func parseFilters() error {
|
||||||
switch strings.ToLower(statusFilter) {
|
switch strings.ToLower(statusFilter) {
|
||||||
case "", "disconnected", "connected":
|
case "", "idle", "connecting", "connected":
|
||||||
if strings.ToLower(statusFilter) != "" {
|
if strings.ToLower(statusFilter) != "" {
|
||||||
enableDetailFlagWhenFilterFlag()
|
enableDetailFlagWhenFilterFlag()
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
|
return fmt.Errorf("wrong status filter, should be one of connected|connecting|idle, got: %s", statusFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ipsFilter) > 0 {
|
if len(ipsFilter) > 0 {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const (
|
|||||||
disableServerRoutesFlag = "disable-server-routes"
|
disableServerRoutesFlag = "disable-server-routes"
|
||||||
disableDNSFlag = "disable-dns"
|
disableDNSFlag = "disable-dns"
|
||||||
disableFirewallFlag = "disable-firewall"
|
disableFirewallFlag = "disable-firewall"
|
||||||
|
blockLANAccessFlag = "block-lan-access"
|
||||||
|
blockInboundFlag = "block-inbound"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -13,6 +15,8 @@ var (
|
|||||||
disableServerRoutes bool
|
disableServerRoutes bool
|
||||||
disableDNS bool
|
disableDNS bool
|
||||||
disableFirewall bool
|
disableFirewall bool
|
||||||
|
blockLANAccess bool
|
||||||
|
blockInbound bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -28,4 +32,11 @@ func init() {
|
|||||||
|
|
||||||
upCmd.PersistentFlags().BoolVar(&disableFirewall, disableFirewallFlag, false,
|
upCmd.PersistentFlags().BoolVar(&disableFirewall, disableFirewallFlag, false,
|
||||||
"Disable firewall configuration. If enabled, the client won't modify firewall rules.")
|
"Disable firewall configuration. If enabled, the client won't modify firewall rules.")
|
||||||
|
|
||||||
|
upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false,
|
||||||
|
"Block access to local networks (LAN) when using this peer as a router or exit node")
|
||||||
|
|
||||||
|
upCmd.PersistentFlags().BoolVar(&blockInbound, blockInboundFlag, false,
|
||||||
|
"Block inbound connections. If enabled, the client will not allow any inbound connections to the local machine nor routed networks.\n"+
|
||||||
|
"This overrides any policies received from the management service.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ var traceCmd = &cobra.Command{
|
|||||||
Example: `
|
Example: `
|
||||||
netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack
|
netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack
|
||||||
netbird debug trace out 10.10.0.1 8.8.8.8 -p udp --dport 53
|
netbird debug trace out 10.10.0.1 8.8.8.8 -p udp --dport 53
|
||||||
netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --type 8 --code 0
|
netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --icmp-type 8 --icmp-code 0
|
||||||
netbird debug trace in 100.64.1.1 self -p tcp --dport 80`,
|
netbird debug trace in 100.64.1.1 self -p tcp --dport 80`,
|
||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
RunE: tracePacket,
|
RunE: tracePacket,
|
||||||
|
|||||||
280
client/cmd/up.go
280
client/cmd/up.go
@@ -55,12 +55,11 @@ func init() {
|
|||||||
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().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. `+
|
`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.`,
|
||||||
)
|
)
|
||||||
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
|
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
|
||||||
upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
|
upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
|
||||||
upCmd.PersistentFlags().BoolVar(&blockLANAccess, blockLANAccessFlag, false, "Block access to local networks (LAN) when using this peer as a router or exit node")
|
|
||||||
|
|
||||||
upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil,
|
upCmd.PersistentFlags().StringSliceVar(&dnsLabels, dnsLabelsFlag, nil,
|
||||||
`Sets DNS labels`+
|
`Sets DNS labels`+
|
||||||
@@ -119,79 +118,9 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ic := internal.ConfigInput{
|
ic, err := setupConfig(customDNSAddressConverted, cmd)
|
||||||
ManagementURL: managementURL,
|
if err != nil {
|
||||||
AdminURL: adminURL,
|
return fmt.Errorf("setup config: %v", err)
|
||||||
ConfigPath: configPath,
|
|
||||||
NATExternalIPs: natExternalIPs,
|
|
||||||
CustomDNSAddress: customDNSAddressConverted,
|
|
||||||
ExtraIFaceBlackList: extraIFaceBlackList,
|
|
||||||
DNSLabels: dnsLabelsValidated,
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(enableRosenpassFlag).Changed {
|
|
||||||
ic.RosenpassEnabled = &rosenpassEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(rosenpassPermissiveFlag).Changed {
|
|
||||||
ic.RosenpassPermissive = &rosenpassPermissive
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
|
||||||
ic.ServerSSHAllowed = &serverSSHAllowed
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(interfaceNameFlag).Changed {
|
|
||||||
if err := parseInterfaceName(interfaceName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ic.InterfaceName = &interfaceName
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(wireguardPortFlag).Changed {
|
|
||||||
p := int(wireguardPort)
|
|
||||||
ic.WireguardPort = &p
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(networkMonitorFlag).Changed {
|
|
||||||
ic.NetworkMonitor = &networkMonitor
|
|
||||||
}
|
|
||||||
|
|
||||||
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
|
||||||
ic.PreSharedKey = &preSharedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(disableAutoConnectFlag).Changed {
|
|
||||||
ic.DisableAutoConnect = &autoConnectDisabled
|
|
||||||
|
|
||||||
if autoConnectDisabled {
|
|
||||||
cmd.Println("Autoconnect has been disabled. The client won't connect automatically when the service starts.")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !autoConnectDisabled {
|
|
||||||
cmd.Println("Autoconnect has been enabled. The client will connect automatically when the service starts.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
|
||||||
ic.DNSRouteInterval = &dnsRouteInterval
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(disableClientRoutesFlag).Changed {
|
|
||||||
ic.DisableClientRoutes = &disableClientRoutes
|
|
||||||
}
|
|
||||||
if cmd.Flag(disableServerRoutesFlag).Changed {
|
|
||||||
ic.DisableServerRoutes = &disableServerRoutes
|
|
||||||
}
|
|
||||||
if cmd.Flag(disableDNSFlag).Changed {
|
|
||||||
ic.DisableDNS = &disableDNS
|
|
||||||
}
|
|
||||||
if cmd.Flag(disableFirewallFlag).Changed {
|
|
||||||
ic.DisableFirewall = &disableFirewall
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.Flag(blockLANAccessFlag).Changed {
|
|
||||||
ic.BlockLANAccess = &blockLANAccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
providedSetupKey, err := getSetupKey()
|
providedSetupKey, err := getSetupKey()
|
||||||
@@ -199,7 +128,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
config, err := internal.UpdateOrCreateConfig(ic)
|
config, err := internal.UpdateOrCreateConfig(*ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get config file: %v", err)
|
return fmt.Errorf("get config file: %v", err)
|
||||||
}
|
}
|
||||||
@@ -258,21 +187,153 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
|
|
||||||
providedSetupKey, err := getSetupKey()
|
providedSetupKey, err := getSetupKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("get setup key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loginRequest, err := setupLoginRequest(providedSetupKey, customDNSAddressConverted, cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("setup login request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginErr error
|
||||||
|
var loginResp *proto.LoginResponse
|
||||||
|
|
||||||
|
err = WithBackOff(func() error {
|
||||||
|
var backOffErr error
|
||||||
|
loginResp, backOffErr = client.Login(ctx, loginRequest)
|
||||||
|
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
||||||
|
s.Code() == codes.PermissionDenied ||
|
||||||
|
s.Code() == codes.NotFound ||
|
||||||
|
s.Code() == codes.Unimplemented) {
|
||||||
|
loginErr = backOffErr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backOffErr
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("login backoff cycle failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginErr != nil {
|
||||||
|
return fmt.Errorf("login failed: %v", loginErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.NeedsSSOLogin {
|
||||||
|
|
||||||
|
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
|
||||||
|
|
||||||
|
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("waiting sso login failed with: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("call service up method: %v", err)
|
||||||
|
}
|
||||||
|
cmd.Println("Connected")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command) (*internal.ConfigInput, error) {
|
||||||
|
ic := internal.ConfigInput{
|
||||||
|
ManagementURL: managementURL,
|
||||||
|
AdminURL: adminURL,
|
||||||
|
ConfigPath: configPath,
|
||||||
|
NATExternalIPs: natExternalIPs,
|
||||||
|
CustomDNSAddress: customDNSAddressConverted,
|
||||||
|
ExtraIFaceBlackList: extraIFaceBlackList,
|
||||||
|
DNSLabels: dnsLabelsValidated,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(enableRosenpassFlag).Changed {
|
||||||
|
ic.RosenpassEnabled = &rosenpassEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(rosenpassPermissiveFlag).Changed {
|
||||||
|
ic.RosenpassPermissive = &rosenpassPermissive
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
||||||
|
ic.ServerSSHAllowed = &serverSSHAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(interfaceNameFlag).Changed {
|
||||||
|
if err := parseInterfaceName(interfaceName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ic.InterfaceName = &interfaceName
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(wireguardPortFlag).Changed {
|
||||||
|
p := int(wireguardPort)
|
||||||
|
ic.WireguardPort = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
|
ic.NetworkMonitor = &networkMonitor
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
|
ic.PreSharedKey = &preSharedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(disableAutoConnectFlag).Changed {
|
||||||
|
ic.DisableAutoConnect = &autoConnectDisabled
|
||||||
|
|
||||||
|
if autoConnectDisabled {
|
||||||
|
cmd.Println("Autoconnect has been disabled. The client won't connect automatically when the service starts.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoConnectDisabled {
|
||||||
|
cmd.Println("Autoconnect has been enabled. The client will connect automatically when the service starts.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
||||||
|
ic.DNSRouteInterval = &dnsRouteInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(disableClientRoutesFlag).Changed {
|
||||||
|
ic.DisableClientRoutes = &disableClientRoutes
|
||||||
|
}
|
||||||
|
if cmd.Flag(disableServerRoutesFlag).Changed {
|
||||||
|
ic.DisableServerRoutes = &disableServerRoutes
|
||||||
|
}
|
||||||
|
if cmd.Flag(disableDNSFlag).Changed {
|
||||||
|
ic.DisableDNS = &disableDNS
|
||||||
|
}
|
||||||
|
if cmd.Flag(disableFirewallFlag).Changed {
|
||||||
|
ic.DisableFirewall = &disableFirewall
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(blockLANAccessFlag).Changed {
|
||||||
|
ic.BlockLANAccess = &blockLANAccess
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(blockInboundFlag).Changed {
|
||||||
|
ic.BlockInbound = &blockInbound
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(enableLazyConnectionFlag).Changed {
|
||||||
|
ic.LazyConnectionEnabled = &lazyConnEnabled
|
||||||
|
}
|
||||||
|
return &ic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte, cmd *cobra.Command) (*proto.LoginRequest, error) {
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: providedSetupKey,
|
SetupKey: providedSetupKey,
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
AdminURL: adminURL,
|
AdminURL: adminURL,
|
||||||
NatExternalIPs: natExternalIPs,
|
NatExternalIPs: natExternalIPs,
|
||||||
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
|
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
|
||||||
CustomDNSAddress: customDNSAddressConverted,
|
CustomDNSAddress: customDNSAddressConverted,
|
||||||
IsLinuxDesktopClient: isLinuxRunningDesktop(),
|
IsUnixDesktopClient: isUnixRunningDesktop(),
|
||||||
Hostname: hostName,
|
Hostname: hostName,
|
||||||
ExtraIFaceBlacklist: extraIFaceBlackList,
|
ExtraIFaceBlacklist: extraIFaceBlackList,
|
||||||
DnsLabels: dnsLabels,
|
DnsLabels: dnsLabels,
|
||||||
CleanDNSLabels: dnsLabels != nil && len(dnsLabels) == 0,
|
CleanDNSLabels: dnsLabels != nil && len(dnsLabels) == 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
@@ -297,7 +358,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
|
|
||||||
if cmd.Flag(interfaceNameFlag).Changed {
|
if cmd.Flag(interfaceNameFlag).Changed {
|
||||||
if err := parseInterfaceName(interfaceName); err != nil {
|
if err := parseInterfaceName(interfaceName); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
loginRequest.InterfaceName = &interfaceName
|
loginRequest.InterfaceName = &interfaceName
|
||||||
}
|
}
|
||||||
@@ -332,45 +393,14 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
loginRequest.BlockLanAccess = &blockLANAccess
|
loginRequest.BlockLanAccess = &blockLANAccess
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginErr error
|
if cmd.Flag(blockInboundFlag).Changed {
|
||||||
|
loginRequest.BlockInbound = &blockInbound
|
||||||
var loginResp *proto.LoginResponse
|
|
||||||
|
|
||||||
err = WithBackOff(func() error {
|
|
||||||
var backOffErr error
|
|
||||||
loginResp, backOffErr = client.Login(ctx, &loginRequest)
|
|
||||||
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
|
||||||
s.Code() == codes.PermissionDenied ||
|
|
||||||
s.Code() == codes.NotFound ||
|
|
||||||
s.Code() == codes.Unimplemented) {
|
|
||||||
loginErr = backOffErr
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return backOffErr
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("login backoff cycle failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if loginErr != nil {
|
if cmd.Flag(enableLazyConnectionFlag).Changed {
|
||||||
return fmt.Errorf("login failed: %v", loginErr)
|
loginRequest.LazyConnectionEnabled = &lazyConnEnabled
|
||||||
}
|
}
|
||||||
|
return &loginRequest, nil
|
||||||
if loginResp.NeedsSSOLogin {
|
|
||||||
|
|
||||||
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode, noBrowser)
|
|
||||||
|
|
||||||
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("waiting sso login failed with: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
|
|
||||||
return fmt.Errorf("call service up method: %v", err)
|
|
||||||
}
|
|
||||||
cmd.Println("Connected")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateNATExternalIPs(list []string) error {
|
func validateNATExternalIPs(list []string) error {
|
||||||
|
|||||||
@@ -113,17 +113,16 @@ func (m *Manager) AddPeerFiltering(
|
|||||||
func (m *Manager) AddRouteFiltering(
|
func (m *Manager) AddRouteFiltering(
|
||||||
id []byte,
|
id []byte,
|
||||||
sources []netip.Prefix,
|
sources []netip.Prefix,
|
||||||
destination netip.Prefix,
|
destination firewall.Network,
|
||||||
proto firewall.Protocol,
|
proto firewall.Protocol,
|
||||||
sPort *firewall.Port,
|
sPort, dPort *firewall.Port,
|
||||||
dPort *firewall.Port,
|
|
||||||
action firewall.Action,
|
action firewall.Action,
|
||||||
) (firewall.Rule, error) {
|
) (firewall.Rule, error) {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
if !destination.Addr().Is4() {
|
if destination.IsPrefix() && !destination.Prefix.Addr().Is4() {
|
||||||
return nil, fmt.Errorf("unsupported IP version: %s", destination.Addr().String())
|
return nil, fmt.Errorf("unsupported IP version: %s", destination.Prefix.Addr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
|
return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
|
||||||
@@ -148,6 +147,10 @@ func (m *Manager) IsServerRouteSupported() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) IsStateful() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
@@ -199,7 +202,7 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
_, err := m.AddPeerFiltering(
|
_, err := m.AddPeerFiltering(
|
||||||
nil,
|
nil,
|
||||||
net.IP{0, 0, 0, 0},
|
net.IP{0, 0, 0, 0},
|
||||||
"all",
|
firewall.ProtocolALL,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
firewall.ActionAccept,
|
firewall.ActionAccept,
|
||||||
@@ -220,10 +223,16 @@ func (m *Manager) SetLogLevel(log.Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) EnableRouting() error {
|
func (m *Manager) EnableRouting() error {
|
||||||
|
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
|
||||||
|
return fmt.Errorf("enable IP forwarding: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DisableRouting() error {
|
func (m *Manager) DisableRouting() error {
|
||||||
|
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
|
||||||
|
return fmt.Errorf("disable IP forwarding: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +252,14 @@ func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
return m.router.DeleteDNATRule(rule)
|
return m.router.DeleteDNATRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSet updates the set with the given prefixes
|
||||||
|
func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.UpdateSet(set, prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
func getConntrackEstablished() []string {
|
func getConntrackEstablished() []string {
|
||||||
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
|
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package iptables
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -19,11 +19,8 @@ var ifaceMock = &iFaceMock{
|
|||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: net.ParseIP("10.20.0.1"),
|
IP: netip.MustParseAddr("10.20.0.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
||||||
IP: net.ParseIP("10.20.0.0"),
|
|
||||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -70,12 +67,12 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
|
|
||||||
var rule2 []fw.Rule
|
var rule2 []fw.Rule
|
||||||
t.Run("add second rule", func(t *testing.T) {
|
t.Run("add second rule", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := netip.MustParseAddr("10.20.0.3")
|
||||||
port := &fw.Port{
|
port := &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
Values: []uint16{8043, 8046},
|
Values: []uint16{8043, 8046},
|
||||||
}
|
}
|
||||||
rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "")
|
rule2, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", port, nil, fw.ActionAccept, "")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
for _, r := range rule2 {
|
for _, r := range rule2 {
|
||||||
@@ -95,9 +92,9 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("reset check", func(t *testing.T) {
|
t.Run("reset check", func(t *testing.T) {
|
||||||
// add second rule
|
// add second rule
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := netip.MustParseAddr("10.20.0.3")
|
||||||
port := &fw.Port{Values: []uint16{5353}}
|
port := &fw.Port{Values: []uint16{5353}}
|
||||||
_, err = manager.AddPeerFiltering(nil, ip, "udp", nil, port, fw.ActionAccept, "")
|
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "udp", nil, port, fw.ActionAccept, "")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
err = manager.Close(nil)
|
err = manager.Close(nil)
|
||||||
@@ -119,11 +116,8 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: net.ParseIP("10.20.0.1"),
|
IP: netip.MustParseAddr("10.20.0.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
||||||
IP: net.ParseIP("10.20.0.0"),
|
|
||||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -144,11 +138,11 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
|
|
||||||
var rule2 []fw.Rule
|
var rule2 []fw.Rule
|
||||||
t.Run("add second rule", func(t *testing.T) {
|
t.Run("add second rule", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := netip.MustParseAddr("10.20.0.3")
|
||||||
port := &fw.Port{
|
port := &fw.Port{
|
||||||
Values: []uint16{443},
|
Values: []uint16{443},
|
||||||
}
|
}
|
||||||
rule2, err = manager.AddPeerFiltering(nil, ip, "tcp", port, nil, fw.ActionAccept, "default")
|
rule2, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", port, nil, fw.ActionAccept, "default")
|
||||||
for _, r := range rule2 {
|
for _, r := range rule2 {
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set")
|
require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set")
|
||||||
@@ -186,11 +180,8 @@ func TestIptablesCreatePerformance(t *testing.T) {
|
|||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: net.ParseIP("10.20.0.1"),
|
IP: netip.MustParseAddr("10.20.0.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
||||||
IP: net.ParseIP("10.20.0.0"),
|
|
||||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -212,11 +203,11 @@ func TestIptablesCreatePerformance(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ip := net.ParseIP("10.20.0.100")
|
ip := netip.MustParseAddr("10.20.0.100")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
for i := 0; i < testMax; i++ {
|
for i := 0; i < testMax; i++ {
|
||||||
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
||||||
_, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "")
|
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "")
|
||||||
|
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,18 +57,18 @@ type ruleInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type routeFilteringRuleParams struct {
|
type routeFilteringRuleParams struct {
|
||||||
Sources []netip.Prefix
|
Source firewall.Network
|
||||||
Destination netip.Prefix
|
Destination firewall.Network
|
||||||
Proto firewall.Protocol
|
Proto firewall.Protocol
|
||||||
SPort *firewall.Port
|
SPort *firewall.Port
|
||||||
DPort *firewall.Port
|
DPort *firewall.Port
|
||||||
Direction firewall.RuleDirection
|
Direction firewall.RuleDirection
|
||||||
Action firewall.Action
|
Action firewall.Action
|
||||||
SetName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type routeRules map[string][]string
|
type routeRules map[string][]string
|
||||||
|
|
||||||
|
// the ipset library currently does not support comments, so we use the name only (string)
|
||||||
type ipsetCounter = refcounter.Counter[string, []netip.Prefix, struct{}]
|
type ipsetCounter = refcounter.Counter[string, []netip.Prefix, struct{}]
|
||||||
|
|
||||||
type router struct {
|
type router struct {
|
||||||
@@ -129,7 +129,7 @@ func (r *router) init(stateManager *statemanager.Manager) error {
|
|||||||
func (r *router) AddRouteFiltering(
|
func (r *router) AddRouteFiltering(
|
||||||
id []byte,
|
id []byte,
|
||||||
sources []netip.Prefix,
|
sources []netip.Prefix,
|
||||||
destination netip.Prefix,
|
destination firewall.Network,
|
||||||
proto firewall.Protocol,
|
proto firewall.Protocol,
|
||||||
sPort *firewall.Port,
|
sPort *firewall.Port,
|
||||||
dPort *firewall.Port,
|
dPort *firewall.Port,
|
||||||
@@ -140,27 +140,28 @@ func (r *router) AddRouteFiltering(
|
|||||||
return ruleKey, nil
|
return ruleKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var setName string
|
var source firewall.Network
|
||||||
if len(sources) > 1 {
|
if len(sources) > 1 {
|
||||||
setName = firewall.GenerateSetName(sources)
|
source.Set = firewall.NewPrefixSet(sources)
|
||||||
if _, err := r.ipsetCounter.Increment(setName, sources); err != nil {
|
} else if len(sources) > 0 {
|
||||||
return nil, fmt.Errorf("create or get ipset: %w", err)
|
source.Prefix = sources[0]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
params := routeFilteringRuleParams{
|
params := routeFilteringRuleParams{
|
||||||
Sources: sources,
|
Source: source,
|
||||||
Destination: destination,
|
Destination: destination,
|
||||||
Proto: proto,
|
Proto: proto,
|
||||||
SPort: sPort,
|
SPort: sPort,
|
||||||
DPort: dPort,
|
DPort: dPort,
|
||||||
Action: action,
|
Action: action,
|
||||||
SetName: setName,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rule := genRouteFilteringRuleSpec(params)
|
rule, err := r.genRouteRuleSpec(params, sources)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("generate route rule spec: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Insert DROP rules at the beginning, append ACCEPT rules at the end
|
// Insert DROP rules at the beginning, append ACCEPT rules at the end
|
||||||
var err error
|
|
||||||
if action == firewall.ActionDrop {
|
if action == firewall.ActionDrop {
|
||||||
// after the established rule
|
// after the established rule
|
||||||
err = r.iptablesClient.Insert(tableFilter, chainRTFWDIN, 2, rule...)
|
err = r.iptablesClient.Insert(tableFilter, chainRTFWDIN, 2, rule...)
|
||||||
@@ -183,17 +184,13 @@ func (r *router) DeleteRouteRule(rule firewall.Rule) error {
|
|||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
|
|
||||||
if rule, exists := r.rules[ruleKey]; exists {
|
if rule, exists := r.rules[ruleKey]; exists {
|
||||||
setName := r.findSetNameInRule(rule)
|
|
||||||
|
|
||||||
if err := r.iptablesClient.Delete(tableFilter, chainRTFWDIN, rule...); err != nil {
|
if err := r.iptablesClient.Delete(tableFilter, chainRTFWDIN, rule...); err != nil {
|
||||||
return fmt.Errorf("delete route rule: %v", err)
|
return fmt.Errorf("delete route rule: %v", err)
|
||||||
}
|
}
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
|
|
||||||
if setName != "" {
|
if err := r.decrementSetCounter(rule); err != nil {
|
||||||
if _, err := r.ipsetCounter.Decrement(setName); err != nil {
|
return fmt.Errorf("decrement ipset counter: %w", err)
|
||||||
return fmt.Errorf("failed to remove ipset: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("route rule %s not found", ruleKey)
|
log.Debugf("route rule %s not found", ruleKey)
|
||||||
@@ -204,13 +201,26 @@ func (r *router) DeleteRouteRule(rule firewall.Rule) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) findSetNameInRule(rule []string) string {
|
func (r *router) decrementSetCounter(rule []string) error {
|
||||||
for i, arg := range rule {
|
sets := r.findSets(rule)
|
||||||
if arg == "-m" && i+3 < len(rule) && rule[i+1] == "set" && rule[i+2] == matchSet {
|
var merr *multierror.Error
|
||||||
return rule[i+3]
|
for _, setName := range sets {
|
||||||
|
if _, err := r.ipsetCounter.Decrement(setName); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("decrement counter: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) findSets(rule []string) []string {
|
||||||
|
var sets []string
|
||||||
|
for i, arg := range rule {
|
||||||
|
if arg == "-m" && i+3 < len(rule) && rule[i+1] == "set" && rule[i+2] == matchSet {
|
||||||
|
sets = append(sets, rule[i+3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) createIpSet(setName string, sources []netip.Prefix) error {
|
func (r *router) createIpSet(setName string, sources []netip.Prefix) error {
|
||||||
@@ -231,15 +241,13 @@ func (r *router) deleteIpSet(setName string) error {
|
|||||||
if err := ipset.Destroy(setName); err != nil {
|
if err := ipset.Destroy(setName); err != nil {
|
||||||
return fmt.Errorf("destroy set %s: %w", setName, err)
|
return fmt.Errorf("destroy set %s: %w", setName, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("Deleted unused ipset %s", setName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddNatRule inserts an iptables rule pair into the nat chain
|
// AddNatRule inserts an iptables rule pair into the nat chain
|
||||||
func (r *router) AddNatRule(pair firewall.RouterPair) error {
|
func (r *router) AddNatRule(pair firewall.RouterPair) error {
|
||||||
if err := r.ipFwdState.RequestForwarding(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.legacyManagement {
|
if r.legacyManagement {
|
||||||
log.Warnf("This peer is connected to a NetBird Management service with an older version. Allowing all traffic for %s", pair.Destination)
|
log.Warnf("This peer is connected to a NetBird Management service with an older version. Allowing all traffic for %s", pair.Destination)
|
||||||
if err := r.addLegacyRouteRule(pair); err != nil {
|
if err := r.addLegacyRouteRule(pair); err != nil {
|
||||||
@@ -266,16 +274,14 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
|
|||||||
|
|
||||||
// RemoveNatRule removes an iptables rule pair from forwarding and nat chains
|
// RemoveNatRule removes an iptables rule pair from forwarding and nat chains
|
||||||
func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
||||||
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
|
if pair.Masquerade {
|
||||||
log.Errorf("%v", err)
|
if err := r.removeNatRule(pair); err != nil {
|
||||||
}
|
return fmt.Errorf("remove nat rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.removeNatRule(pair); err != nil {
|
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
|
||||||
return fmt.Errorf("remove nat rule: %w", err)
|
return fmt.Errorf("remove inverse nat rule: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
|
|
||||||
return fmt.Errorf("remove inverse nat rule: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeLegacyRouteRule(pair); err != nil {
|
if err := r.removeLegacyRouteRule(pair); err != nil {
|
||||||
@@ -313,8 +319,10 @@ func (r *router) removeLegacyRouteRule(pair firewall.RouterPair) error {
|
|||||||
return fmt.Errorf("remove legacy forwarding rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
return fmt.Errorf("remove legacy forwarding rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
||||||
}
|
}
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
} else {
|
|
||||||
log.Debugf("legacy forwarding rule %s not found", ruleKey)
|
if err := r.decrementSetCounter(rule); err != nil {
|
||||||
|
return fmt.Errorf("decrement ipset counter: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -599,12 +607,26 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
|
|||||||
rule = append(rule,
|
rule = append(rule,
|
||||||
"-m", "conntrack",
|
"-m", "conntrack",
|
||||||
"--ctstate", "NEW",
|
"--ctstate", "NEW",
|
||||||
"-s", pair.Source.String(),
|
)
|
||||||
"-d", pair.Destination.String(),
|
sourceExp, err := r.applyNetwork("-s", pair.Source, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("apply network -s: %w", err)
|
||||||
|
}
|
||||||
|
destExp, err := r.applyNetwork("-d", pair.Destination, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("apply network -d: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule = append(rule, sourceExp...)
|
||||||
|
rule = append(rule, destExp...)
|
||||||
|
rule = append(rule,
|
||||||
"-j", "MARK", "--set-mark", fmt.Sprintf("%#x", markValue),
|
"-j", "MARK", "--set-mark", fmt.Sprintf("%#x", markValue),
|
||||||
)
|
)
|
||||||
|
|
||||||
if err := r.iptablesClient.Append(tableMangle, chainRTPRE, rule...); err != nil {
|
// Ensure nat rules come first, so the mark can be overwritten.
|
||||||
|
// Currently overwritten by the dst-type LOCAL rules for redirected traffic.
|
||||||
|
if err := r.iptablesClient.Insert(tableMangle, chainRTPRE, 1, rule...); err != nil {
|
||||||
|
// TODO: rollback ipset counter
|
||||||
return fmt.Errorf("error while adding marking rule for %s: %v", pair.Destination, err)
|
return fmt.Errorf("error while adding marking rule for %s: %v", pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,6 +644,10 @@ func (r *router) removeNatRule(pair firewall.RouterPair) error {
|
|||||||
return fmt.Errorf("error while removing marking rule for %s: %v", pair.Destination, err)
|
return fmt.Errorf("error while removing marking rule for %s: %v", pair.Destination, err)
|
||||||
}
|
}
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
|
|
||||||
|
if err := r.decrementSetCounter(rule); err != nil {
|
||||||
|
return fmt.Errorf("decrement ipset counter: %w", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("marking rule %s not found", ruleKey)
|
log.Debugf("marking rule %s not found", ruleKey)
|
||||||
}
|
}
|
||||||
@@ -787,17 +813,21 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func genRouteFilteringRuleSpec(params routeFilteringRuleParams) []string {
|
func (r *router) genRouteRuleSpec(params routeFilteringRuleParams, sources []netip.Prefix) ([]string, error) {
|
||||||
var rule []string
|
var rule []string
|
||||||
|
|
||||||
if params.SetName != "" {
|
sourceExp, err := r.applyNetwork("-s", params.Source, sources)
|
||||||
rule = append(rule, "-m", "set", matchSet, params.SetName, "src")
|
if err != nil {
|
||||||
} else if len(params.Sources) > 0 {
|
return nil, fmt.Errorf("apply network -s: %w", err)
|
||||||
source := params.Sources[0]
|
|
||||||
rule = append(rule, "-s", source.String())
|
}
|
||||||
|
destExp, err := r.applyNetwork("-d", params.Destination, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("apply network -d: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule = append(rule, "-d", params.Destination.String())
|
rule = append(rule, sourceExp...)
|
||||||
|
rule = append(rule, destExp...)
|
||||||
|
|
||||||
if params.Proto != firewall.ProtocolALL {
|
if params.Proto != firewall.ProtocolALL {
|
||||||
rule = append(rule, "-p", strings.ToLower(string(params.Proto)))
|
rule = append(rule, "-p", strings.ToLower(string(params.Proto)))
|
||||||
@@ -807,7 +837,47 @@ func genRouteFilteringRuleSpec(params routeFilteringRuleParams) []string {
|
|||||||
|
|
||||||
rule = append(rule, "-j", actionToStr(params.Action))
|
rule = append(rule, "-j", actionToStr(params.Action))
|
||||||
|
|
||||||
return rule
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) applyNetwork(flag string, network firewall.Network, prefixes []netip.Prefix) ([]string, error) {
|
||||||
|
direction := "src"
|
||||||
|
if flag == "-d" {
|
||||||
|
direction = "dst"
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.IsSet() {
|
||||||
|
if _, err := r.ipsetCounter.Increment(network.Set.HashedName(), prefixes); err != nil {
|
||||||
|
return nil, fmt.Errorf("create or get ipset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{"-m", "set", matchSet, network.Set.HashedName(), direction}, nil
|
||||||
|
}
|
||||||
|
if network.IsPrefix() {
|
||||||
|
return []string{flag, network.Prefix.String()}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:nilnil
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
||||||
|
var merr *multierror.Error
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
// TODO: Implement IPv6 support
|
||||||
|
if prefix.Addr().Is6() {
|
||||||
|
log.Tracef("skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := ipset.AddPrefix(set.HashedName(), prefix); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("increment ipset counter: %w", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if merr == nil {
|
||||||
|
log.Debugf("updated set %s with prefixes %v", set.HashedName(), prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyPort(flag string, port *firewall.Port) []string {
|
func applyPort(flag string, port *firewall.Port) []string {
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
|||||||
|
|
||||||
pair := firewall.RouterPair{
|
pair := firewall.RouterPair{
|
||||||
ID: "abc",
|
ID: "abc",
|
||||||
Source: netip.MustParsePrefix("100.100.100.1/32"),
|
Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
|
||||||
Destination: netip.MustParsePrefix("100.100.100.0/24"),
|
Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.0/24")},
|
||||||
Masquerade: true,
|
Masquerade: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ruleKey, err := r.AddRouteFiltering(nil, tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action)
|
ruleKey, err := r.AddRouteFiltering(nil, tt.sources, firewall.Network{Prefix: tt.destination}, tt.proto, tt.sPort, tt.dPort, tt.action)
|
||||||
require.NoError(t, err, "AddRouteFiltering failed")
|
require.NoError(t, err, "AddRouteFiltering failed")
|
||||||
|
|
||||||
// Check if the rule is in the internal map
|
// Check if the rule is in the internal map
|
||||||
@@ -347,23 +347,29 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
assert.NoError(t, err, "Failed to check rule existence")
|
assert.NoError(t, err, "Failed to check rule existence")
|
||||||
assert.True(t, exists, "Rule not found in iptables")
|
assert.True(t, exists, "Rule not found in iptables")
|
||||||
|
|
||||||
|
var source firewall.Network
|
||||||
|
if len(tt.sources) > 1 {
|
||||||
|
source.Set = firewall.NewPrefixSet(tt.sources)
|
||||||
|
} else if len(tt.sources) > 0 {
|
||||||
|
source.Prefix = tt.sources[0]
|
||||||
|
}
|
||||||
// Verify rule content
|
// Verify rule content
|
||||||
params := routeFilteringRuleParams{
|
params := routeFilteringRuleParams{
|
||||||
Sources: tt.sources,
|
Source: source,
|
||||||
Destination: tt.destination,
|
Destination: firewall.Network{Prefix: tt.destination},
|
||||||
Proto: tt.proto,
|
Proto: tt.proto,
|
||||||
SPort: tt.sPort,
|
SPort: tt.sPort,
|
||||||
DPort: tt.dPort,
|
DPort: tt.dPort,
|
||||||
Action: tt.action,
|
Action: tt.action,
|
||||||
SetName: "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedRule := genRouteFilteringRuleSpec(params)
|
expectedRule, err := r.genRouteRuleSpec(params, nil)
|
||||||
|
require.NoError(t, err, "Failed to generate expected rule spec")
|
||||||
|
|
||||||
if tt.expectSet {
|
if tt.expectSet {
|
||||||
setName := firewall.GenerateSetName(tt.sources)
|
setName := firewall.NewPrefixSet(tt.sources).HashedName()
|
||||||
params.SetName = setName
|
expectedRule, err = r.genRouteRuleSpec(params, nil)
|
||||||
expectedRule = genRouteFilteringRuleSpec(params)
|
require.NoError(t, err, "Failed to generate expected rule spec with set")
|
||||||
|
|
||||||
// Check if the set was created
|
// Check if the set was created
|
||||||
_, exists := r.ipsetCounter.Get(setName)
|
_, exists := r.ipsetCounter.Get(setName)
|
||||||
@@ -378,3 +384,62 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFindSetNameInRule(t *testing.T) {
|
||||||
|
r := &router{}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
rule []string
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Basic rule with two sets",
|
||||||
|
rule: []string{
|
||||||
|
"-A", "NETBIRD-RT-FWD-IN", "-p", "tcp", "-m", "set", "--match-set", "nb-2e5a2a05", "src",
|
||||||
|
"-m", "set", "--match-set", "nb-349ae051", "dst", "-m", "tcp", "--dport", "8080", "-j", "ACCEPT",
|
||||||
|
},
|
||||||
|
expected: []string{"nb-2e5a2a05", "nb-349ae051"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No sets",
|
||||||
|
rule: []string{"-A", "NETBIRD-RT-FWD-IN", "-p", "tcp", "-j", "ACCEPT"},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Multiple sets with different positions",
|
||||||
|
rule: []string{
|
||||||
|
"-m", "set", "--match-set", "set1", "src", "-p", "tcp",
|
||||||
|
"-m", "set", "--match-set", "set-abc123", "dst", "-j", "ACCEPT",
|
||||||
|
},
|
||||||
|
expected: []string{"set1", "set-abc123"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Boundary case - sequence appears at end",
|
||||||
|
rule: []string{"-p", "tcp", "-m", "set", "--match-set", "final-set"},
|
||||||
|
expected: []string{"final-set"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Incomplete pattern - missing set name",
|
||||||
|
rule: []string{"-p", "tcp", "-m", "set", "--match-set"},
|
||||||
|
expected: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := r.findSets(tc.rule)
|
||||||
|
|
||||||
|
if len(result) != len(tc.expected) {
|
||||||
|
t.Errorf("Expected %d sets, got %d. Sets found: %v", len(tc.expected), len(result), result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, set := range result {
|
||||||
|
if set != tc.expected[i] {
|
||||||
|
t.Errorf("Expected set %q at position %d, got %q", tc.expected[i], i, set)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -43,6 +40,18 @@ const (
|
|||||||
// Action is the action to be taken on a rule
|
// Action is the action to be taken on a rule
|
||||||
type Action int
|
type Action int
|
||||||
|
|
||||||
|
// String returns the string representation of the action
|
||||||
|
func (a Action) String() string {
|
||||||
|
switch a {
|
||||||
|
case ActionAccept:
|
||||||
|
return "accept"
|
||||||
|
case ActionDrop:
|
||||||
|
return "drop"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ActionAccept is the action to accept a packet
|
// ActionAccept is the action to accept a packet
|
||||||
ActionAccept Action = iota
|
ActionAccept Action = iota
|
||||||
@@ -50,6 +59,33 @@ const (
|
|||||||
ActionDrop
|
ActionDrop
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Network is a rule destination, either a set or a prefix
|
||||||
|
type Network struct {
|
||||||
|
Set Set
|
||||||
|
Prefix netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the destination
|
||||||
|
func (d Network) String() string {
|
||||||
|
if d.Prefix.IsValid() {
|
||||||
|
return d.Prefix.String()
|
||||||
|
}
|
||||||
|
if d.IsSet() {
|
||||||
|
return d.Set.HashedName()
|
||||||
|
}
|
||||||
|
return "<invalid network>"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsSet returns true if the destination is a set
|
||||||
|
func (d Network) IsSet() bool {
|
||||||
|
return d.Set != Set{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPrefix returns true if the destination is a valid prefix
|
||||||
|
func (d Network) IsPrefix() bool {
|
||||||
|
return d.Prefix.IsValid()
|
||||||
|
}
|
||||||
|
|
||||||
// Manager is the high level abstraction of a firewall manager
|
// Manager is the high level abstraction of a firewall manager
|
||||||
//
|
//
|
||||||
// It declares methods which handle actions required by the
|
// It declares methods which handle actions required by the
|
||||||
@@ -80,13 +116,14 @@ type Manager interface {
|
|||||||
// IsServerRouteSupported returns true if the firewall supports server side routing operations
|
// IsServerRouteSupported returns true if the firewall supports server side routing operations
|
||||||
IsServerRouteSupported() bool
|
IsServerRouteSupported() bool
|
||||||
|
|
||||||
|
IsStateful() bool
|
||||||
|
|
||||||
AddRouteFiltering(
|
AddRouteFiltering(
|
||||||
id []byte,
|
id []byte,
|
||||||
sources []netip.Prefix,
|
sources []netip.Prefix,
|
||||||
destination netip.Prefix,
|
destination Network,
|
||||||
proto Protocol,
|
proto Protocol,
|
||||||
sPort *Port,
|
sPort, dPort *Port,
|
||||||
dPort *Port,
|
|
||||||
action Action,
|
action Action,
|
||||||
) (Rule, error)
|
) (Rule, error)
|
||||||
|
|
||||||
@@ -119,6 +156,9 @@ type Manager interface {
|
|||||||
|
|
||||||
// DeleteDNATRule deletes a DNAT rule
|
// DeleteDNATRule deletes a DNAT rule
|
||||||
DeleteDNATRule(Rule) error
|
DeleteDNATRule(Rule) error
|
||||||
|
|
||||||
|
// UpdateSet updates the set with the given prefixes
|
||||||
|
UpdateSet(hash Set, prefixes []netip.Prefix) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenKey(format string, pair RouterPair) string {
|
func GenKey(format string, pair RouterPair) string {
|
||||||
@@ -153,22 +193,6 @@ func SetLegacyManagement(router LegacyManager, isLegacy bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSetName generates a unique name for an ipset based on the given sources.
|
|
||||||
func GenerateSetName(sources []netip.Prefix) string {
|
|
||||||
// sort for consistent naming
|
|
||||||
SortPrefixes(sources)
|
|
||||||
|
|
||||||
var sourcesStr strings.Builder
|
|
||||||
for _, src := range sources {
|
|
||||||
sourcesStr.WriteString(src.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := sha256.Sum256([]byte(sourcesStr.String()))
|
|
||||||
shortHash := hex.EncodeToString(hash[:])[:8]
|
|
||||||
|
|
||||||
return fmt.Sprintf("nb-%s", shortHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MergeIPRanges merges overlapping IP ranges and returns a slice of non-overlapping netip.Prefix
|
// MergeIPRanges merges overlapping IP ranges and returns a slice of non-overlapping netip.Prefix
|
||||||
func MergeIPRanges(prefixes []netip.Prefix) []netip.Prefix {
|
func MergeIPRanges(prefixes []netip.Prefix) []netip.Prefix {
|
||||||
if len(prefixes) == 0 {
|
if len(prefixes) == 0 {
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ func TestGenerateSetName(t *testing.T) {
|
|||||||
netip.MustParsePrefix("192.168.1.0/24"),
|
netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
}
|
}
|
||||||
|
|
||||||
result1 := manager.GenerateSetName(prefixes1)
|
result1 := manager.NewPrefixSet(prefixes1)
|
||||||
result2 := manager.GenerateSetName(prefixes2)
|
result2 := manager.NewPrefixSet(prefixes2)
|
||||||
|
|
||||||
if result1 != result2 {
|
if result1 != result2 {
|
||||||
t.Errorf("Different orders produced different hashes: %s != %s", result1, result2)
|
t.Errorf("Different orders produced different hashes: %s != %s", result1, result2)
|
||||||
@@ -34,9 +34,9 @@ func TestGenerateSetName(t *testing.T) {
|
|||||||
netip.MustParsePrefix("10.0.0.0/8"),
|
netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
}
|
}
|
||||||
|
|
||||||
result := manager.GenerateSetName(prefixes)
|
result := manager.NewPrefixSet(prefixes)
|
||||||
|
|
||||||
matched, err := regexp.MatchString(`^nb-[0-9a-f]{8}$`, result)
|
matched, err := regexp.MatchString(`^nb-[0-9a-f]{8}$`, result.HashedName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Error matching regex: %v", err)
|
t.Fatalf("Error matching regex: %v", err)
|
||||||
}
|
}
|
||||||
@@ -46,8 +46,8 @@ func TestGenerateSetName(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Empty input produces consistent result", func(t *testing.T) {
|
t.Run("Empty input produces consistent result", func(t *testing.T) {
|
||||||
result1 := manager.GenerateSetName([]netip.Prefix{})
|
result1 := manager.NewPrefixSet([]netip.Prefix{})
|
||||||
result2 := manager.GenerateSetName([]netip.Prefix{})
|
result2 := manager.NewPrefixSet([]netip.Prefix{})
|
||||||
|
|
||||||
if result1 != result2 {
|
if result1 != result2 {
|
||||||
t.Errorf("Empty input produced inconsistent results: %s != %s", result1, result2)
|
t.Errorf("Empty input produced inconsistent results: %s != %s", result1, result2)
|
||||||
@@ -64,8 +64,8 @@ func TestGenerateSetName(t *testing.T) {
|
|||||||
netip.MustParsePrefix("192.168.1.0/24"),
|
netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
}
|
}
|
||||||
|
|
||||||
result1 := manager.GenerateSetName(prefixes1)
|
result1 := manager.NewPrefixSet(prefixes1)
|
||||||
result2 := manager.GenerateSetName(prefixes2)
|
result2 := manager.NewPrefixSet(prefixes2)
|
||||||
|
|
||||||
if result1 != result2 {
|
if result1 != result2 {
|
||||||
t.Errorf("Different orders of IPv4 and IPv6 produced different hashes: %s != %s", result1, result2)
|
t.Errorf("Different orders of IPv4 and IPv6 produced different hashes: %s != %s", result1, result2)
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
package manager
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RouterPair struct {
|
type RouterPair struct {
|
||||||
ID route.ID
|
ID route.ID
|
||||||
Source netip.Prefix
|
Source Network
|
||||||
Destination netip.Prefix
|
Destination Network
|
||||||
Masquerade bool
|
Masquerade bool
|
||||||
Inverse bool
|
Inverse bool
|
||||||
}
|
}
|
||||||
|
|||||||
74
client/firewall/manager/set.go
Normal file
74
client/firewall/manager/set.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Set struct {
|
||||||
|
hash [4]byte
|
||||||
|
comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of the set: hashed name and comment
|
||||||
|
func (h Set) String() string {
|
||||||
|
if h.comment == "" {
|
||||||
|
return h.HashedName()
|
||||||
|
}
|
||||||
|
return h.HashedName() + ": " + h.comment
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashedName returns the string representation of the hash
|
||||||
|
func (h Set) HashedName() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"nb-%s",
|
||||||
|
hex.EncodeToString(h.hash[:]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment returns the comment of the set
|
||||||
|
func (h Set) Comment() string {
|
||||||
|
return h.comment
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPrefixSet generates a unique name for an ipset based on the given prefixes.
|
||||||
|
func NewPrefixSet(prefixes []netip.Prefix) Set {
|
||||||
|
// sort for consistent naming
|
||||||
|
SortPrefixes(prefixes)
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
for _, src := range prefixes {
|
||||||
|
bytes, err := src.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to marshal prefix %s: %v", src, err)
|
||||||
|
}
|
||||||
|
hash.Write(bytes)
|
||||||
|
}
|
||||||
|
var set Set
|
||||||
|
copy(set.hash[:], hash.Sum(nil)[:4])
|
||||||
|
|
||||||
|
return set
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDomainSet generates a unique name for an ipset based on the given domains.
|
||||||
|
func NewDomainSet(domains domain.List) Set {
|
||||||
|
slices.Sort(domains)
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
for _, d := range domains {
|
||||||
|
hash.Write([]byte(d.PunycodeString()))
|
||||||
|
}
|
||||||
|
set := Set{
|
||||||
|
comment: domains.SafeString(),
|
||||||
|
}
|
||||||
|
copy(set.hash[:], hash.Sum(nil)[:4])
|
||||||
|
|
||||||
|
return set
|
||||||
|
}
|
||||||
@@ -135,17 +135,16 @@ func (m *Manager) AddPeerFiltering(
|
|||||||
func (m *Manager) AddRouteFiltering(
|
func (m *Manager) AddRouteFiltering(
|
||||||
id []byte,
|
id []byte,
|
||||||
sources []netip.Prefix,
|
sources []netip.Prefix,
|
||||||
destination netip.Prefix,
|
destination firewall.Network,
|
||||||
proto firewall.Protocol,
|
proto firewall.Protocol,
|
||||||
sPort *firewall.Port,
|
sPort, dPort *firewall.Port,
|
||||||
dPort *firewall.Port,
|
|
||||||
action firewall.Action,
|
action firewall.Action,
|
||||||
) (firewall.Rule, error) {
|
) (firewall.Rule, error) {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
if !destination.Addr().Is4() {
|
if destination.IsPrefix() && !destination.Prefix.Addr().Is4() {
|
||||||
return nil, fmt.Errorf("unsupported IP version: %s", destination.Addr().String())
|
return nil, fmt.Errorf("unsupported IP version: %s", destination.Prefix.Addr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
|
return m.router.AddRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
|
||||||
@@ -171,6 +170,10 @@ func (m *Manager) IsServerRouteSupported() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) IsStateful() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
@@ -242,7 +245,7 @@ func (m *Manager) SetLegacyManagement(isLegacy bool) error {
|
|||||||
return firewall.SetLegacyManagement(m.router, isLegacy)
|
return firewall.SetLegacyManagement(m.router, isLegacy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset firewall to the default state
|
// Close closes the firewall manager
|
||||||
func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
@@ -325,10 +328,16 @@ func (m *Manager) SetLogLevel(log.Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) EnableRouting() error {
|
func (m *Manager) EnableRouting() error {
|
||||||
|
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
|
||||||
|
return fmt.Errorf("enable IP forwarding: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DisableRouting() error {
|
func (m *Manager) DisableRouting() error {
|
||||||
|
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
|
||||||
|
return fmt.Errorf("disable IP forwarding: %w", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +368,14 @@ func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
return m.router.DeleteDNATRule(rule)
|
return m.router.DeleteDNATRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSet updates the set with the given prefixes
|
||||||
|
func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.UpdateSet(set, prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package nftables
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -25,11 +24,8 @@ var ifaceMock = &iFaceMock{
|
|||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: net.ParseIP("100.96.0.1"),
|
IP: netip.MustParseAddr("100.96.0.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("100.96.0.0/16"),
|
||||||
IP: net.ParseIP("100.96.0.0"),
|
|
||||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -70,11 +66,11 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ip := net.ParseIP("100.96.0.1")
|
ip := netip.MustParseAddr("100.96.0.1").Unmap()
|
||||||
|
|
||||||
testClient := &nftables.Conn{}
|
testClient := &nftables.Conn{}
|
||||||
|
|
||||||
rule, err := manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "")
|
rule, err := manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
err = manager.Flush()
|
err = manager.Flush()
|
||||||
@@ -109,8 +105,6 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1)
|
compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1)
|
||||||
|
|
||||||
ipToAdd, _ := netip.AddrFromSlice(ip)
|
|
||||||
add := ipToAdd.Unmap()
|
|
||||||
expectedExprs2 := []expr.Any{
|
expectedExprs2 := []expr.Any{
|
||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
@@ -132,7 +126,7 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpEq,
|
Op: expr.CmpOpEq,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: add.AsSlice(),
|
Data: ip.AsSlice(),
|
||||||
},
|
},
|
||||||
&expr.Payload{
|
&expr.Payload{
|
||||||
DestRegister: 1,
|
DestRegister: 1,
|
||||||
@@ -173,11 +167,8 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
|||||||
},
|
},
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: net.ParseIP("100.96.0.1"),
|
IP: netip.MustParseAddr("100.96.0.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("100.96.0.0/16"),
|
||||||
IP: net.ParseIP("100.96.0.0"),
|
|
||||||
Mask: net.IPv4Mask(255, 255, 255, 0),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -197,11 +188,11 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
|||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ip := net.ParseIP("10.20.0.100")
|
ip := netip.MustParseAddr("10.20.0.100")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
for i := 0; i < testMax; i++ {
|
for i := 0; i < testMax; i++ {
|
||||||
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
||||||
_, err = manager.AddPeerFiltering(nil, ip, "tcp", nil, port, fw.ActionAccept, "")
|
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), "tcp", nil, port, fw.ActionAccept, "")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
if i%100 == 0 {
|
if i%100 == 0 {
|
||||||
@@ -282,14 +273,14 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
|||||||
verifyIptablesOutput(t, stdout, stderr)
|
verifyIptablesOutput(t, stdout, stderr)
|
||||||
})
|
})
|
||||||
|
|
||||||
ip := net.ParseIP("100.96.0.1")
|
ip := netip.MustParseAddr("100.96.0.1")
|
||||||
_, err = manager.AddPeerFiltering(nil, ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
|
_, err = manager.AddPeerFiltering(nil, ip.AsSlice(), fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "")
|
||||||
require.NoError(t, err, "failed to add peer filtering rule")
|
require.NoError(t, err, "failed to add peer filtering rule")
|
||||||
|
|
||||||
_, err = manager.AddRouteFiltering(
|
_, err = manager.AddRouteFiltering(
|
||||||
nil,
|
nil,
|
||||||
[]netip.Prefix{netip.MustParsePrefix("192.168.2.0/24")},
|
[]netip.Prefix{netip.MustParsePrefix("192.168.2.0/24")},
|
||||||
netip.MustParsePrefix("10.1.0.0/24"),
|
fw.Network{Prefix: netip.MustParsePrefix("10.1.0.0/24")},
|
||||||
fw.ProtocolTCP,
|
fw.ProtocolTCP,
|
||||||
nil,
|
nil,
|
||||||
&fw.Port{Values: []uint16{443}},
|
&fw.Port{Values: []uint16{443}},
|
||||||
@@ -298,8 +289,8 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
|||||||
require.NoError(t, err, "failed to add route filtering rule")
|
require.NoError(t, err, "failed to add route filtering rule")
|
||||||
|
|
||||||
pair := fw.RouterPair{
|
pair := fw.RouterPair{
|
||||||
Source: netip.MustParsePrefix("192.168.1.0/24"),
|
Source: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
Destination: netip.MustParsePrefix("10.0.0.0/24"),
|
Destination: fw.Network{Prefix: netip.MustParsePrefix("10.0.0.0/24")},
|
||||||
Masquerade: true,
|
Masquerade: true,
|
||||||
}
|
}
|
||||||
err = manager.AddNatRule(pair)
|
err = manager.AddNatRule(pair)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
"github.com/google/nftables/binaryutil"
|
"github.com/google/nftables/binaryutil"
|
||||||
"github.com/google/nftables/expr"
|
"github.com/google/nftables/expr"
|
||||||
@@ -44,9 +43,14 @@ const (
|
|||||||
const refreshRulesMapError = "refresh rules map: %w"
|
const refreshRulesMapError = "refresh rules map: %w"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errFilterTableNotFound = fmt.Errorf("nftables: 'filter' table not found")
|
errFilterTableNotFound = fmt.Errorf("'filter' table not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type setInput struct {
|
||||||
|
set firewall.Set
|
||||||
|
prefixes []netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
type router struct {
|
type router struct {
|
||||||
conn *nftables.Conn
|
conn *nftables.Conn
|
||||||
workTable *nftables.Table
|
workTable *nftables.Table
|
||||||
@@ -54,7 +58,7 @@ type router struct {
|
|||||||
chains map[string]*nftables.Chain
|
chains map[string]*nftables.Chain
|
||||||
// rules is useful to avoid duplicates and to get missing attributes that we don't have when adding new rules
|
// rules is useful to avoid duplicates and to get missing attributes that we don't have when adding new rules
|
||||||
rules map[string]*nftables.Rule
|
rules map[string]*nftables.Rule
|
||||||
ipsetCounter *refcounter.Counter[string, []netip.Prefix, *nftables.Set]
|
ipsetCounter *refcounter.Counter[string, setInput, *nftables.Set]
|
||||||
|
|
||||||
wgIface iFaceMapper
|
wgIface iFaceMapper
|
||||||
ipFwdState *ipfwdstate.IPForwardingState
|
ipFwdState *ipfwdstate.IPForwardingState
|
||||||
@@ -163,7 +167,7 @@ func (r *router) removeNatPreroutingRules() error {
|
|||||||
func (r *router) loadFilterTable() (*nftables.Table, error) {
|
func (r *router) loadFilterTable() (*nftables.Table, error) {
|
||||||
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
return nil, fmt.Errorf("unable to list tables: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, table := range tables {
|
for _, table := range tables {
|
||||||
@@ -316,7 +320,7 @@ func (r *router) setupDataPlaneMark() error {
|
|||||||
func (r *router) AddRouteFiltering(
|
func (r *router) AddRouteFiltering(
|
||||||
id []byte,
|
id []byte,
|
||||||
sources []netip.Prefix,
|
sources []netip.Prefix,
|
||||||
destination netip.Prefix,
|
destination firewall.Network,
|
||||||
proto firewall.Protocol,
|
proto firewall.Protocol,
|
||||||
sPort *firewall.Port,
|
sPort *firewall.Port,
|
||||||
dPort *firewall.Port,
|
dPort *firewall.Port,
|
||||||
@@ -331,23 +335,29 @@ func (r *router) AddRouteFiltering(
|
|||||||
chain := r.chains[chainNameRoutingFw]
|
chain := r.chains[chainNameRoutingFw]
|
||||||
var exprs []expr.Any
|
var exprs []expr.Any
|
||||||
|
|
||||||
|
var source firewall.Network
|
||||||
switch {
|
switch {
|
||||||
case len(sources) == 1 && sources[0].Bits() == 0:
|
case len(sources) == 1 && sources[0].Bits() == 0:
|
||||||
// If it's 0.0.0.0/0, we don't need to add any source matching
|
// If it's 0.0.0.0/0, we don't need to add any source matching
|
||||||
case len(sources) == 1:
|
case len(sources) == 1:
|
||||||
// If there's only one source, we can use it directly
|
// If there's only one source, we can use it directly
|
||||||
exprs = append(exprs, generateCIDRMatcherExpressions(true, sources[0])...)
|
source.Prefix = sources[0]
|
||||||
default:
|
default:
|
||||||
// If there are multiple sources, create or get an ipset
|
// If there are multiple sources, use a set
|
||||||
var err error
|
source.Set = firewall.NewPrefixSet(sources)
|
||||||
exprs, err = r.getIpSetExprs(sources, exprs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get ipset expressions: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle destination
|
sourceExp, err := r.applyNetwork(source, sources, true)
|
||||||
exprs = append(exprs, generateCIDRMatcherExpressions(false, destination)...)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("apply source: %w", err)
|
||||||
|
}
|
||||||
|
exprs = append(exprs, sourceExp...)
|
||||||
|
|
||||||
|
destExp, err := r.applyNetwork(destination, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("apply destination: %w", err)
|
||||||
|
}
|
||||||
|
exprs = append(exprs, destExp...)
|
||||||
|
|
||||||
// Handle protocol
|
// Handle protocol
|
||||||
if proto != firewall.ProtocolALL {
|
if proto != firewall.ProtocolALL {
|
||||||
@@ -391,39 +401,27 @@ func (r *router) AddRouteFiltering(
|
|||||||
rule = r.conn.AddRule(rule)
|
rule = r.conn.AddRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("Adding route rule %s", spew.Sdump(rule))
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
return nil, fmt.Errorf(flushError, err)
|
return nil, fmt.Errorf(flushError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rules[string(ruleKey)] = rule
|
r.rules[string(ruleKey)] = rule
|
||||||
|
|
||||||
log.Debugf("nftables: added route rule: sources=%v, destination=%v, proto=%v, sPort=%v, dPort=%v, action=%v", sources, destination, proto, sPort, dPort, action)
|
log.Debugf("added route rule: sources=%v, destination=%v, proto=%v, sPort=%v, dPort=%v, action=%v", sources, destination, proto, sPort, dPort, action)
|
||||||
|
|
||||||
return ruleKey, nil
|
return ruleKey, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) getIpSetExprs(sources []netip.Prefix, exprs []expr.Any) ([]expr.Any, error) {
|
func (r *router) getIpSet(set firewall.Set, prefixes []netip.Prefix, isSource bool) ([]expr.Any, error) {
|
||||||
setName := firewall.GenerateSetName(sources)
|
ref, err := r.ipsetCounter.Increment(set.HashedName(), setInput{
|
||||||
ref, err := r.ipsetCounter.Increment(setName, sources)
|
set: set,
|
||||||
|
prefixes: prefixes,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create or get ipset for sources: %w", err)
|
return nil, fmt.Errorf("create or get ipset: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
exprs = append(exprs,
|
return getIpSetExprs(ref, isSource)
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
|
||||||
Offset: 12,
|
|
||||||
Len: 4,
|
|
||||||
},
|
|
||||||
&expr.Lookup{
|
|
||||||
SourceRegister: 1,
|
|
||||||
SetName: ref.Out.Name,
|
|
||||||
SetID: ref.Out.ID,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return exprs, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) DeleteRouteRule(rule firewall.Rule) error {
|
func (r *router) DeleteRouteRule(rule firewall.Rule) error {
|
||||||
@@ -442,42 +440,54 @@ func (r *router) DeleteRouteRule(rule firewall.Rule) error {
|
|||||||
return fmt.Errorf("route rule %s has no handle", ruleKey)
|
return fmt.Errorf("route rule %s has no handle", ruleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
setName := r.findSetNameInRule(nftRule)
|
|
||||||
|
|
||||||
if err := r.deleteNftRule(nftRule, ruleKey); err != nil {
|
if err := r.deleteNftRule(nftRule, ruleKey); err != nil {
|
||||||
return fmt.Errorf("delete: %w", err)
|
return fmt.Errorf("delete: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if setName != "" {
|
|
||||||
if _, err := r.ipsetCounter.Decrement(setName); err != nil {
|
|
||||||
return fmt.Errorf("decrement ipset reference: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
return fmt.Errorf(flushError, err)
|
return fmt.Errorf(flushError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := r.decrementSetCounter(nftRule); err != nil {
|
||||||
|
return fmt.Errorf("decrement set counter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) createIpSet(setName string, sources []netip.Prefix) (*nftables.Set, error) {
|
func (r *router) createIpSet(setName string, input setInput) (*nftables.Set, error) {
|
||||||
// overlapping prefixes will result in an error, so we need to merge them
|
// overlapping prefixes will result in an error, so we need to merge them
|
||||||
sources = firewall.MergeIPRanges(sources)
|
prefixes := firewall.MergeIPRanges(input.prefixes)
|
||||||
|
|
||||||
set := &nftables.Set{
|
nfset := &nftables.Set{
|
||||||
Name: setName,
|
Name: setName,
|
||||||
Table: r.workTable,
|
Comment: input.set.Comment(),
|
||||||
|
Table: r.workTable,
|
||||||
// required for prefixes
|
// required for prefixes
|
||||||
Interval: true,
|
Interval: true,
|
||||||
KeyType: nftables.TypeIPAddr,
|
KeyType: nftables.TypeIPAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elements := convertPrefixesToSet(prefixes)
|
||||||
|
if err := r.conn.AddSet(nfset, elements); err != nil {
|
||||||
|
return nil, fmt.Errorf("error adding elements to set %s: %w", setName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.conn.Flush(); err != nil {
|
||||||
|
return nil, fmt.Errorf("flush error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Created new ipset: %s with %d elements", setName, len(elements)/2)
|
||||||
|
|
||||||
|
return nfset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPrefixesToSet(prefixes []netip.Prefix) []nftables.SetElement {
|
||||||
var elements []nftables.SetElement
|
var elements []nftables.SetElement
|
||||||
for _, prefix := range sources {
|
for _, prefix := range prefixes {
|
||||||
// TODO: Implement IPv6 support
|
// TODO: Implement IPv6 support
|
||||||
if prefix.Addr().Is6() {
|
if prefix.Addr().Is6() {
|
||||||
log.Printf("Skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
|
log.Tracef("skipping IPv6 prefix %s: IPv6 support not yet implemented", prefix)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,18 +503,7 @@ func (r *router) createIpSet(setName string, sources []netip.Prefix) (*nftables.
|
|||||||
nftables.SetElement{Key: lastIP.AsSlice(), IntervalEnd: true},
|
nftables.SetElement{Key: lastIP.AsSlice(), IntervalEnd: true},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
return elements
|
||||||
if err := r.conn.AddSet(set, elements); err != nil {
|
|
||||||
return nil, fmt.Errorf("error adding elements to set %s: %w", setName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
|
||||||
return nil, fmt.Errorf("flush error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Created new ipset: %s with %d elements", setName, len(elements)/2)
|
|
||||||
|
|
||||||
return set, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateLastIP determines the last IP in a given prefix.
|
// calculateLastIP determines the last IP in a given prefix.
|
||||||
@@ -528,8 +527,8 @@ func uint32ToBytes(ip uint32) [4]byte {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) deleteIpSet(setName string, set *nftables.Set) error {
|
func (r *router) deleteIpSet(setName string, nfset *nftables.Set) error {
|
||||||
r.conn.DelSet(set)
|
r.conn.DelSet(nfset)
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
return fmt.Errorf(flushError, err)
|
return fmt.Errorf(flushError, err)
|
||||||
}
|
}
|
||||||
@@ -538,13 +537,27 @@ func (r *router) deleteIpSet(setName string, set *nftables.Set) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) findSetNameInRule(rule *nftables.Rule) string {
|
func (r *router) decrementSetCounter(rule *nftables.Rule) error {
|
||||||
for _, e := range rule.Exprs {
|
sets := r.findSets(rule)
|
||||||
if lookup, ok := e.(*expr.Lookup); ok {
|
|
||||||
return lookup.SetName
|
var merr *multierror.Error
|
||||||
|
for _, setName := range sets {
|
||||||
|
if _, err := r.ipsetCounter.Decrement(setName); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("decrement set counter: %w", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) findSets(rule *nftables.Rule) []string {
|
||||||
|
var sets []string
|
||||||
|
for _, e := range rule.Exprs {
|
||||||
|
if lookup, ok := e.(*expr.Lookup); ok {
|
||||||
|
sets = append(sets, lookup.SetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) deleteNftRule(rule *nftables.Rule, ruleKey string) error {
|
func (r *router) deleteNftRule(rule *nftables.Rule, ruleKey string) error {
|
||||||
@@ -560,10 +573,6 @@ func (r *router) deleteNftRule(rule *nftables.Rule, ruleKey string) error {
|
|||||||
|
|
||||||
// AddNatRule appends a nftables rule pair to the nat chain
|
// AddNatRule appends a nftables rule pair to the nat chain
|
||||||
func (r *router) AddNatRule(pair firewall.RouterPair) error {
|
func (r *router) AddNatRule(pair firewall.RouterPair) error {
|
||||||
if err := r.ipFwdState.RequestForwarding(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
return fmt.Errorf(refreshRulesMapError, err)
|
return fmt.Errorf(refreshRulesMapError, err)
|
||||||
}
|
}
|
||||||
@@ -586,7 +595,8 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
return fmt.Errorf("nftables: insert rules for %s: %v", pair.Destination, err)
|
// TODO: rollback ipset counter
|
||||||
|
return fmt.Errorf("insert rules for %s: %v", pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -594,19 +604,22 @@ func (r *router) AddNatRule(pair firewall.RouterPair) error {
|
|||||||
|
|
||||||
// addNatRule inserts a nftables rule to the conn client flush queue
|
// addNatRule inserts a nftables rule to the conn client flush queue
|
||||||
func (r *router) addNatRule(pair firewall.RouterPair) error {
|
func (r *router) addNatRule(pair firewall.RouterPair) error {
|
||||||
sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
|
sourceExp, err := r.applyNetwork(pair.Source, nil, true)
|
||||||
destExp := generateCIDRMatcherExpressions(false, pair.Destination)
|
if err != nil {
|
||||||
|
return fmt.Errorf("apply source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destExp, err := r.applyNetwork(pair.Destination, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("apply destination: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
op := expr.CmpOpEq
|
op := expr.CmpOpEq
|
||||||
if pair.Inverse {
|
if pair.Inverse {
|
||||||
op = expr.CmpOpNeq
|
op = expr.CmpOpNeq
|
||||||
}
|
}
|
||||||
|
|
||||||
// We only care about NEW connections to mark them and later identify them in the postrouting chain for masquerading.
|
exprs := []expr.Any{
|
||||||
// Masquerading will take care of the conntrack state, which means we won't need to mark established connections.
|
|
||||||
exprs := getCtNewExprs()
|
|
||||||
exprs = append(exprs,
|
|
||||||
// interface matching
|
|
||||||
&expr.Meta{
|
&expr.Meta{
|
||||||
Key: expr.MetaKeyIIFNAME,
|
Key: expr.MetaKeyIIFNAME,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
@@ -616,7 +629,10 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
|
|||||||
Register: 1,
|
Register: 1,
|
||||||
Data: ifname(r.wgIface.Name()),
|
Data: ifname(r.wgIface.Name()),
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
|
// We only care about NEW connections to mark them and later identify them in the postrouting chain for masquerading.
|
||||||
|
// Masquerading will take care of the conntrack state, which means we won't need to mark established connections.
|
||||||
|
exprs = append(exprs, getCtNewExprs()...)
|
||||||
|
|
||||||
exprs = append(exprs, sourceExp...)
|
exprs = append(exprs, sourceExp...)
|
||||||
exprs = append(exprs, destExp...)
|
exprs = append(exprs, destExp...)
|
||||||
@@ -646,7 +662,9 @@ func (r *router) addNatRule(pair firewall.RouterPair) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
// Ensure nat rules come first, so the mark can be overwritten.
|
||||||
|
// Currently overwritten by the dst-type LOCAL rules for redirected traffic.
|
||||||
|
r.rules[ruleKey] = r.conn.InsertRule(&nftables.Rule{
|
||||||
Table: r.workTable,
|
Table: r.workTable,
|
||||||
Chain: r.chains[chainNameManglePrerouting],
|
Chain: r.chains[chainNameManglePrerouting],
|
||||||
Exprs: exprs,
|
Exprs: exprs,
|
||||||
@@ -729,8 +747,15 @@ func (r *router) addPostroutingRules() error {
|
|||||||
|
|
||||||
// addLegacyRouteRule adds a legacy routing rule for mgmt servers pre route acls
|
// addLegacyRouteRule adds a legacy routing rule for mgmt servers pre route acls
|
||||||
func (r *router) addLegacyRouteRule(pair firewall.RouterPair) error {
|
func (r *router) addLegacyRouteRule(pair firewall.RouterPair) error {
|
||||||
sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
|
sourceExp, err := r.applyNetwork(pair.Source, nil, true)
|
||||||
destExp := generateCIDRMatcherExpressions(false, pair.Destination)
|
if err != nil {
|
||||||
|
return fmt.Errorf("apply source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
destExp, err := r.applyNetwork(pair.Destination, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("apply destination: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
exprs := []expr.Any{
|
exprs := []expr.Any{
|
||||||
&expr.Counter{},
|
&expr.Counter{},
|
||||||
@@ -739,7 +764,8 @@ func (r *router) addLegacyRouteRule(pair firewall.RouterPair) error {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
expression := append(sourceExp, append(destExp, exprs...)...) // nolint:gocritic
|
exprs = append(exprs, sourceExp...)
|
||||||
|
exprs = append(exprs, destExp...)
|
||||||
|
|
||||||
ruleKey := firewall.GenKey(firewall.ForwardingFormat, pair)
|
ruleKey := firewall.GenKey(firewall.ForwardingFormat, pair)
|
||||||
|
|
||||||
@@ -752,7 +778,7 @@ func (r *router) addLegacyRouteRule(pair firewall.RouterPair) error {
|
|||||||
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
||||||
Table: r.workTable,
|
Table: r.workTable,
|
||||||
Chain: r.chains[chainNameRoutingFw],
|
Chain: r.chains[chainNameRoutingFw],
|
||||||
Exprs: expression,
|
Exprs: exprs,
|
||||||
UserData: []byte(ruleKey),
|
UserData: []byte(ruleKey),
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
@@ -767,11 +793,13 @@ func (r *router) removeLegacyRouteRule(pair firewall.RouterPair) error {
|
|||||||
return fmt.Errorf("remove legacy forwarding rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
return fmt.Errorf("remove legacy forwarding rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("nftables: removed legacy forwarding rule %s -> %s", pair.Source, pair.Destination)
|
log.Debugf("removed legacy forwarding rule %s -> %s", pair.Source, pair.Destination)
|
||||||
|
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
} else {
|
|
||||||
log.Debugf("nftables: legacy forwarding rule %s not found", ruleKey)
|
if err := r.decrementSetCounter(rule); err != nil {
|
||||||
|
return fmt.Errorf("decrement set counter: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -974,20 +1002,18 @@ func (r *router) removeAcceptForwardRulesIptables(ipt *iptables.IPTables) error
|
|||||||
|
|
||||||
// RemoveNatRule removes the prerouting mark rule
|
// RemoveNatRule removes the prerouting mark rule
|
||||||
func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
||||||
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
return fmt.Errorf(refreshRulesMapError, err)
|
return fmt.Errorf(refreshRulesMapError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeNatRule(pair); err != nil {
|
if pair.Masquerade {
|
||||||
return fmt.Errorf("remove prerouting rule: %w", err)
|
if err := r.removeNatRule(pair); err != nil {
|
||||||
}
|
return fmt.Errorf("remove prerouting rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
|
if err := r.removeNatRule(firewall.GetInversePair(pair)); err != nil {
|
||||||
return fmt.Errorf("remove inverse prerouting rule: %w", err)
|
return fmt.Errorf("remove inverse prerouting rule: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.removeLegacyRouteRule(pair); err != nil {
|
if err := r.removeLegacyRouteRule(pair); err != nil {
|
||||||
@@ -995,10 +1021,10 @@ func (r *router) RemoveNatRule(pair firewall.RouterPair) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.Destination, err)
|
// TODO: rollback set counter
|
||||||
|
return fmt.Errorf("remove nat rules rule %s: %v", pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("nftables: removed nat rules for %s", pair.Destination)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,16 +1032,19 @@ func (r *router) removeNatRule(pair firewall.RouterPair) error {
|
|||||||
ruleKey := firewall.GenKey(firewall.PreroutingFormat, pair)
|
ruleKey := firewall.GenKey(firewall.PreroutingFormat, pair)
|
||||||
|
|
||||||
if rule, exists := r.rules[ruleKey]; exists {
|
if rule, exists := r.rules[ruleKey]; exists {
|
||||||
err := r.conn.DelRule(rule)
|
if err := r.conn.DelRule(rule); err != nil {
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("remove prerouting rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
return fmt.Errorf("remove prerouting rule %s -> %s: %v", pair.Source, pair.Destination, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("nftables: removed prerouting rule %s -> %s", pair.Source, pair.Destination)
|
log.Debugf("removed prerouting rule %s -> %s", pair.Source, pair.Destination)
|
||||||
|
|
||||||
delete(r.rules, ruleKey)
|
delete(r.rules, ruleKey)
|
||||||
|
|
||||||
|
if err := r.decrementSetCounter(rule); err != nil {
|
||||||
|
return fmt.Errorf("decrement set counter: %w", err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Debugf("nftables: prerouting rule %s not found", ruleKey)
|
log.Debugf("prerouting rule %s not found", ruleKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -1027,7 +1056,7 @@ func (r *router) refreshRulesMap() error {
|
|||||||
for _, chain := range r.chains {
|
for _, chain := range r.chains {
|
||||||
rules, err := r.conn.GetRules(chain.Table, chain)
|
rules, err := r.conn.GetRules(chain.Table, chain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("nftables: unable to list rules: %v", err)
|
return fmt.Errorf(" unable to list rules: %v", err)
|
||||||
}
|
}
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if len(rule.UserData) > 0 {
|
if len(rule.UserData) > 0 {
|
||||||
@@ -1301,13 +1330,54 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateCIDRMatcherExpressions generates nftables expressions that matches a CIDR
|
func (r *router) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
||||||
func generateCIDRMatcherExpressions(source bool, prefix netip.Prefix) []expr.Any {
|
nfset, err := r.conn.GetSetByName(r.workTable, set.HashedName())
|
||||||
var offset uint32
|
if err != nil {
|
||||||
if source {
|
return fmt.Errorf("get set %s: %w", set.HashedName(), err)
|
||||||
offset = 12 // src offset
|
}
|
||||||
} else {
|
|
||||||
offset = 16 // dst offset
|
elements := convertPrefixesToSet(prefixes)
|
||||||
|
if err := r.conn.SetAddElements(nfset, elements); err != nil {
|
||||||
|
return fmt.Errorf("add elements to set %s: %w", set.HashedName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf(flushError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("updated set %s with prefixes %v", set.HashedName(), prefixes)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyNetwork generates nftables expressions for networks (CIDR) or sets
|
||||||
|
func (r *router) applyNetwork(
|
||||||
|
network firewall.Network,
|
||||||
|
setPrefixes []netip.Prefix,
|
||||||
|
isSource bool,
|
||||||
|
) ([]expr.Any, error) {
|
||||||
|
if network.IsSet() {
|
||||||
|
exprs, err := r.getIpSet(network.Set, setPrefixes, isSource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("source: %w", err)
|
||||||
|
}
|
||||||
|
return exprs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if network.IsPrefix() {
|
||||||
|
return applyPrefix(network.Prefix, isSource), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyPrefix generates nftables expressions for a CIDR prefix
|
||||||
|
func applyPrefix(prefix netip.Prefix, isSource bool) []expr.Any {
|
||||||
|
// dst offset
|
||||||
|
offset := uint32(16)
|
||||||
|
if isSource {
|
||||||
|
// src offset
|
||||||
|
offset = 12
|
||||||
}
|
}
|
||||||
|
|
||||||
ones := prefix.Bits()
|
ones := prefix.Bits()
|
||||||
@@ -1415,3 +1485,27 @@ func getCtNewExprs() []expr.Any {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getIpSetExprs(ref refcounter.Ref[*nftables.Set], isSource bool) ([]expr.Any, error) {
|
||||||
|
|
||||||
|
// dst offset
|
||||||
|
offset := uint32(16)
|
||||||
|
if isSource {
|
||||||
|
// src offset
|
||||||
|
offset = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
return []expr.Any{
|
||||||
|
&expr.Payload{
|
||||||
|
DestRegister: 1,
|
||||||
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
|
Offset: offset,
|
||||||
|
Len: 4,
|
||||||
|
},
|
||||||
|
&expr.Lookup{
|
||||||
|
SourceRegister: 1,
|
||||||
|
SetName: ref.Out.Name,
|
||||||
|
SetID: ref.Out.ID,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ func TestNftablesManager_AddNatRule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build CIDR matching expressions
|
// Build CIDR matching expressions
|
||||||
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
sourceExp := applyPrefix(testCase.InputPair.Source.Prefix, true)
|
||||||
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
destExp := applyPrefix(testCase.InputPair.Destination.Prefix, false)
|
||||||
|
|
||||||
// Combine all expressions in the correct order
|
// Combine all expressions in the correct order
|
||||||
// nolint:gocritic
|
// nolint:gocritic
|
||||||
@@ -311,7 +311,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ruleKey, err := r.AddRouteFiltering(nil, tt.sources, tt.destination, tt.proto, tt.sPort, tt.dPort, tt.action)
|
ruleKey, err := r.AddRouteFiltering(nil, tt.sources, firewall.Network{Prefix: tt.destination}, tt.proto, tt.sPort, tt.dPort, tt.action)
|
||||||
require.NoError(t, err, "AddRouteFiltering failed")
|
require.NoError(t, err, "AddRouteFiltering failed")
|
||||||
|
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
@@ -441,8 +441,8 @@ func TestNftablesCreateIpSet(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
setName := firewall.GenerateSetName(tt.sources)
|
setName := firewall.NewPrefixSet(tt.sources).HashedName()
|
||||||
set, err := r.createIpSet(setName, tt.sources)
|
set, err := r.createIpSet(setName, setInput{prefixes: tt.sources})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("Failed to create IP set: %v", err)
|
t.Logf("Failed to create IP set: %v", err)
|
||||||
printNftSets()
|
printNftSets()
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ var (
|
|||||||
Name: "Insert Forwarding IPV4 Rule",
|
Name: "Insert Forwarding IPV4 Rule",
|
||||||
InputPair: firewall.RouterPair{
|
InputPair: firewall.RouterPair{
|
||||||
ID: "zxa",
|
ID: "zxa",
|
||||||
Source: netip.MustParsePrefix("100.100.100.1/32"),
|
Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
|
||||||
Destination: netip.MustParsePrefix("100.100.200.0/24"),
|
Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.200.0/24")},
|
||||||
Masquerade: false,
|
Masquerade: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -24,8 +24,8 @@ var (
|
|||||||
Name: "Insert Forwarding And Nat IPV4 Rules",
|
Name: "Insert Forwarding And Nat IPV4 Rules",
|
||||||
InputPair: firewall.RouterPair{
|
InputPair: firewall.RouterPair{
|
||||||
ID: "zxa",
|
ID: "zxa",
|
||||||
Source: netip.MustParsePrefix("100.100.100.1/32"),
|
Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
|
||||||
Destination: netip.MustParsePrefix("100.100.200.0/24"),
|
Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.200.0/24")},
|
||||||
Masquerade: true,
|
Masquerade: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -40,8 +40,8 @@ var (
|
|||||||
Name: "Remove Forwarding And Nat IPV4 Rules",
|
Name: "Remove Forwarding And Nat IPV4 Rules",
|
||||||
InputPair: firewall.RouterPair{
|
InputPair: firewall.RouterPair{
|
||||||
ID: "zxa",
|
ID: "zxa",
|
||||||
Source: netip.MustParsePrefix("100.100.100.1/32"),
|
Source: firewall.Network{Prefix: netip.MustParsePrefix("100.100.100.1/32")},
|
||||||
Destination: netip.MustParsePrefix("100.100.200.0/24"),
|
Destination: firewall.Network{Prefix: netip.MustParsePrefix("100.100.200.0/24")},
|
||||||
Masquerade: true,
|
Masquerade: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset firewall to the default state
|
// Close cleans up the firewall manager by removing all rules and closing trackers
|
||||||
func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ const (
|
|||||||
firewallRuleName = "Netbird"
|
firewallRuleName = "Netbird"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reset firewall to the default state
|
// Close cleans up the firewall manager by removing all rules and closing trackers
|
||||||
func (m *Manager) Close(*statemanager.Manager) error {
|
func (m *Manager) Close(*statemanager.Manager) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
@@ -32,17 +31,14 @@ func (m *Manager) Close(*statemanager.Manager) error {
|
|||||||
|
|
||||||
if m.udpTracker != nil {
|
if m.udpTracker != nil {
|
||||||
m.udpTracker.Close()
|
m.udpTracker.Close()
|
||||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger, m.flowLogger)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.icmpTracker != nil {
|
if m.icmpTracker != nil {
|
||||||
m.icmpTracker.Close()
|
m.icmpTracker.Close()
|
||||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger, m.flowLogger)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.tcpTracker != nil {
|
if m.tcpTracker != nil {
|
||||||
m.tcpTracker.Close()
|
m.tcpTracker.Close()
|
||||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger, m.flowLogger)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if fwder := m.forwarder.Load(); fwder != nil {
|
if fwder := m.forwarder.Load(); fwder != nil {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"gvisor.dev/gvisor/pkg/buffer"
|
"gvisor.dev/gvisor/pkg/buffer"
|
||||||
@@ -17,6 +19,7 @@ import (
|
|||||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
|
||||||
)
|
)
|
||||||
@@ -29,14 +32,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Forwarder struct {
|
type Forwarder struct {
|
||||||
logger *nblog.Logger
|
logger *nblog.Logger
|
||||||
flowLogger nftypes.FlowLogger
|
flowLogger nftypes.FlowLogger
|
||||||
|
// ruleIdMap is used to store the rule ID for a given connection
|
||||||
|
ruleIdMap sync.Map
|
||||||
stack *stack.Stack
|
stack *stack.Stack
|
||||||
endpoint *endpoint
|
endpoint *endpoint
|
||||||
udpForwarder *udpForwarder
|
udpForwarder *udpForwarder
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
ip net.IP
|
ip tcpip.Address
|
||||||
netstack bool
|
netstack bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +71,11 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
|
|||||||
return nil, fmt.Errorf("failed to create NIC: %v", err)
|
return nil, fmt.Errorf("failed to create NIC: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ones, _ := iface.Address().Network.Mask.Size()
|
|
||||||
protoAddr := tcpip.ProtocolAddress{
|
protoAddr := tcpip.ProtocolAddress{
|
||||||
Protocol: ipv4.ProtocolNumber,
|
Protocol: ipv4.ProtocolNumber,
|
||||||
AddressWithPrefix: tcpip.AddressWithPrefix{
|
AddressWithPrefix: tcpip.AddressWithPrefix{
|
||||||
Address: tcpip.AddrFromSlice(iface.Address().IP.To4()),
|
Address: tcpip.AddrFromSlice(iface.Address().IP.AsSlice()),
|
||||||
PrefixLen: ones,
|
PrefixLen: iface.Address().Network.Bits(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +115,7 @@ func New(iface common.IFaceMapper, logger *nblog.Logger, flowLogger nftypes.Flow
|
|||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
netstack: netstack,
|
netstack: netstack,
|
||||||
ip: iface.Address().IP,
|
ip: tcpip.AddrFromSlice(iface.Address().IP.AsSlice()),
|
||||||
}
|
}
|
||||||
|
|
||||||
receiveWindow := defaultReceiveWindow
|
receiveWindow := defaultReceiveWindow
|
||||||
@@ -162,8 +166,39 @@ func (f *Forwarder) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP {
|
func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP {
|
||||||
if f.netstack && f.ip.Equal(addr.AsSlice()) {
|
if f.netstack && f.ip.Equal(addr) {
|
||||||
return net.IPv4(127, 0, 0, 1)
|
return net.IPv4(127, 0, 0, 1)
|
||||||
}
|
}
|
||||||
return addr.AsSlice()
|
return addr.AsSlice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) RegisterRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uint16, ruleID []byte) {
|
||||||
|
key := buildKey(srcIP, dstIP, srcPort, dstPort)
|
||||||
|
f.ruleIdMap.LoadOrStore(key, ruleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) getRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uint16) ([]byte, bool) {
|
||||||
|
if value, ok := f.ruleIdMap.Load(buildKey(srcIP, dstIP, srcPort, dstPort)); ok {
|
||||||
|
return value.([]byte), true
|
||||||
|
} else if value, ok := f.ruleIdMap.Load(buildKey(dstIP, srcIP, dstPort, srcPort)); ok {
|
||||||
|
return value.([]byte), true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) DeleteRuleID(srcIP, dstIP netip.Addr, srcPort, dstPort uint16) {
|
||||||
|
if _, ok := f.ruleIdMap.LoadAndDelete(buildKey(srcIP, dstIP, srcPort, dstPort)); ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.ruleIdMap.LoadAndDelete(buildKey(dstIP, srcIP, dstPort, srcPort))
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildKey(srcIP, dstIP netip.Addr, srcPort, dstPort uint16) conntrack.ConnKey {
|
||||||
|
return conntrack.ConnKey{
|
||||||
|
SrcIP: srcIP,
|
||||||
|
DstIP: dstIP,
|
||||||
|
SrcPort: srcPort,
|
||||||
|
DstPort: dstPort,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
|
|||||||
}
|
}
|
||||||
|
|
||||||
flowID := uuid.New()
|
flowID := uuid.New()
|
||||||
f.sendICMPEvent(nftypes.TypeStart, flowID, id, icmpType, icmpCode)
|
f.sendICMPEvent(nftypes.TypeStart, flowID, id, icmpType, icmpCode, 0, 0)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(f.ctx, 5*time.Second)
|
ctx, cancel := context.WithTimeout(f.ctx, 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -34,14 +34,14 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
|
|||||||
// TODO: support non-root
|
// TODO: support non-root
|
||||||
conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0")
|
conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.logger.Error("Failed to create ICMP socket for %v: %v", epID(id), err)
|
f.logger.Error("forwarder: Failed to create ICMP socket for %v: %v", epID(id), err)
|
||||||
|
|
||||||
// This will make netstack reply on behalf of the original destination, that's ok for now
|
// This will make netstack reply on behalf of the original destination, that's ok for now
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := conn.Close(); err != nil {
|
if err := conn.Close(); err != nil {
|
||||||
f.logger.Debug("Failed to close ICMP socket: %v", err)
|
f.logger.Debug("forwarder: Failed to close ICMP socket: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -52,36 +52,37 @@ func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBuf
|
|||||||
payload := fullPacket.AsSlice()
|
payload := fullPacket.AsSlice()
|
||||||
|
|
||||||
if _, err = conn.WriteTo(payload, dst); err != nil {
|
if _, err = conn.WriteTo(payload, dst); err != nil {
|
||||||
f.logger.Error("Failed to write ICMP packet for %v: %v", epID(id), err)
|
f.logger.Error("forwarder: Failed to write ICMP packet for %v: %v", epID(id), err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
f.logger.Trace("Forwarded ICMP packet %v type %v code %v",
|
f.logger.Trace("forwarder: Forwarded ICMP packet %v type %v code %v",
|
||||||
epID(id), icmpHdr.Type(), icmpHdr.Code())
|
epID(id), icmpHdr.Type(), icmpHdr.Code())
|
||||||
|
|
||||||
// For Echo Requests, send and handle response
|
// For Echo Requests, send and handle response
|
||||||
if header.ICMPv4Type(icmpType) == header.ICMPv4Echo {
|
if header.ICMPv4Type(icmpType) == header.ICMPv4Echo {
|
||||||
f.handleEchoResponse(icmpHdr, conn, id)
|
rxBytes := pkt.Size()
|
||||||
f.sendICMPEvent(nftypes.TypeEnd, flowID, id, icmpType, icmpCode)
|
txBytes := f.handleEchoResponse(icmpHdr, conn, id)
|
||||||
|
f.sendICMPEvent(nftypes.TypeEnd, flowID, id, icmpType, icmpCode, uint64(rxBytes), uint64(txBytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// For other ICMP types (Time Exceeded, Destination Unreachable, etc) do nothing
|
// For other ICMP types (Time Exceeded, Destination Unreachable, etc) do nothing
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) {
|
func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketConn, id stack.TransportEndpointID) int {
|
||||||
if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||||
f.logger.Error("Failed to set read deadline for ICMP response: %v", err)
|
f.logger.Error("forwarder: Failed to set read deadline for ICMP response: %v", err)
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
response := make([]byte, f.endpoint.mtu)
|
response := make([]byte, f.endpoint.mtu)
|
||||||
n, _, err := conn.ReadFrom(response)
|
n, _, err := conn.ReadFrom(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !isTimeout(err) {
|
if !isTimeout(err) {
|
||||||
f.logger.Error("Failed to read ICMP response: %v", err)
|
f.logger.Error("forwarder: Failed to read ICMP response: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
ipHdr := make([]byte, header.IPv4MinimumSize)
|
ipHdr := make([]byte, header.IPv4MinimumSize)
|
||||||
@@ -100,28 +101,54 @@ func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, conn net.PacketCon
|
|||||||
fullPacket = append(fullPacket, response[:n]...)
|
fullPacket = append(fullPacket, response[:n]...)
|
||||||
|
|
||||||
if err := f.InjectIncomingPacket(fullPacket); err != nil {
|
if err := f.InjectIncomingPacket(fullPacket); err != nil {
|
||||||
f.logger.Error("Failed to inject ICMP response: %v", err)
|
f.logger.Error("forwarder: Failed to inject ICMP response: %v", err)
|
||||||
|
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
f.logger.Trace("Forwarded ICMP echo reply for %v type %v code %v",
|
f.logger.Trace("forwarder: Forwarded ICMP echo reply for %v type %v code %v",
|
||||||
epID(id), icmpHdr.Type(), icmpHdr.Code())
|
epID(id), icmpHdr.Type(), icmpHdr.Code())
|
||||||
|
|
||||||
|
return len(fullPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendICMPEvent stores flow events for ICMP packets
|
// sendICMPEvent stores flow events for ICMP packets
|
||||||
func (f *Forwarder) sendICMPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, icmpType, icmpCode uint8) {
|
func (f *Forwarder) sendICMPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, icmpType, icmpCode uint8, rxBytes, txBytes uint64) {
|
||||||
f.flowLogger.StoreEvent(nftypes.EventFields{
|
var rxPackets, txPackets uint64
|
||||||
|
if rxBytes > 0 {
|
||||||
|
rxPackets = 1
|
||||||
|
}
|
||||||
|
if txBytes > 0 {
|
||||||
|
txPackets = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
|
||||||
|
dstIp := netip.AddrFrom4(id.LocalAddress.As4())
|
||||||
|
|
||||||
|
fields := nftypes.EventFields{
|
||||||
FlowID: flowID,
|
FlowID: flowID,
|
||||||
Type: typ,
|
Type: typ,
|
||||||
Direction: nftypes.Ingress,
|
Direction: nftypes.Ingress,
|
||||||
Protocol: nftypes.ICMP,
|
Protocol: nftypes.ICMP,
|
||||||
// TODO: handle ipv6
|
// TODO: handle ipv6
|
||||||
SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()),
|
SourceIP: srcIp,
|
||||||
DestIP: netip.AddrFrom4(id.LocalAddress.As4()),
|
DestIP: dstIp,
|
||||||
ICMPType: icmpType,
|
ICMPType: icmpType,
|
||||||
ICMPCode: icmpCode,
|
ICMPCode: icmpCode,
|
||||||
|
|
||||||
// TODO: get packets/bytes
|
RxBytes: rxBytes,
|
||||||
})
|
TxBytes: txBytes,
|
||||||
|
RxPackets: rxPackets,
|
||||||
|
TxPackets: txPackets,
|
||||||
|
}
|
||||||
|
|
||||||
|
if typ == nftypes.TypeStart {
|
||||||
|
if ruleId, ok := f.getRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort); ok {
|
||||||
|
fields.RuleID = ruleId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
f.DeleteRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.flowLogger.StoreEvent(fields)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"gvisor.dev/gvisor/pkg/tcpip"
|
"gvisor.dev/gvisor/pkg/tcpip"
|
||||||
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||||
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||||
@@ -23,11 +25,11 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
|
|||||||
|
|
||||||
flowID := uuid.New()
|
flowID := uuid.New()
|
||||||
|
|
||||||
f.sendTCPEvent(nftypes.TypeStart, flowID, id, nil)
|
f.sendTCPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
|
||||||
var success bool
|
var success bool
|
||||||
defer func() {
|
defer func() {
|
||||||
if !success {
|
if !success {
|
||||||
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, nil)
|
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -65,67 +67,97 @@ func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint, flowID uuid.UUID) {
|
func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint, flowID uuid.UUID) {
|
||||||
defer func() {
|
|
||||||
if err := inConn.Close(); err != nil {
|
|
||||||
f.logger.Debug("forwarder: inConn close error: %v", err)
|
|
||||||
}
|
|
||||||
if err := outConn.Close(); err != nil {
|
|
||||||
f.logger.Debug("forwarder: outConn close error: %v", err)
|
|
||||||
}
|
|
||||||
ep.Close()
|
|
||||||
|
|
||||||
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, ep)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Create context for managing the proxy goroutines
|
|
||||||
ctx, cancel := context.WithCancel(f.ctx)
|
ctx, cancel := context.WithCancel(f.ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
errChan := make(chan error, 2)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
_, err := io.Copy(outConn, inConn)
|
<-ctx.Done()
|
||||||
errChan <- err
|
// Close connections and endpoint.
|
||||||
}()
|
if err := inConn.Close(); err != nil && !isClosedError(err) {
|
||||||
|
f.logger.Debug("forwarder: inConn close error: %v", err)
|
||||||
go func() {
|
}
|
||||||
_, err := io.Copy(inConn, outConn)
|
if err := outConn.Close(); err != nil && !isClosedError(err) {
|
||||||
errChan <- err
|
f.logger.Debug("forwarder: outConn close error: %v", err)
|
||||||
}()
|
}
|
||||||
|
|
||||||
select {
|
ep.Close()
|
||||||
case <-ctx.Done():
|
}()
|
||||||
f.logger.Trace("forwarder: tearing down TCP connection %v due to context done", epID(id))
|
|
||||||
return
|
var wg sync.WaitGroup
|
||||||
case err := <-errChan:
|
wg.Add(2)
|
||||||
if err != nil && !isClosedError(err) {
|
|
||||||
f.logger.Error("proxyTCP: copy error: %v", err)
|
var (
|
||||||
|
bytesFromInToOut int64 // bytes from client to server (tx for client)
|
||||||
|
bytesFromOutToIn int64 // bytes from server to client (rx for client)
|
||||||
|
errInToOut error
|
||||||
|
errOutToIn error
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
bytesFromInToOut, errInToOut = io.Copy(outConn, inConn)
|
||||||
|
cancel()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
|
||||||
|
bytesFromOutToIn, errOutToIn = io.Copy(inConn, outConn)
|
||||||
|
cancel()
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if errInToOut != nil {
|
||||||
|
if !isClosedError(errInToOut) {
|
||||||
|
f.logger.Error("proxyTCP: copy error (in -> out) for %s: %v", epID(id), errInToOut)
|
||||||
}
|
}
|
||||||
f.logger.Trace("forwarder: tearing down TCP connection %v", epID(id))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if errOutToIn != nil {
|
||||||
|
if !isClosedError(errOutToIn) {
|
||||||
|
f.logger.Error("proxyTCP: copy error (out -> in) for %s: %v", epID(id), errOutToIn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rxPackets, txPackets uint64
|
||||||
|
if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
|
||||||
|
// fields are flipped since this is the in conn
|
||||||
|
rxPackets = tcpStats.SegmentsSent.Value()
|
||||||
|
txPackets = tcpStats.SegmentsReceived.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Trace("forwarder: Removed TCP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, bytesFromOutToIn, txPackets, bytesFromInToOut)
|
||||||
|
|
||||||
|
f.sendTCPEvent(nftypes.TypeEnd, flowID, id, uint64(bytesFromOutToIn), uint64(bytesFromInToOut), rxPackets, txPackets)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forwarder) sendTCPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, ep tcpip.Endpoint) {
|
func (f *Forwarder) sendTCPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, rxBytes, txBytes, rxPackets, txPackets uint64) {
|
||||||
|
srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
|
||||||
|
dstIp := netip.AddrFrom4(id.LocalAddress.As4())
|
||||||
|
|
||||||
fields := nftypes.EventFields{
|
fields := nftypes.EventFields{
|
||||||
FlowID: flowID,
|
FlowID: flowID,
|
||||||
Type: typ,
|
Type: typ,
|
||||||
Direction: nftypes.Ingress,
|
Direction: nftypes.Ingress,
|
||||||
Protocol: nftypes.TCP,
|
Protocol: nftypes.TCP,
|
||||||
// TODO: handle ipv6
|
// TODO: handle ipv6
|
||||||
SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()),
|
SourceIP: srcIp,
|
||||||
DestIP: netip.AddrFrom4(id.LocalAddress.As4()),
|
DestIP: dstIp,
|
||||||
SourcePort: id.RemotePort,
|
SourcePort: id.RemotePort,
|
||||||
DestPort: id.LocalPort,
|
DestPort: id.LocalPort,
|
||||||
|
RxBytes: rxBytes,
|
||||||
|
TxBytes: txBytes,
|
||||||
|
RxPackets: rxPackets,
|
||||||
|
TxPackets: txPackets,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ep != nil {
|
if typ == nftypes.TypeStart {
|
||||||
if tcpStats, ok := ep.Stats().(*tcp.Stats); ok {
|
if ruleId, ok := f.getRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort); ok {
|
||||||
// fields are flipped since this is the in conn
|
fields.RuleID = ruleId
|
||||||
// TODO: get bytes
|
|
||||||
fields.RxPackets = tcpStats.SegmentsSent.Value()
|
|
||||||
fields.TxPackets = tcpStats.SegmentsReceived.Value()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
f.DeleteRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.flowLogger.StoreEvent(fields)
|
f.flowLogger.StoreEvent(fields)
|
||||||
|
|||||||
@@ -149,11 +149,11 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
|
|||||||
|
|
||||||
flowID := uuid.New()
|
flowID := uuid.New()
|
||||||
|
|
||||||
f.sendUDPEvent(nftypes.TypeStart, flowID, id, nil)
|
f.sendUDPEvent(nftypes.TypeStart, flowID, id, 0, 0, 0, 0)
|
||||||
var success bool
|
var success bool
|
||||||
defer func() {
|
defer func() {
|
||||||
if !success {
|
if !success {
|
||||||
f.sendUDPEvent(nftypes.TypeEnd, flowID, id, nil)
|
f.sendUDPEvent(nftypes.TypeEnd, flowID, id, 0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -199,7 +199,6 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
|
|||||||
if err := outConn.Close(); err != nil {
|
if err := outConn.Close(); err != nil {
|
||||||
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err)
|
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
f.udpForwarder.conns[id] = pConn
|
f.udpForwarder.conns[id] = pConn
|
||||||
@@ -212,68 +211,94 @@ func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack.TransportEndpointID, ep tcpip.Endpoint) {
|
func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack.TransportEndpointID, ep tcpip.Endpoint) {
|
||||||
defer func() {
|
|
||||||
|
ctx, cancel := context.WithCancel(f.ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
pConn.cancel()
|
pConn.cancel()
|
||||||
if err := pConn.conn.Close(); err != nil {
|
if err := pConn.conn.Close(); err != nil && !isClosedError(err) {
|
||||||
f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err)
|
f.logger.Debug("forwarder: UDP inConn close error for %v: %v", epID(id), err)
|
||||||
}
|
}
|
||||||
if err := pConn.outConn.Close(); err != nil {
|
if err := pConn.outConn.Close(); err != nil && !isClosedError(err) {
|
||||||
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err)
|
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", epID(id), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ep.Close()
|
ep.Close()
|
||||||
|
|
||||||
f.udpForwarder.Lock()
|
|
||||||
delete(f.udpForwarder.conns, id)
|
|
||||||
f.udpForwarder.Unlock()
|
|
||||||
|
|
||||||
f.sendUDPEvent(nftypes.TypeEnd, pConn.flowID, id, ep)
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
errChan := make(chan error, 2)
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(2)
|
||||||
|
|
||||||
|
var txBytes, rxBytes int64
|
||||||
|
var outboundErr, inboundErr error
|
||||||
|
|
||||||
|
// outbound->inbound: copy from pConn.conn to pConn.outConn
|
||||||
go func() {
|
go func() {
|
||||||
errChan <- pConn.copy(ctx, pConn.conn, pConn.outConn, &f.udpForwarder.bufPool, "outbound->inbound")
|
defer wg.Done()
|
||||||
|
txBytes, outboundErr = pConn.copy(ctx, pConn.conn, pConn.outConn, &f.udpForwarder.bufPool, "outbound->inbound")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// inbound->outbound: copy from pConn.outConn to pConn.conn
|
||||||
go func() {
|
go func() {
|
||||||
errChan <- pConn.copy(ctx, pConn.outConn, pConn.conn, &f.udpForwarder.bufPool, "inbound->outbound")
|
defer wg.Done()
|
||||||
|
rxBytes, inboundErr = pConn.copy(ctx, pConn.outConn, pConn.conn, &f.udpForwarder.bufPool, "inbound->outbound")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
select {
|
wg.Wait()
|
||||||
case <-ctx.Done():
|
|
||||||
f.logger.Trace("forwarder: tearing down UDP connection %v due to context done", epID(id))
|
if outboundErr != nil && !isClosedError(outboundErr) {
|
||||||
return
|
f.logger.Error("proxyUDP: copy error (outbound->inbound) for %s: %v", epID(id), outboundErr)
|
||||||
case err := <-errChan:
|
|
||||||
if err != nil && !isClosedError(err) {
|
|
||||||
f.logger.Error("proxyUDP: copy error: %v", err)
|
|
||||||
}
|
|
||||||
f.logger.Trace("forwarder: tearing down UDP connection %v", epID(id))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
if inboundErr != nil && !isClosedError(inboundErr) {
|
||||||
|
f.logger.Error("proxyUDP: copy error (inbound->outbound) for %s: %v", epID(id), inboundErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rxPackets, txPackets uint64
|
||||||
|
if udpStats, ok := ep.Stats().(*tcpip.TransportEndpointStats); ok {
|
||||||
|
// fields are flipped since this is the in conn
|
||||||
|
rxPackets = udpStats.PacketsSent.Value()
|
||||||
|
txPackets = udpStats.PacketsReceived.Value()
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Trace("forwarder: Removed UDP connection %s [in: %d Pkts/%d B, out: %d Pkts/%d B]", epID(id), rxPackets, rxBytes, txPackets, txBytes)
|
||||||
|
|
||||||
|
f.udpForwarder.Lock()
|
||||||
|
delete(f.udpForwarder.conns, id)
|
||||||
|
f.udpForwarder.Unlock()
|
||||||
|
|
||||||
|
f.sendUDPEvent(nftypes.TypeEnd, pConn.flowID, id, uint64(rxBytes), uint64(txBytes), rxPackets, txPackets)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendUDPEvent stores flow events for UDP connections
|
// sendUDPEvent stores flow events for UDP connections
|
||||||
func (f *Forwarder) sendUDPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, ep tcpip.Endpoint) {
|
func (f *Forwarder) sendUDPEvent(typ nftypes.Type, flowID uuid.UUID, id stack.TransportEndpointID, rxBytes, txBytes, rxPackets, txPackets uint64) {
|
||||||
|
srcIp := netip.AddrFrom4(id.RemoteAddress.As4())
|
||||||
|
dstIp := netip.AddrFrom4(id.LocalAddress.As4())
|
||||||
|
|
||||||
fields := nftypes.EventFields{
|
fields := nftypes.EventFields{
|
||||||
FlowID: flowID,
|
FlowID: flowID,
|
||||||
Type: typ,
|
Type: typ,
|
||||||
Direction: nftypes.Ingress,
|
Direction: nftypes.Ingress,
|
||||||
Protocol: nftypes.UDP,
|
Protocol: nftypes.UDP,
|
||||||
// TODO: handle ipv6
|
// TODO: handle ipv6
|
||||||
SourceIP: netip.AddrFrom4(id.RemoteAddress.As4()),
|
SourceIP: srcIp,
|
||||||
DestIP: netip.AddrFrom4(id.LocalAddress.As4()),
|
DestIP: dstIp,
|
||||||
SourcePort: id.RemotePort,
|
SourcePort: id.RemotePort,
|
||||||
DestPort: id.LocalPort,
|
DestPort: id.LocalPort,
|
||||||
|
RxBytes: rxBytes,
|
||||||
|
TxBytes: txBytes,
|
||||||
|
RxPackets: rxPackets,
|
||||||
|
TxPackets: txPackets,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ep != nil {
|
if typ == nftypes.TypeStart {
|
||||||
if tcpStats, ok := ep.Stats().(*tcpip.TransportEndpointStats); ok {
|
if ruleId, ok := f.getRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort); ok {
|
||||||
// fields are flipped since this is the in conn
|
fields.RuleID = ruleId
|
||||||
// TODO: get bytes
|
|
||||||
fields.RxPackets = tcpStats.PacketsSent.Value()
|
|
||||||
fields.TxPackets = tcpStats.PacketsReceived.Value()
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
f.DeleteRuleID(srcIp, dstIp, id.RemotePort, id.LocalPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.flowLogger.StoreEvent(fields)
|
f.flowLogger.StoreEvent(fields)
|
||||||
@@ -288,18 +313,20 @@ func (c *udpPacketConn) getIdleDuration() time.Duration {
|
|||||||
return time.Since(lastSeen)
|
return time.Since(lastSeen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bufPool *sync.Pool, direction string) error {
|
// copy reads from src and writes to dst.
|
||||||
|
func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bufPool *sync.Pool, direction string) (int64, error) {
|
||||||
bufp := bufPool.Get().(*[]byte)
|
bufp := bufPool.Get().(*[]byte)
|
||||||
defer bufPool.Put(bufp)
|
defer bufPool.Put(bufp)
|
||||||
buffer := *bufp
|
buffer := *bufp
|
||||||
|
var totalBytes int64 = 0
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return totalBytes, ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := src.SetDeadline(time.Now().Add(udpTimeout)); err != nil {
|
if err := src.SetDeadline(time.Now().Add(udpTimeout)); err != nil {
|
||||||
return fmt.Errorf("set read deadline: %w", err)
|
return totalBytes, fmt.Errorf("set read deadline: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
n, err := src.Read(buffer)
|
n, err := src.Read(buffer)
|
||||||
@@ -307,14 +334,15 @@ func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bu
|
|||||||
if isTimeout(err) {
|
if isTimeout(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return fmt.Errorf("read from %s: %w", direction, err)
|
return totalBytes, fmt.Errorf("read from %s: %w", direction, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = dst.Write(buffer[:n])
|
nWritten, err := dst.Write(buffer[:n])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("write to %s: %w", direction, err)
|
return totalBytes, fmt.Errorf("write to %s: %w", direction, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
totalBytes += int64(nWritten)
|
||||||
c.updateLastSeen()
|
c.updateLastSeen()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,24 +45,26 @@ func (m *localIPManager) setBitmapBit(ip net.IP) {
|
|||||||
m.ipv4Bitmap[high].bitmap[index] |= 1 << bit
|
m.ipv4Bitmap[high].bitmap[index] |= 1 << bit
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *localIPManager) setBitInBitmap(ip net.IP, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) {
|
func (m *localIPManager) setBitInBitmap(ip netip.Addr, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) {
|
||||||
if ipv4 := ip.To4(); ipv4 != nil {
|
if !ip.Is4() {
|
||||||
high := uint16(ipv4[0])
|
return
|
||||||
low := (uint16(ipv4[1]) << 8) | (uint16(ipv4[2]) << 4) | uint16(ipv4[3])
|
}
|
||||||
|
ipv4 := ip.AsSlice()
|
||||||
|
|
||||||
if bitmap[high] == nil {
|
high := uint16(ipv4[0])
|
||||||
bitmap[high] = &ipv4LowBitmap{}
|
low := (uint16(ipv4[1]) << 8) | (uint16(ipv4[2]) << 4) | uint16(ipv4[3])
|
||||||
}
|
|
||||||
|
|
||||||
index := low / 32
|
if bitmap[high] == nil {
|
||||||
bit := low % 32
|
bitmap[high] = &ipv4LowBitmap{}
|
||||||
bitmap[high].bitmap[index] |= 1 << bit
|
}
|
||||||
|
|
||||||
ipStr := ipv4.String()
|
index := low / 32
|
||||||
if _, exists := ipv4Set[ipStr]; !exists {
|
bit := low % 32
|
||||||
ipv4Set[ipStr] = struct{}{}
|
bitmap[high].bitmap[index] |= 1 << bit
|
||||||
*ipv4Addresses = append(*ipv4Addresses, ipStr)
|
|
||||||
}
|
if _, exists := ipv4Set[ip]; !exists {
|
||||||
|
ipv4Set[ip] = struct{}{}
|
||||||
|
*ipv4Addresses = append(*ipv4Addresses, ip)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,12 +81,12 @@ func (m *localIPManager) checkBitmapBit(ip []byte) bool {
|
|||||||
return (m.ipv4Bitmap[high].bitmap[index] & (1 << bit)) != 0
|
return (m.ipv4Bitmap[high].bitmap[index] & (1 << bit)) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *localIPManager) processIP(ip net.IP, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) error {
|
func (m *localIPManager) processIP(ip netip.Addr, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) error {
|
||||||
m.setBitInBitmap(ip, bitmap, ipv4Set, ipv4Addresses)
|
m.setBitInBitmap(ip, bitmap, ipv4Set, ipv4Addresses)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv4LowBitmap, ipv4Set map[string]struct{}, ipv4Addresses *[]string) {
|
func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv4LowBitmap, ipv4Set map[netip.Addr]struct{}, ipv4Addresses *[]netip.Addr) {
|
||||||
addrs, err := iface.Addrs()
|
addrs, err := iface.Addrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("get addresses for interface %s failed: %v", iface.Name, err)
|
log.Debugf("get addresses for interface %s failed: %v", iface.Name, err)
|
||||||
@@ -102,7 +104,13 @@ func (m *localIPManager) processInterface(iface net.Interface, bitmap *[256]*ipv
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.processIP(ip, bitmap, ipv4Set, ipv4Addresses); err != nil {
|
addr, ok := netip.AddrFromSlice(ip)
|
||||||
|
if !ok {
|
||||||
|
log.Warnf("invalid IP address %s in interface %s", ip.String(), iface.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.processIP(addr.Unmap(), bitmap, ipv4Set, ipv4Addresses); err != nil {
|
||||||
log.Debugf("process IP failed: %v", err)
|
log.Debugf("process IP failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,8 +124,8 @@ func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var newIPv4Bitmap [256]*ipv4LowBitmap
|
var newIPv4Bitmap [256]*ipv4LowBitmap
|
||||||
ipv4Set := make(map[string]struct{})
|
ipv4Set := make(map[netip.Addr]struct{})
|
||||||
var ipv4Addresses []string
|
var ipv4Addresses []netip.Addr
|
||||||
|
|
||||||
// 127.0.0.0/8
|
// 127.0.0.0/8
|
||||||
newIPv4Bitmap[127] = &ipv4LowBitmap{}
|
newIPv4Bitmap[127] = &ipv4LowBitmap{}
|
||||||
|
|||||||
@@ -20,11 +20,8 @@ func TestLocalIPManager(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Localhost range",
|
name: "Localhost range",
|
||||||
setupAddr: wgaddr.Address{
|
setupAddr: wgaddr.Address{
|
||||||
IP: net.ParseIP("192.168.1.1"),
|
IP: netip.MustParseAddr("192.168.1.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
IP: net.ParseIP("192.168.1.0"),
|
|
||||||
Mask: net.CIDRMask(24, 32),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
testIP: netip.MustParseAddr("127.0.0.2"),
|
testIP: netip.MustParseAddr("127.0.0.2"),
|
||||||
expected: true,
|
expected: true,
|
||||||
@@ -32,11 +29,8 @@ func TestLocalIPManager(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Localhost standard address",
|
name: "Localhost standard address",
|
||||||
setupAddr: wgaddr.Address{
|
setupAddr: wgaddr.Address{
|
||||||
IP: net.ParseIP("192.168.1.1"),
|
IP: netip.MustParseAddr("192.168.1.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
IP: net.ParseIP("192.168.1.0"),
|
|
||||||
Mask: net.CIDRMask(24, 32),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
testIP: netip.MustParseAddr("127.0.0.1"),
|
testIP: netip.MustParseAddr("127.0.0.1"),
|
||||||
expected: true,
|
expected: true,
|
||||||
@@ -44,11 +38,8 @@ func TestLocalIPManager(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Localhost range edge",
|
name: "Localhost range edge",
|
||||||
setupAddr: wgaddr.Address{
|
setupAddr: wgaddr.Address{
|
||||||
IP: net.ParseIP("192.168.1.1"),
|
IP: netip.MustParseAddr("192.168.1.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
IP: net.ParseIP("192.168.1.0"),
|
|
||||||
Mask: net.CIDRMask(24, 32),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
testIP: netip.MustParseAddr("127.255.255.255"),
|
testIP: netip.MustParseAddr("127.255.255.255"),
|
||||||
expected: true,
|
expected: true,
|
||||||
@@ -56,11 +47,8 @@ func TestLocalIPManager(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Local IP matches",
|
name: "Local IP matches",
|
||||||
setupAddr: wgaddr.Address{
|
setupAddr: wgaddr.Address{
|
||||||
IP: net.ParseIP("192.168.1.1"),
|
IP: netip.MustParseAddr("192.168.1.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
IP: net.ParseIP("192.168.1.0"),
|
|
||||||
Mask: net.CIDRMask(24, 32),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
testIP: netip.MustParseAddr("192.168.1.1"),
|
testIP: netip.MustParseAddr("192.168.1.1"),
|
||||||
expected: true,
|
expected: true,
|
||||||
@@ -68,11 +56,8 @@ func TestLocalIPManager(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Local IP doesn't match",
|
name: "Local IP doesn't match",
|
||||||
setupAddr: wgaddr.Address{
|
setupAddr: wgaddr.Address{
|
||||||
IP: net.ParseIP("192.168.1.1"),
|
IP: netip.MustParseAddr("192.168.1.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
IP: net.ParseIP("192.168.1.0"),
|
|
||||||
Mask: net.CIDRMask(24, 32),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
testIP: netip.MustParseAddr("192.168.1.2"),
|
testIP: netip.MustParseAddr("192.168.1.2"),
|
||||||
expected: false,
|
expected: false,
|
||||||
@@ -80,11 +65,8 @@ func TestLocalIPManager(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Local IP doesn't match - addresses 32 apart",
|
name: "Local IP doesn't match - addresses 32 apart",
|
||||||
setupAddr: wgaddr.Address{
|
setupAddr: wgaddr.Address{
|
||||||
IP: net.ParseIP("192.168.1.1"),
|
IP: netip.MustParseAddr("192.168.1.1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
IP: net.ParseIP("192.168.1.0"),
|
|
||||||
Mask: net.CIDRMask(24, 32),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
testIP: netip.MustParseAddr("192.168.1.33"),
|
testIP: netip.MustParseAddr("192.168.1.33"),
|
||||||
expected: false,
|
expected: false,
|
||||||
@@ -92,11 +74,8 @@ func TestLocalIPManager(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "IPv6 address",
|
name: "IPv6 address",
|
||||||
setupAddr: wgaddr.Address{
|
setupAddr: wgaddr.Address{
|
||||||
IP: net.ParseIP("fe80::1"),
|
IP: netip.MustParseAddr("fe80::1"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
IP: net.ParseIP("fe80::"),
|
|
||||||
Mask: net.CIDRMask(64, 128),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
testIP: netip.MustParseAddr("fe80::1"),
|
testIP: netip.MustParseAddr("fe80::1"),
|
||||||
expected: false,
|
expected: false,
|
||||||
|
|||||||
@@ -29,14 +29,15 @@ func (r *PeerRule) ID() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RouteRule struct {
|
type RouteRule struct {
|
||||||
id string
|
id string
|
||||||
mgmtId []byte
|
mgmtId []byte
|
||||||
sources []netip.Prefix
|
sources []netip.Prefix
|
||||||
destination netip.Prefix
|
dstSet firewall.Set
|
||||||
proto firewall.Protocol
|
destinations []netip.Prefix
|
||||||
srcPort *firewall.Port
|
proto firewall.Protocol
|
||||||
dstPort *firewall.Port
|
srcPort *firewall.Port
|
||||||
action firewall.Action
|
dstPort *firewall.Port
|
||||||
|
action firewall.Action
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the rule id
|
// ID returns the rule id
|
||||||
|
|||||||
@@ -38,11 +38,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: net.ParseIP("100.10.0.100"),
|
IP: netip.MustParseAddr("100.10.0.100"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("100.10.0.0/16"),
|
||||||
IP: net.ParseIP("100.10.0.0"),
|
|
||||||
Mask: net.CIDRMask(16, 32),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -198,12 +195,12 @@ func TestTracePacket(t *testing.T) {
|
|||||||
m.forwarder.Store(&forwarder.Forwarder{})
|
m.forwarder.Store(&forwarder.Forwarder{})
|
||||||
|
|
||||||
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
|
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
|
||||||
dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 17, 0, 2}), 32)
|
dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 17, 2}), 32)
|
||||||
_, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, dst, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
|
_, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
packetBuilder: func() *PacketBuilder {
|
packetBuilder: func() *PacketBuilder {
|
||||||
return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
@@ -222,12 +219,12 @@ func TestTracePacket(t *testing.T) {
|
|||||||
m.nativeRouter.Store(false)
|
m.nativeRouter.Store(false)
|
||||||
|
|
||||||
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
|
src := netip.PrefixFrom(netip.AddrFrom4([4]byte{1, 1, 1, 1}), 32)
|
||||||
dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{172, 17, 0, 2}), 32)
|
dst := netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 168, 17, 2}), 32)
|
||||||
_, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, dst, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
|
_, err := m.AddRouteFiltering(nil, []netip.Prefix{src}, fw.Network{Prefix: dst}, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionDrop)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
},
|
},
|
||||||
packetBuilder: func() *PacketBuilder {
|
packetBuilder: func() *PacketBuilder {
|
||||||
return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
@@ -245,7 +242,7 @@ func TestTracePacket(t *testing.T) {
|
|||||||
m.nativeRouter.Store(true)
|
m.nativeRouter.Store(true)
|
||||||
},
|
},
|
||||||
packetBuilder: func() *PacketBuilder {
|
packetBuilder: func() *PacketBuilder {
|
||||||
return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
@@ -263,7 +260,7 @@ func TestTracePacket(t *testing.T) {
|
|||||||
m.routingEnabled.Store(false)
|
m.routingEnabled.Store(false)
|
||||||
},
|
},
|
||||||
packetBuilder: func() *PacketBuilder {
|
packetBuilder: func() *PacketBuilder {
|
||||||
return createPacketBuilder("1.1.1.1", "172.17.0.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
return createPacketBuilder("1.1.1.1", "192.168.17.2", "tcp", 12345, 80, fw.RuleDirectionIN)
|
||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
@@ -425,8 +422,8 @@ func TestTracePacket(t *testing.T) {
|
|||||||
|
|
||||||
require.True(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("100.10.0.100")),
|
require.True(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("100.10.0.100")),
|
||||||
"100.10.0.100 should be recognized as a local IP")
|
"100.10.0.100 should be recognized as a local IP")
|
||||||
require.False(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("172.17.0.2")),
|
require.False(t, m.localipmanager.IsLocalIP(netip.MustParseAddr("192.168.17.2")),
|
||||||
"172.17.0.2 should not be recognized as a local IP")
|
"192.168.17.2 should not be recognized as a local IP")
|
||||||
|
|
||||||
pb := tc.packetBuilder()
|
pb := tc.packetBuilder()
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,12 @@ const (
|
|||||||
// EnvForceUserspaceRouter forces userspace routing even if native routing is available.
|
// EnvForceUserspaceRouter forces userspace routing even if native routing is available.
|
||||||
EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER"
|
EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER"
|
||||||
|
|
||||||
// EnvEnableNetstackLocalForwarding enables forwarding of local traffic to the native stack when running netstack
|
// EnvEnableLocalForwarding enables forwarding of local traffic to the native stack for internal (non-NetBird) interfaces.
|
||||||
// Leaving this on by default introduces a security risk as sockets on listening on localhost only will be accessible
|
// Default off as it might be security risk because sockets listening on localhost only will become accessible.
|
||||||
|
EnvEnableLocalForwarding = "NB_ENABLE_LOCAL_FORWARDING"
|
||||||
|
|
||||||
|
// EnvEnableNetstackLocalForwarding is an alias for EnvEnableLocalForwarding.
|
||||||
|
// In netstack mode, it enables forwarding of local traffic to the native stack for all interfaces.
|
||||||
EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING"
|
EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,10 +53,10 @@ var errNatNotSupported = errors.New("nat not supported with userspace firewall")
|
|||||||
// RuleSet is a set of rules grouped by a string key
|
// RuleSet is a set of rules grouped by a string key
|
||||||
type RuleSet map[string]PeerRule
|
type RuleSet map[string]PeerRule
|
||||||
|
|
||||||
type RouteRules []RouteRule
|
type RouteRules []*RouteRule
|
||||||
|
|
||||||
func (r RouteRules) Sort() {
|
func (r RouteRules) Sort() {
|
||||||
slices.SortStableFunc(r, func(a, b RouteRule) int {
|
slices.SortStableFunc(r, func(a, b *RouteRule) int {
|
||||||
// Deny rules come first
|
// Deny rules come first
|
||||||
if a.action == firewall.ActionDrop && b.action != firewall.ActionDrop {
|
if a.action == firewall.ActionDrop && b.action != firewall.ActionDrop {
|
||||||
return -1
|
return -1
|
||||||
@@ -71,7 +75,6 @@ type Manager struct {
|
|||||||
// incomingRules is used for filtering and hooks
|
// incomingRules is used for filtering and hooks
|
||||||
incomingRules map[netip.Addr]RuleSet
|
incomingRules map[netip.Addr]RuleSet
|
||||||
routeRules RouteRules
|
routeRules RouteRules
|
||||||
wgNetwork *net.IPNet
|
|
||||||
decoders sync.Pool
|
decoders sync.Pool
|
||||||
wgIface common.IFaceMapper
|
wgIface common.IFaceMapper
|
||||||
nativeFirewall firewall.Manager
|
nativeFirewall firewall.Manager
|
||||||
@@ -99,6 +102,8 @@ type Manager struct {
|
|||||||
forwarder atomic.Pointer[forwarder.Forwarder]
|
forwarder atomic.Pointer[forwarder.Forwarder]
|
||||||
logger *nblog.Logger
|
logger *nblog.Logger
|
||||||
flowLogger nftypes.FlowLogger
|
flowLogger nftypes.FlowLogger
|
||||||
|
|
||||||
|
blockRule firewall.Rule
|
||||||
}
|
}
|
||||||
|
|
||||||
// decoder for packages
|
// decoder for packages
|
||||||
@@ -146,6 +151,11 @@ func parseCreateEnv() (bool, bool) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err)
|
log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err)
|
||||||
}
|
}
|
||||||
|
} else if val := os.Getenv(EnvEnableLocalForwarding); val != "" {
|
||||||
|
enableLocalForwarding, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s: %v", EnvEnableLocalForwarding, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return disableConntrack, enableLocalForwarding
|
return disableConntrack, enableLocalForwarding
|
||||||
@@ -201,41 +211,35 @@ func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableSe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.blockInvalidRouted(iface); err != nil {
|
|
||||||
log.Errorf("failed to block invalid routed traffic: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := iface.SetFilter(m); err != nil {
|
if err := iface.SetFilter(m); err != nil {
|
||||||
return nil, fmt.Errorf("set filter: %w", err)
|
return nil, fmt.Errorf("set filter: %w", err)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error {
|
func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) (firewall.Rule, error) {
|
||||||
if m.forwarder.Load() == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
wgPrefix, err := netip.ParsePrefix(iface.Address().Network.String())
|
wgPrefix, err := netip.ParsePrefix(iface.Address().Network.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse wireguard network: %w", err)
|
return nil, fmt.Errorf("parse wireguard network: %w", err)
|
||||||
}
|
}
|
||||||
log.Debugf("blocking invalid routed traffic for %s", wgPrefix)
|
log.Debugf("blocking invalid routed traffic for %s", wgPrefix)
|
||||||
|
|
||||||
if _, err := m.AddRouteFiltering(
|
rule, err := m.addRouteFiltering(
|
||||||
nil,
|
nil,
|
||||||
[]netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)},
|
[]netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)},
|
||||||
wgPrefix,
|
firewall.Network{Prefix: wgPrefix},
|
||||||
firewall.ProtocolALL,
|
firewall.ProtocolALL,
|
||||||
nil,
|
nil,
|
||||||
nil,
|
nil,
|
||||||
firewall.ActionDrop,
|
firewall.ActionDrop,
|
||||||
); err != nil {
|
)
|
||||||
return fmt.Errorf("block wg nte : %w", err)
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("block wg nte : %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Block networks that we're a client of
|
// TODO: Block networks that we're a client of
|
||||||
|
|
||||||
return nil
|
return rule, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) determineRouting() error {
|
func (m *Manager) determineRouting() error {
|
||||||
@@ -273,7 +277,7 @@ func (m *Manager) determineRouting() error {
|
|||||||
|
|
||||||
log.Info("userspace routing is forced")
|
log.Info("userspace routing is forced")
|
||||||
|
|
||||||
case !m.netstack && m.nativeFirewall != nil && m.nativeFirewall.IsServerRouteSupported():
|
case !m.netstack && m.nativeFirewall != nil:
|
||||||
// if the OS supports routing natively, then we don't need to filter/route ourselves
|
// if the OS supports routing natively, then we don't need to filter/route ourselves
|
||||||
// netstack mode won't support native routing as there is no interface
|
// netstack mode won't support native routing as there is no interface
|
||||||
|
|
||||||
@@ -330,6 +334,10 @@ func (m *Manager) IsServerRouteSupported() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) IsStateful() bool {
|
||||||
|
return m.stateful
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
||||||
if m.nativeRouter.Load() && m.nativeFirewall != nil {
|
if m.nativeRouter.Load() && m.nativeFirewall != nil {
|
||||||
return m.nativeFirewall.AddNatRule(pair)
|
return m.nativeFirewall.AddNatRule(pair)
|
||||||
@@ -413,10 +421,23 @@ func (m *Manager) AddPeerFiltering(
|
|||||||
func (m *Manager) AddRouteFiltering(
|
func (m *Manager) AddRouteFiltering(
|
||||||
id []byte,
|
id []byte,
|
||||||
sources []netip.Prefix,
|
sources []netip.Prefix,
|
||||||
destination netip.Prefix,
|
destination firewall.Network,
|
||||||
proto firewall.Protocol,
|
proto firewall.Protocol,
|
||||||
sPort *firewall.Port,
|
sPort, dPort *firewall.Port,
|
||||||
dPort *firewall.Port,
|
action firewall.Action,
|
||||||
|
) (firewall.Rule, error) {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.addRouteFiltering(id, sources, destination, proto, sPort, dPort, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) addRouteFiltering(
|
||||||
|
id []byte,
|
||||||
|
sources []netip.Prefix,
|
||||||
|
destination firewall.Network,
|
||||||
|
proto firewall.Protocol,
|
||||||
|
sPort, dPort *firewall.Port,
|
||||||
action firewall.Action,
|
action firewall.Action,
|
||||||
) (firewall.Rule, error) {
|
) (firewall.Rule, error) {
|
||||||
if m.nativeRouter.Load() && m.nativeFirewall != nil {
|
if m.nativeRouter.Load() && m.nativeFirewall != nil {
|
||||||
@@ -426,34 +447,39 @@ func (m *Manager) AddRouteFiltering(
|
|||||||
ruleID := uuid.New().String()
|
ruleID := uuid.New().String()
|
||||||
rule := RouteRule{
|
rule := RouteRule{
|
||||||
// TODO: consolidate these IDs
|
// TODO: consolidate these IDs
|
||||||
id: ruleID,
|
id: ruleID,
|
||||||
mgmtId: id,
|
mgmtId: id,
|
||||||
sources: sources,
|
sources: sources,
|
||||||
destination: destination,
|
dstSet: destination.Set,
|
||||||
proto: proto,
|
proto: proto,
|
||||||
srcPort: sPort,
|
srcPort: sPort,
|
||||||
dstPort: dPort,
|
dstPort: dPort,
|
||||||
action: action,
|
action: action,
|
||||||
|
}
|
||||||
|
if destination.IsPrefix() {
|
||||||
|
rule.destinations = []netip.Prefix{destination.Prefix}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mutex.Lock()
|
m.routeRules = append(m.routeRules, &rule)
|
||||||
m.routeRules = append(m.routeRules, rule)
|
|
||||||
m.routeRules.Sort()
|
m.routeRules.Sort()
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
return &rule, nil
|
return &rule, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DeleteRouteRule(rule firewall.Rule) error {
|
func (m *Manager) DeleteRouteRule(rule firewall.Rule) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.deleteRouteRule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) deleteRouteRule(rule firewall.Rule) error {
|
||||||
if m.nativeRouter.Load() && m.nativeFirewall != nil {
|
if m.nativeRouter.Load() && m.nativeFirewall != nil {
|
||||||
return m.nativeFirewall.DeleteRouteRule(rule)
|
return m.nativeFirewall.DeleteRouteRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
ruleID := rule.ID()
|
ruleID := rule.ID()
|
||||||
idx := slices.IndexFunc(m.routeRules, func(r RouteRule) bool {
|
idx := slices.IndexFunc(m.routeRules, func(r *RouteRule) bool {
|
||||||
return r.id == ruleID
|
return r.id == ruleID
|
||||||
})
|
})
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
@@ -509,6 +535,52 @@ func (m *Manager) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
return m.nativeFirewall.DeleteDNATRule(rule)
|
return m.nativeFirewall.DeleteDNATRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateSet updates the rule destinations associated with the given set
|
||||||
|
// by merging the existing prefixes with the new ones, then deduplicating.
|
||||||
|
func (m *Manager) UpdateSet(set firewall.Set, prefixes []netip.Prefix) error {
|
||||||
|
if m.nativeRouter.Load() && m.nativeFirewall != nil {
|
||||||
|
return m.nativeFirewall.UpdateSet(set, prefixes)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
var matches []*RouteRule
|
||||||
|
for _, rule := range m.routeRules {
|
||||||
|
if rule.dstSet == set {
|
||||||
|
matches = append(matches, rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return fmt.Errorf("no route rule found for set: %s", set)
|
||||||
|
}
|
||||||
|
|
||||||
|
destinations := matches[0].destinations
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if prefix.Addr().Is4() {
|
||||||
|
destinations = append(destinations, prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(destinations, func(a, b netip.Prefix) int {
|
||||||
|
cmp := a.Addr().Compare(b.Addr())
|
||||||
|
if cmp != 0 {
|
||||||
|
return cmp
|
||||||
|
}
|
||||||
|
return a.Bits() - b.Bits()
|
||||||
|
})
|
||||||
|
|
||||||
|
destinations = slices.Compact(destinations)
|
||||||
|
|
||||||
|
for _, rule := range matches {
|
||||||
|
rule.destinations = destinations
|
||||||
|
}
|
||||||
|
log.Debugf("updated set %s to prefixes %v", set.HashedName(), destinations)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DropOutgoing filter outgoing packets
|
// DropOutgoing filter outgoing packets
|
||||||
func (m *Manager) DropOutgoing(packetData []byte, size int) bool {
|
func (m *Manager) DropOutgoing(packetData []byte, size int) bool {
|
||||||
return m.processOutgoingHooks(packetData, size)
|
return m.processOutgoingHooks(packetData, size)
|
||||||
@@ -546,9 +618,8 @@ func (m *Manager) processOutgoingHooks(packetData []byte, size int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.stateful {
|
// for netflow we keep track even if the firewall is stateless
|
||||||
m.trackOutbound(d, srcIP, dstIP, size)
|
m.trackOutbound(d, srcIP, dstIP, size)
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -717,9 +788,10 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// if running in netstack mode we need to pass this to the forwarder
|
// If requested we pass local traffic to internal interfaces to the forwarder.
|
||||||
if m.netstack && m.localForwarding {
|
// netstack doesn't have an interface to forward packets to the native stack so we always need to use the forwarder.
|
||||||
return m.handleNetstackLocalTraffic(packetData)
|
if m.localForwarding && (m.netstack || dstIP != m.wgIface.Address().IP) {
|
||||||
|
return m.handleForwardedLocalTraffic(packetData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// track inbound packets to get the correct direction and session id for flows
|
// track inbound packets to get the correct direction and session id for flows
|
||||||
@@ -729,8 +801,7 @@ func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP netip.Addr, packet
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) handleNetstackLocalTraffic(packetData []byte) bool {
|
func (m *Manager) handleForwardedLocalTraffic(packetData []byte) bool {
|
||||||
|
|
||||||
fwd := m.forwarder.Load()
|
fwd := m.forwarder.Load()
|
||||||
if fwd == nil {
|
if fwd == nil {
|
||||||
m.logger.Trace("Dropping local packet (forwarder not initialized)")
|
m.logger.Trace("Dropping local packet (forwarder not initialized)")
|
||||||
@@ -764,7 +835,8 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe
|
|||||||
proto, pnum := getProtocolFromPacket(d)
|
proto, pnum := getProtocolFromPacket(d)
|
||||||
srcPort, dstPort := getPortsFromPacket(d)
|
srcPort, dstPort := getPortsFromPacket(d)
|
||||||
|
|
||||||
if ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort); !pass {
|
ruleID, pass := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort)
|
||||||
|
if !pass {
|
||||||
m.logger.Trace("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d",
|
m.logger.Trace("Dropping routed packet (ACL denied): rule_id=%s proto=%v src=%s:%d dst=%s:%d",
|
||||||
ruleID, pnum, srcIP, srcPort, dstIP, dstPort)
|
ruleID, pnum, srcIP, srcPort, dstIP, dstPort)
|
||||||
|
|
||||||
@@ -790,8 +862,11 @@ func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP netip.Addr, packe
|
|||||||
if fwd == nil {
|
if fwd == nil {
|
||||||
m.logger.Trace("failed to forward routed packet (forwarder not initialized)")
|
m.logger.Trace("failed to forward routed packet (forwarder not initialized)")
|
||||||
} else {
|
} else {
|
||||||
|
fwd.RegisterRuleID(srcIP, dstIP, srcPort, dstPort, ruleID)
|
||||||
|
|
||||||
if err := fwd.InjectIncomingPacket(packetData); err != nil {
|
if err := fwd.InjectIncomingPacket(packetData); err != nil {
|
||||||
m.logger.Error("Failed to inject routed packet: %v", err)
|
m.logger.Error("Failed to inject routed packet: %v", err)
|
||||||
|
fwd.DeleteRuleID(srcIP, dstIP, srcPort, dstPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,8 +1063,15 @@ func (m *Manager) routeACLsPass(srcIP, dstIP netip.Addr, proto firewall.Protocol
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool {
|
func (m *Manager) ruleMatches(rule *RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool {
|
||||||
if !rule.destination.Contains(dstAddr) {
|
destMatched := false
|
||||||
|
for _, dst := range rule.destinations {
|
||||||
|
if dst.Contains(dstAddr) {
|
||||||
|
destMatched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !destMatched {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1017,11 +1099,6 @@ func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNetwork of the wireguard interface to which filtering applied
|
|
||||||
func (m *Manager) SetNetwork(network *net.IPNet) {
|
|
||||||
m.wgNetwork = network
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddUDPPacketHook calls hook when UDP packet from given direction matched
|
// AddUDPPacketHook calls hook when UDP packet from given direction matched
|
||||||
//
|
//
|
||||||
// Hook function returns flag which indicates should be the matched package dropped or not
|
// Hook function returns flag which indicates should be the matched package dropped or not
|
||||||
@@ -1091,7 +1168,22 @@ func (m *Manager) EnableRouting() error {
|
|||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
return m.determineRouting()
|
if err := m.determineRouting(); err != nil {
|
||||||
|
return fmt.Errorf("determine routing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.forwarder.Load() == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rule, err := m.blockInvalidRouted(m.wgIface)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("block invalid routed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.blockRule = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DisableRouting() error {
|
func (m *Manager) DisableRouting() error {
|
||||||
@@ -1116,5 +1208,12 @@ func (m *Manager) DisableRouting() error {
|
|||||||
|
|
||||||
log.Debug("forwarder stopped")
|
log.Debug("forwarder stopped")
|
||||||
|
|
||||||
|
if m.blockRule != nil {
|
||||||
|
if err := m.deleteRouteRule(m.blockRule); err != nil {
|
||||||
|
return fmt.Errorf("delete block rule: %w", err)
|
||||||
|
}
|
||||||
|
m.blockRule = nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,11 +174,6 @@ func BenchmarkCoreFiltering(b *testing.B) {
|
|||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply scenario-specific setup
|
// Apply scenario-specific setup
|
||||||
sc.setupFunc(manager)
|
sc.setupFunc(manager)
|
||||||
|
|
||||||
@@ -219,11 +214,6 @@ func BenchmarkStateScaling(b *testing.B) {
|
|||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-populate connection table
|
// Pre-populate connection table
|
||||||
srcIPs := generateRandomIPs(count)
|
srcIPs := generateRandomIPs(count)
|
||||||
dstIPs := generateRandomIPs(count)
|
dstIPs := generateRandomIPs(count)
|
||||||
@@ -267,11 +257,6 @@ func BenchmarkEstablishmentOverhead(b *testing.B) {
|
|||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
}
|
|
||||||
|
|
||||||
srcIP := generateRandomIPs(1)[0]
|
srcIP := generateRandomIPs(1)[0]
|
||||||
dstIP := generateRandomIPs(1)[0]
|
dstIP := generateRandomIPs(1)[0]
|
||||||
outbound := generatePacket(b, srcIP, dstIP, 1024, 80, layers.IPProtocolTCP)
|
outbound := generatePacket(b, srcIP, dstIP, 1024, 80, layers.IPProtocolTCP)
|
||||||
@@ -304,10 +289,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolTCP,
|
proto: layers.IPProtocolTCP,
|
||||||
state: "new",
|
state: "new",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
}
|
|
||||||
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -321,10 +302,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolTCP,
|
proto: layers.IPProtocolTCP,
|
||||||
state: "established",
|
state: "established",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
}
|
|
||||||
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -339,10 +316,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolUDP,
|
proto: layers.IPProtocolUDP,
|
||||||
state: "new",
|
state: "new",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
}
|
|
||||||
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -356,10 +329,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolUDP,
|
proto: layers.IPProtocolUDP,
|
||||||
state: "established",
|
state: "established",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
}
|
|
||||||
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
b.Setenv("NB_DISABLE_CONNTRACK", "1")
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -373,10 +342,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolTCP,
|
proto: layers.IPProtocolTCP,
|
||||||
state: "new",
|
state: "new",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("0.0.0.0"),
|
|
||||||
Mask: net.CIDRMask(0, 32),
|
|
||||||
}
|
|
||||||
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -390,10 +355,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolTCP,
|
proto: layers.IPProtocolTCP,
|
||||||
state: "established",
|
state: "established",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("0.0.0.0"),
|
|
||||||
Mask: net.CIDRMask(0, 32),
|
|
||||||
}
|
|
||||||
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -408,10 +369,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolTCP,
|
proto: layers.IPProtocolTCP,
|
||||||
state: "post_handshake",
|
state: "post_handshake",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("0.0.0.0"),
|
|
||||||
Mask: net.CIDRMask(0, 32),
|
|
||||||
}
|
|
||||||
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -426,10 +383,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolUDP,
|
proto: layers.IPProtocolUDP,
|
||||||
state: "new",
|
state: "new",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("0.0.0.0"),
|
|
||||||
Mask: net.CIDRMask(0, 32),
|
|
||||||
}
|
|
||||||
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -443,10 +396,6 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
proto: layers.IPProtocolUDP,
|
proto: layers.IPProtocolUDP,
|
||||||
state: "established",
|
state: "established",
|
||||||
setupFunc: func(m *Manager) {
|
setupFunc: func(m *Manager) {
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("0.0.0.0"),
|
|
||||||
Mask: net.CIDRMask(0, 32),
|
|
||||||
}
|
|
||||||
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
require.NoError(b, os.Unsetenv("NB_DISABLE_CONNTRACK"))
|
||||||
},
|
},
|
||||||
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
genPackets: func(srcIP, dstIP net.IP) ([]byte, []byte) {
|
||||||
@@ -593,11 +542,6 @@ func BenchmarkLongLivedConnections(b *testing.B) {
|
|||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.SetNetwork(&net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup initial state based on scenario
|
// Setup initial state based on scenario
|
||||||
if sc.rules {
|
if sc.rules {
|
||||||
// Single rule to allow all return traffic from port 80
|
// Single rule to allow all return traffic from port 80
|
||||||
@@ -681,11 +625,6 @@ func BenchmarkShortLivedConnections(b *testing.B) {
|
|||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.SetNetwork(&net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup initial state based on scenario
|
// Setup initial state based on scenario
|
||||||
if sc.rules {
|
if sc.rules {
|
||||||
// Single rule to allow all return traffic from port 80
|
// Single rule to allow all return traffic from port 80
|
||||||
@@ -797,11 +736,6 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
|
|||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.SetNetwork(&net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Setup initial state based on scenario
|
// Setup initial state based on scenario
|
||||||
if sc.rules {
|
if sc.rules {
|
||||||
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
|
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
|
||||||
@@ -882,11 +816,6 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
|
|||||||
require.NoError(b, manager.Close(nil))
|
require.NoError(b, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.SetNetwork(&net.IPNet{
|
|
||||||
IP: net.ParseIP("100.64.0.0"),
|
|
||||||
Mask: net.CIDRMask(10, 32),
|
|
||||||
})
|
|
||||||
|
|
||||||
if sc.rules {
|
if sc.rules {
|
||||||
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
|
_, err := manager.AddPeerFiltering(nil, net.ParseIP("0.0.0.0"), fw.ProtocolTCP, &fw.Port{Values: []uint16{80}}, nil, fw.ActionAccept, "")
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
@@ -1032,7 +961,8 @@ func BenchmarkRouteACLs(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range rules {
|
for _, r := range rules {
|
||||||
_, err := manager.AddRouteFiltering(nil, r.sources, r.dest, r.proto, nil, r.port, fw.ActionAccept)
|
dst := fw.Network{Prefix: r.dest}
|
||||||
|
_, err := manager.AddRouteFiltering(nil, r.sources, dst, r.proto, nil, r.port, fw.ActionAccept)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,15 +15,12 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
"github.com/netbirdio/netbird/client/iface/mocks"
|
"github.com/netbirdio/netbird/client/iface/mocks"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPeerACLFiltering(t *testing.T) {
|
func TestPeerACLFiltering(t *testing.T) {
|
||||||
localIP := net.ParseIP("100.10.0.100")
|
localIP := netip.MustParseAddr("100.10.0.100")
|
||||||
wgNet := &net.IPNet{
|
wgNet := netip.MustParsePrefix("100.10.0.0/16")
|
||||||
IP: net.ParseIP("100.10.0.0"),
|
|
||||||
Mask: net.CIDRMask(16, 32),
|
|
||||||
}
|
|
||||||
|
|
||||||
ifaceMock := &IFaceMock{
|
ifaceMock := &IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
@@ -42,8 +39,6 @@ func TestPeerACLFiltering(t *testing.T) {
|
|||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
})
|
})
|
||||||
|
|
||||||
manager.wgNetwork = wgNet
|
|
||||||
|
|
||||||
err = manager.UpdateLocalIPs()
|
err = manager.UpdateLocalIPs()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -188,6 +183,281 @@ func TestPeerACLFiltering(t *testing.T) {
|
|||||||
ruleAction: fw.ActionAccept,
|
ruleAction: fw.ActionAccept,
|
||||||
shouldBeBlocked: true,
|
shouldBeBlocked: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Allow TCP traffic without port specification",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 443,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allow UDP traffic without port specification",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 53,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolUDP,
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TCP packet doesn't match UDP filter with same port",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 443,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolUDP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{443}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UDP packet doesn't match TCP filter with same port",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 443,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{443}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICMP packet doesn't match TCP filter",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolICMP,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICMP packet doesn't match UDP filter",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolICMP,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolUDP,
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allow TCP traffic within port range",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 8080,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Block TCP traffic outside port range",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 7999,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Edge Case - Port at Range Boundary",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 8100,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UDP Port Range",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 5060,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolUDP,
|
||||||
|
ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{5060, 5070}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allow multiple destination ports",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 8080,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{80, 8080, 443}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allow multiple source ports",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 80,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleSrcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}},
|
||||||
|
ruleAction: fw.ActionAccept,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
// New drop test cases
|
||||||
|
{
|
||||||
|
name: "Drop TCP traffic from WG peer",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 443,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{443}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drop UDP traffic from WG peer",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 53,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolUDP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{53}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drop ICMP traffic from WG peer",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolICMP,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolICMP,
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drop all traffic from WG peer",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 443,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolALL,
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drop traffic from multiple source ports",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 80,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleSrcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drop multiple destination ports",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 8080,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{80, 8080, 443}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drop TCP traffic within port range",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 8080,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Accept TCP traffic outside drop port range",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 7999,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Drop TCP traffic with source port range",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 32100,
|
||||||
|
dstPort: 80,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleSrcPort: &fw.Port{IsRange: true, Values: []uint16{32000, 33000}},
|
||||||
|
ruleAction: fw.ActionDrop,
|
||||||
|
shouldBeBlocked: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mixed rule - drop specific port but allow other ports",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "100.10.0.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 443,
|
||||||
|
ruleIP: "100.10.0.1",
|
||||||
|
ruleProto: fw.ProtocolTCP,
|
||||||
|
ruleDstPort: &fw.Port{Values: []uint16{443}},
|
||||||
|
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) {
|
||||||
@@ -198,6 +468,28 @@ 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 {
|
||||||
|
// add general accept rule to test drop rule
|
||||||
|
// TODO: this only works because 0.0.0.0 is tested last, we need to implement order
|
||||||
|
rules, err := manager.AddPeerFiltering(
|
||||||
|
nil,
|
||||||
|
net.ParseIP("0.0.0.0"),
|
||||||
|
fw.ProtocolALL,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
fw.ActionAccept,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, rules)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
for _, rule := range rules {
|
||||||
|
require.NoError(t, manager.DeletePeerRule(rule))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
rules, err := manager.AddPeerFiltering(
|
rules, err := manager.AddPeerFiltering(
|
||||||
nil,
|
nil,
|
||||||
net.ParseIP(tc.ruleIP),
|
net.ParseIP(tc.ruleIP),
|
||||||
@@ -283,14 +575,13 @@ func setupRoutedManager(tb testing.TB, network string) *Manager {
|
|||||||
dev := mocks.NewMockDevice(ctrl)
|
dev := mocks.NewMockDevice(ctrl)
|
||||||
dev.EXPECT().MTU().Return(1500, nil).AnyTimes()
|
dev.EXPECT().MTU().Return(1500, nil).AnyTimes()
|
||||||
|
|
||||||
localIP, wgNet, err := net.ParseCIDR(network)
|
wgNet := netip.MustParsePrefix(network)
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
ifaceMock := &IFaceMock{
|
ifaceMock := &IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: localIP,
|
IP: wgNet.Addr(),
|
||||||
Network: wgNet,
|
Network: wgNet,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -303,8 +594,8 @@ func setupRoutedManager(tb testing.TB, network string) *Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
manager, err := Create(ifaceMock, false, flowLogger)
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
require.NoError(tb, manager.EnableRouting())
|
|
||||||
require.NoError(tb, err)
|
require.NoError(tb, err)
|
||||||
|
require.NoError(tb, manager.EnableRouting())
|
||||||
require.NotNil(tb, manager)
|
require.NotNil(tb, manager)
|
||||||
require.True(tb, manager.routingEnabled.Load())
|
require.True(tb, manager.routingEnabled.Load())
|
||||||
require.False(tb, manager.nativeRouter.Load())
|
require.False(tb, manager.nativeRouter.Load())
|
||||||
@@ -321,7 +612,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
|
|
||||||
type rule struct {
|
type rule struct {
|
||||||
sources []netip.Prefix
|
sources []netip.Prefix
|
||||||
dest netip.Prefix
|
dest fw.Network
|
||||||
proto fw.Protocol
|
proto fw.Protocol
|
||||||
srcPort *fw.Port
|
srcPort *fw.Port
|
||||||
dstPort *fw.Port
|
dstPort *fw.Port
|
||||||
@@ -347,7 +638,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 443,
|
dstPort: 443,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{443}},
|
dstPort: &fw.Port{Values: []uint16{443}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -363,7 +654,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 443,
|
dstPort: 443,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{443}},
|
dstPort: &fw.Port{Values: []uint16{443}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -379,7 +670,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 443,
|
dstPort: 443,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
dest: netip.MustParsePrefix("0.0.0.0/0"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{443}},
|
dstPort: &fw.Port{Values: []uint16{443}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -395,7 +686,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 53,
|
dstPort: 53,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolUDP,
|
proto: fw.ProtocolUDP,
|
||||||
dstPort: &fw.Port{Values: []uint16{53}},
|
dstPort: &fw.Port{Values: []uint16{53}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -409,7 +700,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
proto: fw.ProtocolICMP,
|
proto: fw.ProtocolICMP,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("0.0.0.0/0"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
proto: fw.ProtocolICMP,
|
proto: fw.ProtocolICMP,
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
},
|
},
|
||||||
@@ -424,7 +715,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolALL,
|
proto: fw.ProtocolALL,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -440,7 +731,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 8080,
|
dstPort: 8080,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -456,7 +747,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -472,7 +763,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -488,7 +779,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
srcPort: &fw.Port{Values: []uint16{12345}},
|
srcPort: &fw.Port{Values: []uint16{12345}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -507,7 +798,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
netip.MustParsePrefix("100.10.0.0/16"),
|
netip.MustParsePrefix("100.10.0.0/16"),
|
||||||
netip.MustParsePrefix("172.16.0.0/16"),
|
netip.MustParsePrefix("172.16.0.0/16"),
|
||||||
},
|
},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -521,7 +812,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
proto: fw.ProtocolICMP,
|
proto: fw.ProtocolICMP,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolALL,
|
proto: fw.ProtocolALL,
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
},
|
},
|
||||||
@@ -536,33 +827,13 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolALL,
|
proto: fw.ProtocolALL,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
},
|
},
|
||||||
shouldPass: true,
|
shouldPass: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Multiple source networks with mismatched protocol",
|
|
||||||
srcIP: "172.16.0.1",
|
|
||||||
dstIP: "192.168.1.100",
|
|
||||||
// Should not match TCP rule
|
|
||||||
proto: fw.ProtocolUDP,
|
|
||||||
srcPort: 12345,
|
|
||||||
dstPort: 80,
|
|
||||||
rule: rule{
|
|
||||||
sources: []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("100.10.0.0/16"),
|
|
||||||
netip.MustParsePrefix("172.16.0.0/16"),
|
|
||||||
},
|
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
|
||||||
proto: fw.ProtocolTCP,
|
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
|
||||||
action: fw.ActionAccept,
|
|
||||||
},
|
|
||||||
shouldPass: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Allow multiple destination ports",
|
name: "Allow multiple destination ports",
|
||||||
srcIP: "100.10.0.1",
|
srcIP: "100.10.0.1",
|
||||||
@@ -572,7 +843,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 8080,
|
dstPort: 8080,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80, 8080, 443}},
|
dstPort: &fw.Port{Values: []uint16{80, 8080, 443}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -588,7 +859,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
srcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}},
|
srcPort: &fw.Port{Values: []uint16{12345, 12346, 12347}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -604,7 +875,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolALL,
|
proto: fw.ProtocolALL,
|
||||||
srcPort: &fw.Port{Values: []uint16{12345}},
|
srcPort: &fw.Port{Values: []uint16{12345}},
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
@@ -621,7 +892,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 8080,
|
dstPort: 8080,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{
|
dstPort: &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
@@ -640,7 +911,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 7999,
|
dstPort: 7999,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{
|
dstPort: &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
@@ -659,7 +930,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
srcPort: &fw.Port{
|
srcPort: &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
@@ -678,7 +949,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 443,
|
dstPort: 443,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
srcPort: &fw.Port{
|
srcPort: &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
@@ -700,7 +971,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 8100,
|
dstPort: 8100,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{
|
dstPort: &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
@@ -719,7 +990,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 5060,
|
dstPort: 5060,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolUDP,
|
proto: fw.ProtocolUDP,
|
||||||
dstPort: &fw.Port{
|
dstPort: &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
@@ -738,7 +1009,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 8080,
|
dstPort: 8080,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolALL,
|
proto: fw.ProtocolALL,
|
||||||
dstPort: &fw.Port{
|
dstPort: &fw.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
@@ -757,7 +1028,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 443,
|
dstPort: 443,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{443}},
|
dstPort: &fw.Port{Values: []uint16{443}},
|
||||||
action: fw.ActionDrop,
|
action: fw.ActionDrop,
|
||||||
@@ -773,7 +1044,7 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
dstPort: 80,
|
dstPort: 80,
|
||||||
rule: rule{
|
rule: rule{
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolALL,
|
proto: fw.ProtocolALL,
|
||||||
action: fw.ActionDrop,
|
action: fw.ActionDrop,
|
||||||
},
|
},
|
||||||
@@ -791,17 +1062,158 @@ func TestRouteACLFiltering(t *testing.T) {
|
|||||||
netip.MustParsePrefix("100.10.0.0/16"),
|
netip.MustParsePrefix("100.10.0.0/16"),
|
||||||
netip.MustParsePrefix("172.16.0.0/16"),
|
netip.MustParsePrefix("172.16.0.0/16"),
|
||||||
},
|
},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionDrop,
|
action: fw.ActionDrop,
|
||||||
},
|
},
|
||||||
shouldPass: false,
|
shouldPass: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Drop empty destination set",
|
||||||
|
srcIP: "172.16.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 80,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("172.16.0.0/16"),
|
||||||
|
},
|
||||||
|
dest: fw.Network{Set: fw.Set{}},
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
|
action: fw.ActionAccept,
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Accept TCP traffic outside drop port range",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 7999,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
dstPort: &fw.Port{IsRange: true, Values: []uint16{8000, 8100}},
|
||||||
|
action: fw.ActionDrop,
|
||||||
|
},
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allow TCP traffic without port specification",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 443,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
action: fw.ActionAccept,
|
||||||
|
},
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Allow UDP traffic without port specification",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 53,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
action: fw.ActionAccept,
|
||||||
|
},
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "TCP packet doesn't match UDP filter with same port",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 80,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
|
action: fw.ActionAccept,
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UDP packet doesn't match TCP filter with same port",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
srcPort: 12345,
|
||||||
|
dstPort: 80,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
|
action: fw.ActionAccept,
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICMP packet doesn't match TCP filter",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolICMP,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
action: fw.ActionAccept,
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ICMP packet doesn't match UDP filter",
|
||||||
|
srcIP: "100.10.0.1",
|
||||||
|
dstIP: "192.168.1.100",
|
||||||
|
proto: fw.ProtocolICMP,
|
||||||
|
rule: rule{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
action: fw.ActionAccept,
|
||||||
|
},
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
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.rule.action == fw.ActionDrop {
|
||||||
|
// add general accept rule to test drop rule
|
||||||
|
rule, err := manager.AddRouteFiltering(
|
||||||
|
nil,
|
||||||
|
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
|
fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
|
fw.ProtocolALL,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
fw.ActionAccept,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rule)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, manager.DeleteRouteRule(rule))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
rule, err := manager.AddRouteFiltering(
|
rule, err := manager.AddRouteFiltering(
|
||||||
nil,
|
nil,
|
||||||
tc.rule.sources,
|
tc.rule.sources,
|
||||||
@@ -836,7 +1248,7 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
rules []struct {
|
rules []struct {
|
||||||
sources []netip.Prefix
|
sources []netip.Prefix
|
||||||
dest netip.Prefix
|
dest fw.Network
|
||||||
proto fw.Protocol
|
proto fw.Protocol
|
||||||
srcPort *fw.Port
|
srcPort *fw.Port
|
||||||
dstPort *fw.Port
|
dstPort *fw.Port
|
||||||
@@ -857,7 +1269,7 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
name: "Drop rules take precedence over accept",
|
name: "Drop rules take precedence over accept",
|
||||||
rules: []struct {
|
rules: []struct {
|
||||||
sources []netip.Prefix
|
sources []netip.Prefix
|
||||||
dest netip.Prefix
|
dest fw.Network
|
||||||
proto fw.Protocol
|
proto fw.Protocol
|
||||||
srcPort *fw.Port
|
srcPort *fw.Port
|
||||||
dstPort *fw.Port
|
dstPort *fw.Port
|
||||||
@@ -866,7 +1278,7 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Accept rule added first
|
// Accept rule added first
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80, 443}},
|
dstPort: &fw.Port{Values: []uint16{80, 443}},
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
@@ -874,7 +1286,7 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Drop rule added second but should be evaluated first
|
// Drop rule added second but should be evaluated first
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{443}},
|
dstPort: &fw.Port{Values: []uint16{443}},
|
||||||
action: fw.ActionDrop,
|
action: fw.ActionDrop,
|
||||||
@@ -912,7 +1324,7 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
name: "Multiple drop rules take precedence",
|
name: "Multiple drop rules take precedence",
|
||||||
rules: []struct {
|
rules: []struct {
|
||||||
sources []netip.Prefix
|
sources []netip.Prefix
|
||||||
dest netip.Prefix
|
dest fw.Network
|
||||||
proto fw.Protocol
|
proto fw.Protocol
|
||||||
srcPort *fw.Port
|
srcPort *fw.Port
|
||||||
dstPort *fw.Port
|
dstPort *fw.Port
|
||||||
@@ -921,14 +1333,14 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Accept all
|
// Accept all
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
dest: netip.MustParsePrefix("0.0.0.0/0"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
proto: fw.ProtocolALL,
|
proto: fw.ProtocolALL,
|
||||||
action: fw.ActionAccept,
|
action: fw.ActionAccept,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Drop specific port
|
// Drop specific port
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{443}},
|
dstPort: &fw.Port{Values: []uint16{443}},
|
||||||
action: fw.ActionDrop,
|
action: fw.ActionDrop,
|
||||||
@@ -936,7 +1348,7 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
{
|
{
|
||||||
// Drop different port
|
// Drop different port
|
||||||
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
dest: fw.Network{Prefix: netip.MustParsePrefix("192.168.1.0/24")},
|
||||||
proto: fw.ProtocolTCP,
|
proto: fw.ProtocolTCP,
|
||||||
dstPort: &fw.Port{Values: []uint16{80}},
|
dstPort: &fw.Port{Values: []uint16{80}},
|
||||||
action: fw.ActionDrop,
|
action: fw.ActionDrop,
|
||||||
@@ -1015,3 +1427,50 @@ func TestRouteACLOrder(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouteACLSet(t *testing.T) {
|
||||||
|
ifaceMock := &IFaceMock{
|
||||||
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
|
AddressFunc: func() wgaddr.Address {
|
||||||
|
return wgaddr.Address{
|
||||||
|
IP: netip.MustParseAddr("100.10.0.100"),
|
||||||
|
Network: netip.MustParsePrefix("100.10.0.0/16"),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, manager.Close(nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
set := fw.NewDomainSet(domain.List{"example.org"})
|
||||||
|
|
||||||
|
// Add rule that uses the set (initially empty)
|
||||||
|
rule, err := manager.AddRouteFiltering(
|
||||||
|
nil,
|
||||||
|
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
|
fw.Network{Set: set},
|
||||||
|
fw.ProtocolTCP,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
fw.ActionAccept,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rule)
|
||||||
|
|
||||||
|
srcIP := netip.MustParseAddr("100.10.0.1")
|
||||||
|
dstIP := netip.MustParseAddr("192.168.1.100")
|
||||||
|
|
||||||
|
// Check that traffic is dropped (empty set shouldn't match anything)
|
||||||
|
_, isAllowed := manager.routeACLsPass(srcIP, dstIP, fw.ProtocolTCP, 12345, 80)
|
||||||
|
require.False(t, isAllowed, "Empty set should not allow any traffic")
|
||||||
|
|
||||||
|
err = manager.UpdateSet(set, []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Now the packet should be allowed
|
||||||
|
_, isAllowed = manager.routeACLsPass(srcIP, dstIP, fw.ProtocolTCP, 12345, 80)
|
||||||
|
require.True(t, isAllowed, "After set update, traffic to the added network should be allowed")
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/netflow"
|
"github.com/netbirdio/netbird/client/internal/netflow"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger = log.NewFromLogrus(logrus.StandardLogger())
|
var logger = log.NewFromLogrus(logrus.StandardLogger())
|
||||||
@@ -270,11 +271,8 @@ func TestNotMatchByIP(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
AddressFunc: func() wgaddr.Address {
|
AddressFunc: func() wgaddr.Address {
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: net.ParseIP("100.10.0.100"),
|
IP: netip.MustParseAddr("100.10.0.100"),
|
||||||
Network: &net.IPNet{
|
Network: netip.MustParsePrefix("100.10.0.0/16"),
|
||||||
IP: net.ParseIP("100.10.0.0"),
|
|
||||||
Mask: net.CIDRMask(16, 32),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -284,10 +282,6 @@ func TestNotMatchByIP(t *testing.T) {
|
|||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.10.0.0"),
|
|
||||||
Mask: net.CIDRMask(16, 32),
|
|
||||||
}
|
|
||||||
|
|
||||||
ip := net.ParseIP("0.0.0.0")
|
ip := net.ParseIP("0.0.0.0")
|
||||||
proto := fw.ProtocolUDP
|
proto := fw.ProtocolUDP
|
||||||
@@ -395,10 +389,6 @@ func TestProcessOutgoingHooks(t *testing.T) {
|
|||||||
}, false, flowLogger)
|
}, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.10.0.0"),
|
|
||||||
Mask: net.CIDRMask(16, 32),
|
|
||||||
}
|
|
||||||
manager.udpTracker.Close()
|
manager.udpTracker.Close()
|
||||||
manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger, flowLogger)
|
manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger, flowLogger)
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -508,11 +498,6 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
|||||||
}, false, flowLogger)
|
}, false, flowLogger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.wgNetwork = &net.IPNet{
|
|
||||||
IP: net.ParseIP("100.10.0.0"),
|
|
||||||
Mask: net.CIDRMask(16, 32),
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.udpTracker.Close() // Close the existing tracker
|
manager.udpTracker.Close() // Close the existing tracker
|
||||||
manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger, flowLogger)
|
manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger, flowLogger)
|
||||||
manager.decoders = sync.Pool{
|
manager.decoders = sync.Pool{
|
||||||
@@ -711,3 +696,203 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateSetMerge(t *testing.T) {
|
||||||
|
ifaceMock := &IFaceMock{
|
||||||
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, manager.Close(nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
set := fw.NewDomainSet(domain.List{"example.org"})
|
||||||
|
|
||||||
|
initialPrefixes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
|
netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
}
|
||||||
|
|
||||||
|
rule, err := manager.AddRouteFiltering(
|
||||||
|
nil,
|
||||||
|
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
|
fw.Network{Set: set},
|
||||||
|
fw.ProtocolTCP,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
fw.ActionAccept,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rule)
|
||||||
|
|
||||||
|
// Update the set with initial prefixes
|
||||||
|
err = manager.UpdateSet(set, initialPrefixes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test initial prefixes work
|
||||||
|
srcIP := netip.MustParseAddr("100.10.0.1")
|
||||||
|
dstIP1 := netip.MustParseAddr("10.0.0.100")
|
||||||
|
dstIP2 := netip.MustParseAddr("192.168.1.100")
|
||||||
|
dstIP3 := netip.MustParseAddr("172.16.0.100")
|
||||||
|
|
||||||
|
_, isAllowed1 := manager.routeACLsPass(srcIP, dstIP1, fw.ProtocolTCP, 12345, 80)
|
||||||
|
_, isAllowed2 := manager.routeACLsPass(srcIP, dstIP2, fw.ProtocolTCP, 12345, 80)
|
||||||
|
_, isAllowed3 := manager.routeACLsPass(srcIP, dstIP3, fw.ProtocolTCP, 12345, 80)
|
||||||
|
|
||||||
|
require.True(t, isAllowed1, "Traffic to 10.0.0.100 should be allowed")
|
||||||
|
require.True(t, isAllowed2, "Traffic to 192.168.1.100 should be allowed")
|
||||||
|
require.False(t, isAllowed3, "Traffic to 172.16.0.100 should be denied")
|
||||||
|
|
||||||
|
newPrefixes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("172.16.0.0/16"),
|
||||||
|
netip.MustParsePrefix("10.1.0.0/24"),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.UpdateSet(set, newPrefixes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that all original prefixes are still included
|
||||||
|
_, isAllowed1 = manager.routeACLsPass(srcIP, dstIP1, fw.ProtocolTCP, 12345, 80)
|
||||||
|
_, isAllowed2 = manager.routeACLsPass(srcIP, dstIP2, fw.ProtocolTCP, 12345, 80)
|
||||||
|
require.True(t, isAllowed1, "Traffic to 10.0.0.100 should still be allowed after update")
|
||||||
|
require.True(t, isAllowed2, "Traffic to 192.168.1.100 should still be allowed after update")
|
||||||
|
|
||||||
|
// Check that new prefixes are included
|
||||||
|
dstIP4 := netip.MustParseAddr("172.16.1.100")
|
||||||
|
dstIP5 := netip.MustParseAddr("10.1.0.50")
|
||||||
|
|
||||||
|
_, isAllowed4 := manager.routeACLsPass(srcIP, dstIP4, fw.ProtocolTCP, 12345, 80)
|
||||||
|
_, isAllowed5 := manager.routeACLsPass(srcIP, dstIP5, fw.ProtocolTCP, 12345, 80)
|
||||||
|
|
||||||
|
require.True(t, isAllowed4, "Traffic to new prefix 172.16.0.0/16 should be allowed")
|
||||||
|
require.True(t, isAllowed5, "Traffic to new prefix 10.1.0.0/24 should be allowed")
|
||||||
|
|
||||||
|
// Verify the rule has all prefixes
|
||||||
|
manager.mutex.RLock()
|
||||||
|
foundRule := false
|
||||||
|
for _, r := range manager.routeRules {
|
||||||
|
if r.id == rule.ID() {
|
||||||
|
foundRule = true
|
||||||
|
require.Len(t, r.destinations, len(initialPrefixes)+len(newPrefixes),
|
||||||
|
"Rule should have all prefixes merged")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manager.mutex.RUnlock()
|
||||||
|
require.True(t, foundRule, "Rule should be found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSetDeduplication(t *testing.T) {
|
||||||
|
ifaceMock := &IFaceMock{
|
||||||
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := Create(ifaceMock, false, flowLogger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
require.NoError(t, manager.Close(nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
set := fw.NewDomainSet(domain.List{"example.org"})
|
||||||
|
|
||||||
|
rule, err := manager.AddRouteFiltering(
|
||||||
|
nil,
|
||||||
|
[]netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
|
fw.Network{Set: set},
|
||||||
|
fw.ProtocolTCP,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
fw.ActionAccept,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rule)
|
||||||
|
|
||||||
|
initialPrefixes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
|
netip.MustParsePrefix("10.0.0.0/24"), // Duplicate
|
||||||
|
netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
netip.MustParsePrefix("192.168.1.0/24"), // Duplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.UpdateSet(set, initialPrefixes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check the internal state for deduplication
|
||||||
|
manager.mutex.RLock()
|
||||||
|
foundRule := false
|
||||||
|
for _, r := range manager.routeRules {
|
||||||
|
if r.id == rule.ID() {
|
||||||
|
foundRule = true
|
||||||
|
// Should have deduplicated to 2 prefixes
|
||||||
|
require.Len(t, r.destinations, 2, "Duplicate prefixes should be removed")
|
||||||
|
|
||||||
|
// Check the prefixes are correct
|
||||||
|
expectedPrefixes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
|
netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
}
|
||||||
|
for i, prefix := range expectedPrefixes {
|
||||||
|
require.True(t, r.destinations[i] == prefix,
|
||||||
|
"Prefix should match expected value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manager.mutex.RUnlock()
|
||||||
|
require.True(t, foundRule, "Rule should be found")
|
||||||
|
|
||||||
|
// Test with overlapping prefixes of different sizes
|
||||||
|
overlappingPrefixes := []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("10.0.0.0/16"), // More general
|
||||||
|
netip.MustParsePrefix("10.0.0.0/24"), // More specific (already exists)
|
||||||
|
netip.MustParsePrefix("192.168.0.0/16"), // More general
|
||||||
|
netip.MustParsePrefix("192.168.1.0/24"), // More specific (already exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = manager.UpdateSet(set, overlappingPrefixes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check that all prefixes are included (no deduplication of overlapping prefixes)
|
||||||
|
manager.mutex.RLock()
|
||||||
|
for _, r := range manager.routeRules {
|
||||||
|
if r.id == rule.ID() {
|
||||||
|
// Should have all 4 prefixes (2 original + 2 new more general ones)
|
||||||
|
require.Len(t, r.destinations, 4,
|
||||||
|
"Overlapping prefixes should not be deduplicated")
|
||||||
|
|
||||||
|
// Verify they're sorted correctly (more specific prefixes should come first)
|
||||||
|
prefixes := make([]string, 0, len(r.destinations))
|
||||||
|
for _, p := range r.destinations {
|
||||||
|
prefixes = append(prefixes, p.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check sorted order
|
||||||
|
require.Equal(t, []string{
|
||||||
|
"10.0.0.0/16",
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.0/16",
|
||||||
|
"192.168.1.0/24",
|
||||||
|
}, prefixes, "Prefixes should be sorted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manager.mutex.RUnlock()
|
||||||
|
|
||||||
|
// Test functionality with all prefixes
|
||||||
|
testCases := []struct {
|
||||||
|
dstIP netip.Addr
|
||||||
|
expected bool
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
{netip.MustParseAddr("10.0.0.100"), true, "IP in both /16 and /24"},
|
||||||
|
{netip.MustParseAddr("10.0.1.100"), true, "IP only in /16"},
|
||||||
|
{netip.MustParseAddr("192.168.1.100"), true, "IP in both /16 and /24"},
|
||||||
|
{netip.MustParseAddr("192.168.2.100"), true, "IP only in /16"},
|
||||||
|
{netip.MustParseAddr("172.16.0.100"), false, "IP not in any prefix"},
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIP := netip.MustParseAddr("100.10.0.1")
|
||||||
|
for _, tc := range testCases {
|
||||||
|
_, isAllowed := manager.routeACLsPass(srcIP, tc.dstIP, fw.ProtocolTCP, 12345, 80)
|
||||||
|
require.Equal(t, tc.expected, isAllowed, tc.desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func (u *udpConn) performFilterCheck(addr net.Addr) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.address.Network.Contains(a.AsSlice()) {
|
if u.address.Network.Contains(a) {
|
||||||
log.Warnf("Address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
log.Warnf("Address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
||||||
return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
return fmt.Errorf("address %s is part of the NetBird network %s, refusing to write", addr, u.address)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var zeroKey wgtypes.Key
|
||||||
|
|
||||||
type KernelConfigurer struct {
|
type KernelConfigurer struct {
|
||||||
deviceName string
|
deviceName string
|
||||||
}
|
}
|
||||||
@@ -201,14 +203,71 @@ func (c *KernelConfigurer) configure(config wgtypes.Config) error {
|
|||||||
func (c *KernelConfigurer) Close() {
|
func (c *KernelConfigurer) Close() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *KernelConfigurer) GetStats(peerKey string) (WGStats, error) {
|
func (c *KernelConfigurer) FullStats() (*Stats, error) {
|
||||||
peer, err := c.getPeer(c.deviceName, peerKey)
|
wg, err := wgctrl.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return WGStats{}, fmt.Errorf("get wireguard stats: %w", err)
|
return nil, fmt.Errorf("wgctl: %w", err)
|
||||||
}
|
}
|
||||||
return WGStats{
|
defer func() {
|
||||||
LastHandshake: peer.LastHandshakeTime,
|
err = wg.Close()
|
||||||
TxBytes: peer.TransmitBytes,
|
if err != nil {
|
||||||
RxBytes: peer.ReceiveBytes,
|
log.Errorf("Got error while closing wgctl: %v", err)
|
||||||
}, nil
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wgDevice, err := wg.Device(c.deviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get device %s: %w", c.deviceName, err)
|
||||||
|
}
|
||||||
|
fullStats := &Stats{
|
||||||
|
DeviceName: wgDevice.Name,
|
||||||
|
PublicKey: wgDevice.PublicKey.String(),
|
||||||
|
ListenPort: wgDevice.ListenPort,
|
||||||
|
FWMark: wgDevice.FirewallMark,
|
||||||
|
Peers: []Peer{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range wgDevice.Peers {
|
||||||
|
peer := Peer{
|
||||||
|
PublicKey: p.PublicKey.String(),
|
||||||
|
AllowedIPs: p.AllowedIPs,
|
||||||
|
TxBytes: p.TransmitBytes,
|
||||||
|
RxBytes: p.ReceiveBytes,
|
||||||
|
LastHandshake: p.LastHandshakeTime,
|
||||||
|
PresharedKey: p.PresharedKey != zeroKey,
|
||||||
|
}
|
||||||
|
if p.Endpoint != nil {
|
||||||
|
peer.Endpoint = *p.Endpoint
|
||||||
|
}
|
||||||
|
fullStats.Peers = append(fullStats.Peers, peer)
|
||||||
|
}
|
||||||
|
return fullStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *KernelConfigurer) GetStats() (map[string]WGStats, error) {
|
||||||
|
stats := make(map[string]WGStats)
|
||||||
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wgctl: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = wg.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Got error while closing wgctl: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wgDevice, err := wg.Device(c.deviceName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get device %s: %w", c.deviceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range wgDevice.Peers {
|
||||||
|
stats[peer.PublicKey.String()] = WGStats{
|
||||||
|
LastHandshake: peer.LastHandshakeTime,
|
||||||
|
TxBytes: peer.TransmitBytes,
|
||||||
|
RxBytes: peer.ReceiveBytes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package configurer
|
package configurer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@@ -17,6 +18,20 @@ import (
|
|||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
privateKey = "private_key"
|
||||||
|
ipcKeyLastHandshakeTimeSec = "last_handshake_time_sec"
|
||||||
|
ipcKeyLastHandshakeTimeNsec = "last_handshake_time_nsec"
|
||||||
|
ipcKeyTxBytes = "tx_bytes"
|
||||||
|
ipcKeyRxBytes = "rx_bytes"
|
||||||
|
allowedIP = "allowed_ip"
|
||||||
|
endpoint = "endpoint"
|
||||||
|
fwmark = "fwmark"
|
||||||
|
listenPort = "listen_port"
|
||||||
|
publicKey = "public_key"
|
||||||
|
presharedKey = "preshared_key"
|
||||||
|
)
|
||||||
|
|
||||||
var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found")
|
var ErrAllowedIPNotFound = fmt.Errorf("allowed IP not found")
|
||||||
|
|
||||||
type WGUSPConfigurer struct {
|
type WGUSPConfigurer struct {
|
||||||
@@ -178,6 +193,15 @@ func (c *WGUSPConfigurer) RemoveAllowedIP(peerKey string, ip string) error {
|
|||||||
return c.device.IpcSet(toWgUserspaceString(config))
|
return c.device.IpcSet(toWgUserspaceString(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *WGUSPConfigurer) FullStats() (*Stats, error) {
|
||||||
|
ipcStr, err := c.device.IpcGet()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("IpcGet failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseStatus(c.deviceName, ipcStr)
|
||||||
|
}
|
||||||
|
|
||||||
// startUAPI starts the UAPI listener for managing the WireGuard interface via external tool
|
// startUAPI starts the UAPI listener for managing the WireGuard interface via external tool
|
||||||
func (t *WGUSPConfigurer) startUAPI() {
|
func (t *WGUSPConfigurer) startUAPI() {
|
||||||
var err error
|
var err error
|
||||||
@@ -217,91 +241,75 @@ func (t *WGUSPConfigurer) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WGUSPConfigurer) GetStats(peerKey string) (WGStats, error) {
|
func (t *WGUSPConfigurer) GetStats() (map[string]WGStats, error) {
|
||||||
ipc, err := t.device.IpcGet()
|
ipc, err := t.device.IpcGet()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return WGStats{}, fmt.Errorf("ipc get: %w", err)
|
return nil, fmt.Errorf("ipc get: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stats, err := findPeerInfo(ipc, peerKey, []string{
|
return parseTransfers(ipc)
|
||||||
"last_handshake_time_sec",
|
|
||||||
"last_handshake_time_nsec",
|
|
||||||
"tx_bytes",
|
|
||||||
"rx_bytes",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return WGStats{}, fmt.Errorf("find peer info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sec, err := strconv.ParseInt(stats["last_handshake_time_sec"], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return WGStats{}, fmt.Errorf("parse handshake sec: %w", err)
|
|
||||||
}
|
|
||||||
nsec, err := strconv.ParseInt(stats["last_handshake_time_nsec"], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return WGStats{}, fmt.Errorf("parse handshake nsec: %w", err)
|
|
||||||
}
|
|
||||||
txBytes, err := strconv.ParseInt(stats["tx_bytes"], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return WGStats{}, fmt.Errorf("parse tx_bytes: %w", err)
|
|
||||||
}
|
|
||||||
rxBytes, err := strconv.ParseInt(stats["rx_bytes"], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return WGStats{}, fmt.Errorf("parse rx_bytes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return WGStats{
|
|
||||||
LastHandshake: time.Unix(sec, nsec),
|
|
||||||
TxBytes: txBytes,
|
|
||||||
RxBytes: rxBytes,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func findPeerInfo(ipcInput string, peerKey string, searchConfigKeys []string) (map[string]string, error) {
|
func parseTransfers(ipc string) (map[string]WGStats, error) {
|
||||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
stats := make(map[string]WGStats)
|
||||||
if err != nil {
|
var (
|
||||||
return nil, fmt.Errorf("parse key: %w", err)
|
currentKey string
|
||||||
}
|
currentStats WGStats
|
||||||
|
hasPeer bool
|
||||||
hexKey := hex.EncodeToString(peerKeyParsed[:])
|
)
|
||||||
|
lines := strings.Split(ipc, "\n")
|
||||||
lines := strings.Split(ipcInput, "\n")
|
|
||||||
|
|
||||||
configFound := map[string]string{}
|
|
||||||
foundPeer := false
|
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
line = strings.TrimSpace(line)
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
// If we're within the details of the found peer and encounter another public key,
|
// If we're within the details of the found peer and encounter another public key,
|
||||||
// this means we're starting another peer's details. So, stop.
|
// this means we're starting another peer's details. So, stop.
|
||||||
if strings.HasPrefix(line, "public_key=") && foundPeer {
|
if strings.HasPrefix(line, "public_key=") {
|
||||||
break
|
peerID := strings.TrimPrefix(line, "public_key=")
|
||||||
}
|
h, err := hex.DecodeString(peerID)
|
||||||
|
if err != nil {
|
||||||
// Identify the peer with the specific public key
|
return nil, fmt.Errorf("decode peerID: %w", err)
|
||||||
if line == fmt.Sprintf("public_key=%s", hexKey) {
|
|
||||||
foundPeer = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, key := range searchConfigKeys {
|
|
||||||
if foundPeer && strings.HasPrefix(line, key+"=") {
|
|
||||||
v := strings.SplitN(line, "=", 2)
|
|
||||||
configFound[v[0]] = v[1]
|
|
||||||
}
|
}
|
||||||
|
currentKey = base64.StdEncoding.EncodeToString(h)
|
||||||
|
currentStats = WGStats{} // Reset stats for the new peer
|
||||||
|
hasPeer = true
|
||||||
|
stats[currentKey] = currentStats
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasPeer {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.SplitN(line, "=", 2)
|
||||||
|
if len(key) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key[0] {
|
||||||
|
case ipcKeyLastHandshakeTimeSec:
|
||||||
|
hs, err := toLastHandshake(key[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
currentStats.LastHandshake = hs
|
||||||
|
stats[currentKey] = currentStats
|
||||||
|
case ipcKeyRxBytes:
|
||||||
|
rxBytes, err := toBytes(key[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse rx_bytes: %w", err)
|
||||||
|
}
|
||||||
|
currentStats.RxBytes = rxBytes
|
||||||
|
stats[currentKey] = currentStats
|
||||||
|
case ipcKeyTxBytes:
|
||||||
|
TxBytes, err := toBytes(key[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse tx_bytes: %w", err)
|
||||||
|
}
|
||||||
|
currentStats.TxBytes = TxBytes
|
||||||
|
stats[currentKey] = currentStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: use multierr
|
return stats, nil
|
||||||
for _, key := range searchConfigKeys {
|
|
||||||
if _, ok := configFound[key]; !ok {
|
|
||||||
return configFound, fmt.Errorf("config key not found: %s", key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !foundPeer {
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrPeerNotFound, peerKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
return configFound, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toWgUserspaceString(wgCfg wgtypes.Config) string {
|
func toWgUserspaceString(wgCfg wgtypes.Config) string {
|
||||||
@@ -355,9 +363,154 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toLastHandshake(stringVar string) (time.Time, error) {
|
||||||
|
sec, err := strconv.ParseInt(stringVar, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("parse handshake sec: %w", err)
|
||||||
|
}
|
||||||
|
return time.Unix(sec, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBytes(s string) (int64, error) {
|
||||||
|
return strconv.ParseInt(s, 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
func getFwmark() int {
|
func getFwmark() int {
|
||||||
if nbnet.AdvancedRouting() {
|
if nbnet.AdvancedRouting() {
|
||||||
return nbnet.ControlPlaneMark
|
return nbnet.ControlPlaneMark
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hexToWireguardKey(hexKey string) (wgtypes.Key, error) {
|
||||||
|
// Decode hex string to bytes
|
||||||
|
keyBytes, err := hex.DecodeString(hexKey)
|
||||||
|
if err != nil {
|
||||||
|
return wgtypes.Key{}, fmt.Errorf("failed to decode hex key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have the right number of bytes (WireGuard keys are 32 bytes)
|
||||||
|
if len(keyBytes) != 32 {
|
||||||
|
return wgtypes.Key{}, fmt.Errorf("invalid key length: expected 32 bytes, got %d", len(keyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to wgtypes.Key
|
||||||
|
var key wgtypes.Key
|
||||||
|
copy(key[:], keyBytes)
|
||||||
|
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseStatus(deviceName, ipcStr string) (*Stats, error) {
|
||||||
|
stats := &Stats{DeviceName: deviceName}
|
||||||
|
var currentPeer *Peer
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(ipcStr), "\n") {
|
||||||
|
if line == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := parts[0]
|
||||||
|
val := parts[1]
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case privateKey:
|
||||||
|
key, err := hexToWireguardKey(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to parse private key: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stats.PublicKey = key.PublicKey().String()
|
||||||
|
case publicKey:
|
||||||
|
// Save previous peer
|
||||||
|
if currentPeer != nil {
|
||||||
|
stats.Peers = append(stats.Peers, *currentPeer)
|
||||||
|
}
|
||||||
|
key, err := hexToWireguardKey(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to parse public key: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentPeer = &Peer{
|
||||||
|
PublicKey: key.String(),
|
||||||
|
}
|
||||||
|
case listenPort:
|
||||||
|
if port, err := strconv.Atoi(val); err == nil {
|
||||||
|
stats.ListenPort = port
|
||||||
|
}
|
||||||
|
case fwmark:
|
||||||
|
if fwmark, err := strconv.Atoi(val); err == nil {
|
||||||
|
stats.FWMark = fwmark
|
||||||
|
}
|
||||||
|
case endpoint:
|
||||||
|
if currentPeer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
host, portStr, err := net.SplitHostPort(strings.Trim(val, "[]"))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to parse endpoint: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to parse endpoint port: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentPeer.Endpoint = net.UDPAddr{
|
||||||
|
IP: net.ParseIP(host),
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
case allowedIP:
|
||||||
|
if currentPeer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_, ipnet, err := net.ParseCIDR(val)
|
||||||
|
if err == nil {
|
||||||
|
currentPeer.AllowedIPs = append(currentPeer.AllowedIPs, *ipnet)
|
||||||
|
}
|
||||||
|
case ipcKeyTxBytes:
|
||||||
|
if currentPeer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rxBytes, err := toBytes(val)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentPeer.TxBytes = rxBytes
|
||||||
|
case ipcKeyRxBytes:
|
||||||
|
if currentPeer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rxBytes, err := toBytes(val)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentPeer.RxBytes = rxBytes
|
||||||
|
|
||||||
|
case ipcKeyLastHandshakeTimeSec:
|
||||||
|
if currentPeer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ts, err := toLastHandshake(val)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
currentPeer.LastHandshake = ts
|
||||||
|
case presharedKey:
|
||||||
|
if currentPeer == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if val != "" {
|
||||||
|
currentPeer.PresharedKey = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if currentPeer != nil {
|
||||||
|
stats.Peers = append(stats.Peers, *currentPeer)
|
||||||
|
}
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package configurer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
)
|
)
|
||||||
@@ -34,58 +32,35 @@ errno=0
|
|||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func Test_findPeerInfo(t *testing.T) {
|
func Test_parseTransfers(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
peerKey string
|
peerKey string
|
||||||
searchKeys []string
|
want WGStats
|
||||||
want map[string]string
|
|
||||||
wantErr bool
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "single",
|
name: "single",
|
||||||
peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376",
|
peerKey: "b85996fecc9c7f1fc6d2572a76eda11d59bcd20be8e543b15ce4bd85a8e75a33",
|
||||||
searchKeys: []string{"tx_bytes"},
|
want: WGStats{
|
||||||
want: map[string]string{
|
TxBytes: 0,
|
||||||
"tx_bytes": "38333",
|
RxBytes: 0,
|
||||||
},
|
},
|
||||||
wantErr: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "multiple",
|
name: "multiple",
|
||||||
peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376",
|
peerKey: "58402e695ba1772b1cc9309755f043251ea77fdcf10fbe63989ceb7e19321376",
|
||||||
searchKeys: []string{"tx_bytes", "rx_bytes"},
|
want: WGStats{
|
||||||
want: map[string]string{
|
TxBytes: 38333,
|
||||||
"tx_bytes": "38333",
|
RxBytes: 2224,
|
||||||
"rx_bytes": "2224",
|
|
||||||
},
|
},
|
||||||
wantErr: false,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "lastpeer",
|
name: "lastpeer",
|
||||||
peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58",
|
peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58",
|
||||||
searchKeys: []string{"tx_bytes", "rx_bytes"},
|
want: WGStats{
|
||||||
want: map[string]string{
|
TxBytes: 1212111,
|
||||||
"tx_bytes": "1212111",
|
RxBytes: 1929999999,
|
||||||
"rx_bytes": "1929999999",
|
|
||||||
},
|
},
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "peer not found",
|
|
||||||
peerKey: "1111111111111111111111111111111111111111111111111111111111111111",
|
|
||||||
searchKeys: nil,
|
|
||||||
want: nil,
|
|
||||||
wantErr: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "key not found",
|
|
||||||
peerKey: "662e14fd594556f522604703340351258903b64f35553763f19426ab2a515c58",
|
|
||||||
searchKeys: []string{"tx_bytes", "unknown_key"},
|
|
||||||
want: map[string]string{
|
|
||||||
"tx_bytes": "1212111",
|
|
||||||
},
|
|
||||||
wantErr: true,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -96,9 +71,19 @@ func Test_findPeerInfo(t *testing.T) {
|
|||||||
key, err := wgtypes.NewKey(res)
|
key, err := wgtypes.NewKey(res)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
got, err := findPeerInfo(ipcFixture, key.String(), tt.searchKeys)
|
stats, err := parseTransfers(ipcFixture)
|
||||||
assert.Equalf(t, tt.wantErr, err != nil, fmt.Sprintf("findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys))
|
if err != nil {
|
||||||
assert.Equalf(t, tt.want, got, "findPeerInfo(%v, %v, %v)", ipcFixture, key.String(), tt.searchKeys)
|
require.NoError(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, ok := stats[key.String()]
|
||||||
|
if !ok {
|
||||||
|
require.True(t, ok)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.want, stat)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
client/iface/configurer/wgshow.go
Normal file
24
client/iface/configurer/wgshow.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package configurer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Peer struct {
|
||||||
|
PublicKey string
|
||||||
|
Endpoint net.UDPAddr
|
||||||
|
AllowedIPs []net.IPNet
|
||||||
|
TxBytes int64
|
||||||
|
RxBytes int64
|
||||||
|
LastHandshake time.Time
|
||||||
|
PresharedKey bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
DeviceName string
|
||||||
|
PublicKey string
|
||||||
|
ListenPort int
|
||||||
|
FWMark int
|
||||||
|
Peers []Peer
|
||||||
|
}
|
||||||
@@ -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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WGTunDevice ignore the WGTunDevice interface on Android because the creation of the tun device is different on this platform
|
// WGTunDevice ignore the WGTunDevice interface on Android because the creation of the tun device is different on this platform
|
||||||
@@ -43,11 +44,11 @@ func NewTunDevice(address wgaddr.Address, port int, key string, mtu int, iceBind
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string) (WGConfigurer, error) {
|
func (t *WGTunDevice) Create(routes []string, dns string, searchDomains domain.List) (WGConfigurer, error) {
|
||||||
log.Info("create tun interface")
|
log.Info("create tun interface")
|
||||||
|
|
||||||
routesString := routesToString(routes)
|
routesString := routesToString(routes)
|
||||||
searchDomainsToString := searchDomainsToString(searchDomains)
|
searchDomainsToString := searchDomainsToString(searchDomains.ToPunycodeList())
|
||||||
|
|
||||||
fd, err := t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, dns, searchDomainsToString, routesString)
|
fd, err := t.tunAdapter.ConfigureInterface(t.address.String(), t.mtu, dns, searchDomainsToString, routesString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -24,9 +23,6 @@ type PacketFilter interface {
|
|||||||
|
|
||||||
// RemovePacketHook removes hook by ID
|
// RemovePacketHook removes hook by ID
|
||||||
RemovePacketHook(hookID string) error
|
RemovePacketHook(hookID string) error
|
||||||
|
|
||||||
// SetNetwork of the wireguard interface to which filtering applied
|
|
||||||
SetNetwork(*net.IPNet)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilteredDevice to override Read or Write of packets
|
// FilteredDevice to override Read or Write of packets
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ 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
|
||||||
dnsAddr := nbnet.GetLastIPFromNetwork(t.address.Network, 1)
|
dnsAddr, err := nbnet.GetLastIPFromNetwork(t.address.Network, 1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("last ip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
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, t.mtu)
|
||||||
log.Debugf("netstack using dns address: %s", dnsAddr)
|
log.Debugf("netstack using dns address: %s", dnsAddr)
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ type WGConfigurer interface {
|
|||||||
AddAllowedIP(peerKey string, allowedIP string) error
|
AddAllowedIP(peerKey string, allowedIP string) error
|
||||||
RemoveAllowedIP(peerKey string, allowedIP string) error
|
RemoveAllowedIP(peerKey string, allowedIP string) error
|
||||||
Close()
|
Close()
|
||||||
GetStats(peerKey string) (configurer.WGStats, error)
|
GetStats() (map[string]configurer.WGStats, error)
|
||||||
|
FullStats() (*configurer.Stats, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,15 @@ func (l *wgLink) assignAddr(address wgaddr.Address) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ip := address.IP.String()
|
ip := address.IP.String()
|
||||||
mask := "0x" + address.Network.Mask.String()
|
|
||||||
|
// Convert prefix length to hex netmask
|
||||||
|
prefixLen := address.Network.Bits()
|
||||||
|
if !address.IP.Is4() {
|
||||||
|
return fmt.Errorf("IPv6 not supported for interface assignment")
|
||||||
|
}
|
||||||
|
|
||||||
|
maskBits := uint32(0xffffffff) << (32 - prefixLen)
|
||||||
|
mask := fmt.Sprintf("0x%08x", maskBits)
|
||||||
|
|
||||||
log.Infof("assign addr %s mask %s to %s interface", ip, mask, l.name)
|
log.Infof("assign addr %s mask %s to %s interface", ip, mask, l.name)
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ 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/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WGTunDevice interface {
|
type WGTunDevice interface {
|
||||||
Create(routes []string, dns string, searchDomains []string) (device.WGConfigurer, error)
|
Create(routes []string, dns string, searchDomains domain.List) (device.WGConfigurer, error)
|
||||||
Up() (*bind.UniversalUDPMuxDefault, error)
|
Up() (*bind.UniversalUDPMuxDefault, error)
|
||||||
UpdateAddr(address wgaddr.Address) error
|
UpdateAddr(address wgaddr.Address) error
|
||||||
WgAddress() wgaddr.Address
|
WgAddress() wgaddr.Address
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ func (w *WGIface) SetFilter(filter device.PacketFilter) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.filter = filter
|
w.filter = filter
|
||||||
w.filter.SetNetwork(w.tun.WgAddress().Network)
|
|
||||||
|
|
||||||
w.tun.FilteredDevice().SetFilter(filter)
|
w.tun.FilteredDevice().SetFilter(filter)
|
||||||
return nil
|
return nil
|
||||||
@@ -212,9 +211,13 @@ func (w *WGIface) GetWGDevice() *wgdevice.Device {
|
|||||||
return w.tun.Device()
|
return w.tun.Device()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStats returns the last handshake time, rx and tx bytes for the given peer
|
// GetStats returns the last handshake time, rx and tx bytes
|
||||||
func (w *WGIface) GetStats(peerKey string) (configurer.WGStats, error) {
|
func (w *WGIface) GetStats() (map[string]configurer.WGStats, error) {
|
||||||
return w.configurer.GetStats(peerKey)
|
return w.configurer.GetStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WGIface) FullStats() (*configurer.Stats, error) {
|
||||||
|
return w.configurer.FullStats()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WGIface) waitUntilRemoved() error {
|
func (w *WGIface) waitUntilRemoved() error {
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
package iface
|
package iface
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
|
)
|
||||||
|
|
||||||
// Create creates a new Wireguard interface, sets a given IP and brings it up.
|
// Create creates a new Wireguard interface, sets a given IP and brings it up.
|
||||||
// Will reuse an existing one.
|
// Will reuse an existing one.
|
||||||
@@ -21,6 +25,6 @@ func (w *WGIface) Create() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateOnAndroid this function make sense on mobile only
|
// CreateOnAndroid this function make sense on mobile only
|
||||||
func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
|
func (w *WGIface) CreateOnAndroid([]string, string, domain.List) error {
|
||||||
return fmt.Errorf("this function has not implemented on non mobile")
|
return fmt.Errorf("this function has not implemented on non mobile")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package iface
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateOnAndroid creates a new Wireguard interface, sets a given IP and brings it up.
|
// CreateOnAndroid creates a new Wireguard interface, sets a given IP and brings it up.
|
||||||
// Will reuse an existing one.
|
// Will reuse an existing one.
|
||||||
func (w *WGIface) CreateOnAndroid(routes []string, dns string, searchDomains []string) error {
|
func (w *WGIface) CreateOnAndroid(routes []string, dns string, searchDomains domain.List) error {
|
||||||
w.mu.Lock()
|
w.mu.Lock()
|
||||||
defer w.mu.Unlock()
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create creates a new Wireguard interface, sets a given IP and brings it up.
|
// Create creates a new Wireguard interface, sets a given IP and brings it up.
|
||||||
@@ -36,6 +38,6 @@ func (w *WGIface) Create() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateOnAndroid this function make sense on mobile only
|
// CreateOnAndroid this function make sense on mobile only
|
||||||
func (w *WGIface) CreateOnAndroid([]string, string, []string) error {
|
func (w *WGIface) CreateOnAndroid([]string, string, domain.List) error {
|
||||||
return fmt.Errorf("this function has not implemented on this platform")
|
return fmt.Errorf("this function has not implemented on this platform")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
net "net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
@@ -90,15 +89,3 @@ func (mr *MockPacketFilterMockRecorder) RemovePacketHook(arg0 interface{}) *gomo
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePacketHook", reflect.TypeOf((*MockPacketFilter)(nil).RemovePacketHook), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePacketHook", reflect.TypeOf((*MockPacketFilter)(nil).RemovePacketHook), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNetwork mocks base method.
|
|
||||||
func (m *MockPacketFilter) SetNetwork(arg0 *net.IPNet) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
m.ctrl.Call(m, "SetNetwork", arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNetwork indicates an expected call of SetNetwork.
|
|
||||||
func (mr *MockPacketFilterMockRecorder) SetNetwork(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetwork", reflect.TypeOf((*MockPacketFilter)(nil).SetNetwork), arg0)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package netstack
|
package netstack
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -15,8 +13,8 @@ import (
|
|||||||
const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY"
|
const EnvSkipProxy = "NB_NETSTACK_SKIP_PROXY"
|
||||||
|
|
||||||
type NetStackTun struct { //nolint:revive
|
type NetStackTun struct { //nolint:revive
|
||||||
address net.IP
|
address netip.Addr
|
||||||
dnsAddress net.IP
|
dnsAddress netip.Addr
|
||||||
mtu int
|
mtu int
|
||||||
listenAddress string
|
listenAddress string
|
||||||
|
|
||||||
@@ -24,7 +22,7 @@ type NetStackTun struct { //nolint:revive
|
|||||||
tundev tun.Device
|
tundev tun.Device
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu int) *NetStackTun {
|
func NewNetStackTun(listenAddress string, address netip.Addr, dnsAddress netip.Addr, mtu int) *NetStackTun {
|
||||||
return &NetStackTun{
|
return &NetStackTun{
|
||||||
address: address,
|
address: address,
|
||||||
dnsAddress: dnsAddress,
|
dnsAddress: dnsAddress,
|
||||||
@@ -34,19 +32,9 @@ func NewNetStackTun(listenAddress string, address net.IP, dnsAddress net.IP, mtu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) {
|
func (t *NetStackTun) Create() (tun.Device, *netstack.Net, error) {
|
||||||
addr, ok := netip.AddrFromSlice(t.address)
|
|
||||||
if !ok {
|
|
||||||
return nil, nil, fmt.Errorf("convert address to netip.Addr: %v", t.address)
|
|
||||||
}
|
|
||||||
|
|
||||||
dnsAddr, ok := netip.AddrFromSlice(t.dnsAddress)
|
|
||||||
if !ok {
|
|
||||||
return nil, nil, fmt.Errorf("convert dns address to netip.Addr: %v", t.dnsAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
nsTunDev, tunNet, err := netstack.CreateNetTUN(
|
nsTunDev, tunNet, err := netstack.CreateNetTUN(
|
||||||
[]netip.Addr{addr.Unmap()},
|
[]netip.Addr{t.address},
|
||||||
[]netip.Addr{dnsAddr.Unmap()},
|
[]netip.Addr{t.dnsAddress},
|
||||||
t.mtu)
|
t.mtu)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|||||||
@@ -2,28 +2,27 @@ package wgaddr
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Address WireGuard parsed address
|
// Address WireGuard parsed address
|
||||||
type Address struct {
|
type Address struct {
|
||||||
IP net.IP
|
IP netip.Addr
|
||||||
Network *net.IPNet
|
Network netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseWGAddress parse a string ("1.2.3.4/24") address to WG Address
|
// ParseWGAddress parse a string ("1.2.3.4/24") address to WG Address
|
||||||
func ParseWGAddress(address string) (Address, error) {
|
func ParseWGAddress(address string) (Address, error) {
|
||||||
ip, network, err := net.ParseCIDR(address)
|
prefix, err := netip.ParsePrefix(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Address{}, err
|
return Address{}, err
|
||||||
}
|
}
|
||||||
return Address{
|
return Address{
|
||||||
IP: ip,
|
IP: prefix.Addr().Unmap(),
|
||||||
Network: network,
|
Network: prefix.Masked(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (addr Address) String() string {
|
func (addr Address) String() string {
|
||||||
maskSize, _ := addr.Network.Mask.Size()
|
return fmt.Sprintf("%s/%d", addr.IP.String(), addr.Network.Bits())
|
||||||
return fmt.Sprintf("%s/%d", addr.IP.String(), maskSize)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
|
|
||||||
!define AUTOSTART_REG_KEY "Software\Microsoft\Windows\CurrentVersion\Run"
|
!define AUTOSTART_REG_KEY "Software\Microsoft\Windows\CurrentVersion\Run"
|
||||||
|
|
||||||
|
!define NETBIRD_DATA_DIR "$COMMONPROGRAMDATA\Netbird"
|
||||||
|
|
||||||
Unicode True
|
Unicode True
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@@ -49,6 +51,10 @@ ShowInstDetails Show
|
|||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
!include "MUI2.nsh"
|
||||||
|
!include LogicLib.nsh
|
||||||
|
!include "nsDialogs.nsh"
|
||||||
|
|
||||||
!define MUI_ICON "${ICON}"
|
!define MUI_ICON "${ICON}"
|
||||||
!define MUI_UNICON "${ICON}"
|
!define MUI_UNICON "${ICON}"
|
||||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
|
||||||
@@ -58,9 +64,6 @@ ShowInstDetails Show
|
|||||||
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
|
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
!include "MUI2.nsh"
|
|
||||||
!include LogicLib.nsh
|
|
||||||
|
|
||||||
!define MUI_ABORTWARNING
|
!define MUI_ABORTWARNING
|
||||||
!define MUI_UNABORTWARNING
|
!define MUI_UNABORTWARNING
|
||||||
|
|
||||||
@@ -70,13 +73,16 @@ ShowInstDetails Show
|
|||||||
|
|
||||||
!insertmacro MUI_PAGE_DIRECTORY
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
; Custom page for autostart checkbox
|
|
||||||
Page custom AutostartPage AutostartPageLeave
|
Page custom AutostartPage AutostartPageLeave
|
||||||
|
|
||||||
!insertmacro MUI_PAGE_INSTFILES
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
!insertmacro MUI_PAGE_FINISH
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_WELCOME
|
||||||
|
|
||||||
|
UninstPage custom un.DeleteDataPage un.DeleteDataPageLeave
|
||||||
|
|
||||||
!insertmacro MUI_UNPAGE_CONFIRM
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
|
||||||
!insertmacro MUI_UNPAGE_INSTFILES
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
@@ -89,6 +95,10 @@ Page custom AutostartPage AutostartPageLeave
|
|||||||
Var AutostartCheckbox
|
Var AutostartCheckbox
|
||||||
Var AutostartEnabled
|
Var AutostartEnabled
|
||||||
|
|
||||||
|
; Variables for uninstall data deletion option
|
||||||
|
Var DeleteDataCheckbox
|
||||||
|
Var DeleteDataEnabled
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
; Function to create the autostart options page
|
; Function to create the autostart options page
|
||||||
@@ -104,8 +114,8 @@ Function AutostartPage
|
|||||||
|
|
||||||
${NSD_CreateCheckbox} 0 20u 100% 10u "Start ${APP_NAME} UI automatically when Windows starts"
|
${NSD_CreateCheckbox} 0 20u 100% 10u "Start ${APP_NAME} UI automatically when Windows starts"
|
||||||
Pop $AutostartCheckbox
|
Pop $AutostartCheckbox
|
||||||
${NSD_Check} $AutostartCheckbox ; Default to checked
|
${NSD_Check} $AutostartCheckbox
|
||||||
StrCpy $AutostartEnabled "1" ; Default to enabled
|
StrCpy $AutostartEnabled "1"
|
||||||
|
|
||||||
nsDialogs::Show
|
nsDialogs::Show
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
@@ -115,6 +125,30 @@ Function AutostartPageLeave
|
|||||||
${NSD_GetState} $AutostartCheckbox $AutostartEnabled
|
${NSD_GetState} $AutostartCheckbox $AutostartEnabled
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|
||||||
|
; Function to create the uninstall data deletion page
|
||||||
|
Function un.DeleteDataPage
|
||||||
|
!insertmacro MUI_HEADER_TEXT "Uninstall Options" "Choose whether to delete ${APP_NAME} data."
|
||||||
|
|
||||||
|
nsDialogs::Create 1018
|
||||||
|
Pop $0
|
||||||
|
|
||||||
|
${If} $0 == error
|
||||||
|
Abort
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${NSD_CreateCheckbox} 0 20u 100% 10u "Delete all ${APP_NAME} configuration and state data (${NETBIRD_DATA_DIR})"
|
||||||
|
Pop $DeleteDataCheckbox
|
||||||
|
${NSD_Uncheck} $DeleteDataCheckbox
|
||||||
|
StrCpy $DeleteDataEnabled "0"
|
||||||
|
|
||||||
|
nsDialogs::Show
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Function to handle leaving the data deletion page
|
||||||
|
Function un.DeleteDataPageLeave
|
||||||
|
${NSD_GetState} $DeleteDataCheckbox $DeleteDataEnabled
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
Function GetAppFromCommand
|
Function GetAppFromCommand
|
||||||
Exch $1
|
Exch $1
|
||||||
Push $2
|
Push $2
|
||||||
@@ -176,10 +210,10 @@ ${EndIf}
|
|||||||
FunctionEnd
|
FunctionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
Section -MainProgram
|
Section -MainProgram
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
# SetOverwrite ifnewer
|
# SetOverwrite ifnewer
|
||||||
SetOutPath "$INSTDIR"
|
SetOutPath "$INSTDIR"
|
||||||
File /r "..\\dist\\netbird_windows_amd64\\"
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
SectionEnd
|
SectionEnd
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
@@ -225,31 +259,58 @@ SectionEnd
|
|||||||
Section Uninstall
|
Section Uninstall
|
||||||
${INSTALL_TYPE}
|
${INSTALL_TYPE}
|
||||||
|
|
||||||
|
DetailPrint "Stopping Netbird service..."
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
|
||||||
|
DetailPrint "Uninstalling Netbird service..."
|
||||||
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
||||||
|
|
||||||
# kill ui client
|
DetailPrint "Terminating Netbird UI process..."
|
||||||
ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
|
||||||
|
|
||||||
; Remove autostart registry entry
|
; Remove autostart registry entry
|
||||||
|
DetailPrint "Removing autostart registry entry if exists..."
|
||||||
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
|
||||||
|
|
||||||
|
; Handle data deletion based on checkbox
|
||||||
|
DetailPrint "Checking if user requested data deletion..."
|
||||||
|
${If} $DeleteDataEnabled == "1"
|
||||||
|
DetailPrint "User opted to delete Netbird data. Removing ${NETBIRD_DATA_DIR}..."
|
||||||
|
ClearErrors
|
||||||
|
RMDir /r "${NETBIRD_DATA_DIR}"
|
||||||
|
IfErrors 0 +2 ; If no errors, jump over the message
|
||||||
|
DetailPrint "Error deleting Netbird data directory. It might be in use or already removed."
|
||||||
|
DetailPrint "Netbird data directory removal complete."
|
||||||
|
${Else}
|
||||||
|
DetailPrint "User did not opt to delete Netbird data."
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
# wait the service uninstall take unblock the executable
|
# wait the service uninstall take unblock the executable
|
||||||
|
DetailPrint "Waiting for service handle to be released..."
|
||||||
Sleep 3000
|
Sleep 3000
|
||||||
|
|
||||||
|
DetailPrint "Deleting application files..."
|
||||||
Delete "$INSTDIR\${UI_APP_EXE}"
|
Delete "$INSTDIR\${UI_APP_EXE}"
|
||||||
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
Delete "$INSTDIR\wintun.dll"
|
Delete "$INSTDIR\wintun.dll"
|
||||||
Delete "$INSTDIR\opengl32.dll"
|
Delete "$INSTDIR\opengl32.dll"
|
||||||
|
DetailPrint "Removing application directory..."
|
||||||
RmDir /r "$INSTDIR"
|
RmDir /r "$INSTDIR"
|
||||||
|
|
||||||
|
DetailPrint "Removing shortcuts..."
|
||||||
SetShellVarContext all
|
SetShellVarContext all
|
||||||
Delete "$DESKTOP\${APP_NAME}.lnk"
|
Delete "$DESKTOP\${APP_NAME}.lnk"
|
||||||
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
|
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
|
||||||
|
|
||||||
|
DetailPrint "Removing registry keys..."
|
||||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
|
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
|
||||||
|
|
||||||
|
DetailPrint "Removing application directory from PATH..."
|
||||||
EnVar::SetHKLM
|
EnVar::SetHKLM
|
||||||
EnVar::DeleteValue "path" "$INSTDIR"
|
EnVar::DeleteValue "path" "$INSTDIR"
|
||||||
|
|
||||||
|
DetailPrint "Uninstallation finished."
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func (r RuleID) ID() string {
|
|||||||
|
|
||||||
func GenerateRouteRuleKey(
|
func GenerateRouteRuleKey(
|
||||||
sources []netip.Prefix,
|
sources []netip.Prefix,
|
||||||
destination netip.Prefix,
|
destination manager.Network,
|
||||||
proto manager.Protocol,
|
proto manager.Protocol,
|
||||||
sPort *manager.Port,
|
sPort *manager.Port,
|
||||||
dPort *manager.Port,
|
dPort *manager.Port,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/internal/acl/id"
|
"github.com/netbirdio/netbird/client/internal/acl/id"
|
||||||
"github.com/netbirdio/netbird/client/ssh"
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ var ErrSourceRangesEmpty = errors.New("sources range is empty")
|
|||||||
|
|
||||||
// Manager is a ACL rules manager
|
// Manager is a ACL rules manager
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
ApplyFiltering(networkMap *mgmProto.NetworkMap)
|
ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRouteFeatureFlag bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type protoMatch struct {
|
type protoMatch struct {
|
||||||
@@ -53,10 +54,15 @@ func NewDefaultManager(fm firewall.Manager) *DefaultManager {
|
|||||||
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
|
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
|
||||||
//
|
//
|
||||||
// If allowByDefault is true it appends allow ALL traffic rules to input and output chains.
|
// If allowByDefault is true it appends allow ALL traffic rules to input and output chains.
|
||||||
func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRouteFeatureFlag bool) {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
|
|
||||||
|
if d.firewall == nil {
|
||||||
|
log.Debug("firewall manager is not supported, skipping firewall rules")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
total := 0
|
total := 0
|
||||||
@@ -68,21 +74,9 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
|||||||
time.Since(start), total)
|
time.Since(start), total)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if d.firewall == nil {
|
|
||||||
log.Debug("firewall manager is not supported, skipping firewall rules")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
d.applyPeerACLs(networkMap)
|
d.applyPeerACLs(networkMap)
|
||||||
|
|
||||||
// If we got empty rules list but management did not set the networkMap.FirewallRulesIsEmpty flag,
|
if err := d.applyRouteACLs(networkMap.RoutesFirewallRules, dnsRouteFeatureFlag); err != nil {
|
||||||
// then the mgmt server is older than the client, and we need to allow all traffic for routes
|
|
||||||
isLegacy := len(networkMap.RoutesFirewallRules) == 0 && !networkMap.RoutesFirewallRulesIsEmpty
|
|
||||||
if err := d.firewall.SetLegacyManagement(isLegacy); err != nil {
|
|
||||||
log.Errorf("failed to set legacy management flag: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := d.applyRouteACLs(networkMap.RoutesFirewallRules); err != nil {
|
|
||||||
log.Errorf("Failed to apply route ACLs: %v", err)
|
log.Errorf("Failed to apply route ACLs: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,16 +170,16 @@ func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) {
|
|||||||
d.peerRulesPairs = newRulePairs
|
d.peerRulesPairs = newRulePairs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultManager) applyRouteACLs(rules []*mgmProto.RouteFirewallRule) error {
|
func (d *DefaultManager) applyRouteACLs(rules []*mgmProto.RouteFirewallRule, dynamicResolver bool) error {
|
||||||
newRouteRules := make(map[id.RuleID]struct{}, len(rules))
|
newRouteRules := make(map[id.RuleID]struct{}, len(rules))
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
|
|
||||||
// Apply new rules - firewall manager will return existing rule ID if already present
|
// Apply new rules - firewall manager will return existing rule ID if already present
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
id, err := d.applyRouteACL(rule)
|
id, err := d.applyRouteACL(rule, dynamicResolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, ErrSourceRangesEmpty) {
|
if errors.Is(err, ErrSourceRangesEmpty) {
|
||||||
log.Debugf("skipping empty rule with destination %s: %v", rule.Destination, err)
|
log.Debugf("skipping empty sources rule with destination %s: %v", rule.Destination, err)
|
||||||
} else {
|
} else {
|
||||||
merr = multierror.Append(merr, fmt.Errorf("add route rule: %w", err))
|
merr = multierror.Append(merr, fmt.Errorf("add route rule: %w", err))
|
||||||
}
|
}
|
||||||
@@ -208,7 +202,7 @@ func (d *DefaultManager) applyRouteACLs(rules []*mgmProto.RouteFirewallRule) err
|
|||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultManager) applyRouteACL(rule *mgmProto.RouteFirewallRule) (id.RuleID, error) {
|
func (d *DefaultManager) applyRouteACL(rule *mgmProto.RouteFirewallRule, dynamicResolver bool) (id.RuleID, error) {
|
||||||
if len(rule.SourceRanges) == 0 {
|
if len(rule.SourceRanges) == 0 {
|
||||||
return "", ErrSourceRangesEmpty
|
return "", ErrSourceRangesEmpty
|
||||||
}
|
}
|
||||||
@@ -222,15 +216,9 @@ func (d *DefaultManager) applyRouteACL(rule *mgmProto.RouteFirewallRule) (id.Rul
|
|||||||
sources = append(sources, source)
|
sources = append(sources, source)
|
||||||
}
|
}
|
||||||
|
|
||||||
var destination netip.Prefix
|
destination, err := determineDestination(rule, dynamicResolver, sources)
|
||||||
if rule.IsDynamic {
|
if err != nil {
|
||||||
destination = getDefault(sources[0])
|
return "", fmt.Errorf("determine destination: %w", err)
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
destination, err = netip.ParsePrefix(rule.Destination)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("parse destination: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol, err := convertToFirewallProtocol(rule.Protocol)
|
protocol, err := convertToFirewallProtocol(rule.Protocol)
|
||||||
@@ -296,8 +284,10 @@ func (d *DefaultManager) protoRuleToFirewallRule(
|
|||||||
case mgmProto.RuleDirection_IN:
|
case mgmProto.RuleDirection_IN:
|
||||||
rules, err = d.addInRules(r.PolicyID, ip, protocol, port, action, ipsetName)
|
rules, err = d.addInRules(r.PolicyID, ip, protocol, port, action, ipsetName)
|
||||||
case mgmProto.RuleDirection_OUT:
|
case mgmProto.RuleDirection_OUT:
|
||||||
// TODO: Remove this soon. Outbound rules are obsolete.
|
if d.firewall.IsStateful() {
|
||||||
// We only maintain this for return traffic (inbound dir) which is now handled by the stateful firewall already
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
// return traffic for outbound connections if firewall is stateless
|
||||||
rules, err = d.addOutRules(r.PolicyID, ip, protocol, port, action, ipsetName)
|
rules, err = d.addOutRules(r.PolicyID, ip, protocol, port, action, ipsetName)
|
||||||
default:
|
default:
|
||||||
return "", nil, fmt.Errorf("invalid direction, skipping firewall rule")
|
return "", nil, fmt.Errorf("invalid direction, skipping firewall rule")
|
||||||
@@ -580,6 +570,33 @@ func convertPortInfo(portInfo *mgmProto.PortInfo) *firewall.Port {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func determineDestination(rule *mgmProto.RouteFirewallRule, dynamicResolver bool, sources []netip.Prefix) (firewall.Network, error) {
|
||||||
|
var destination firewall.Network
|
||||||
|
|
||||||
|
if rule.IsDynamic {
|
||||||
|
if dynamicResolver {
|
||||||
|
if len(rule.Domains) > 0 {
|
||||||
|
destination.Set = firewall.NewDomainSet(domain.FromPunycodeList(rule.Domains))
|
||||||
|
} else {
|
||||||
|
// isDynamic is set but no domains = outdated management server
|
||||||
|
log.Warn("connected to an older version of management server (no domains in rules), using default destination")
|
||||||
|
destination.Prefix = getDefault(sources[0])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// client resolves DNS, we (router) don't know the destination
|
||||||
|
destination.Prefix = getDefault(sources[0])
|
||||||
|
}
|
||||||
|
return destination, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix, err := netip.ParsePrefix(rule.Destination)
|
||||||
|
if err != nil {
|
||||||
|
return destination, fmt.Errorf("parse destination: %w", err)
|
||||||
|
}
|
||||||
|
destination.Prefix = prefix
|
||||||
|
return destination, nil
|
||||||
|
}
|
||||||
|
|
||||||
func getDefault(prefix netip.Prefix) netip.Prefix {
|
func getDefault(prefix netip.Prefix) netip.Prefix {
|
||||||
if prefix.Addr().Is6() {
|
if prefix.Addr().Is6() {
|
||||||
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall"
|
"github.com/netbirdio/netbird/client/firewall"
|
||||||
"github.com/netbirdio/netbird/client/firewall/manager"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
"github.com/netbirdio/netbird/client/internal/acl/mocks"
|
"github.com/netbirdio/netbird/client/internal/acl/mocks"
|
||||||
"github.com/netbirdio/netbird/client/internal/netflow"
|
"github.com/netbirdio/netbird/client/internal/netflow"
|
||||||
@@ -42,35 +43,31 @@ func TestDefaultManager(t *testing.T) {
|
|||||||
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
||||||
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
||||||
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
||||||
ip, network, err := net.ParseCIDR("172.0.0.1/32")
|
network := netip.MustParsePrefix("172.0.0.1/32")
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to parse IP address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
||||||
ifaceMock.EXPECT().Address().Return(wgaddr.Address{
|
ifaceMock.EXPECT().Address().Return(wgaddr.Address{
|
||||||
IP: ip,
|
IP: network.Addr(),
|
||||||
Network: network,
|
Network: network,
|
||||||
}).AnyTimes()
|
}).AnyTimes()
|
||||||
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
||||||
|
|
||||||
// we receive one rule from the management so for testing purposes ignore it
|
|
||||||
fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
|
fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Errorf("create firewall: %v", err)
|
defer func() {
|
||||||
return
|
err = fw.Close(nil)
|
||||||
}
|
require.NoError(t, err)
|
||||||
defer func(fw manager.Manager) {
|
}()
|
||||||
_ = fw.Close(nil)
|
|
||||||
}(fw)
|
|
||||||
acl := NewDefaultManager(fw)
|
acl := NewDefaultManager(fw)
|
||||||
|
|
||||||
t.Run("apply firewall rules", func(t *testing.T) {
|
t.Run("apply firewall rules", func(t *testing.T) {
|
||||||
acl.ApplyFiltering(networkMap)
|
acl.ApplyFiltering(networkMap, false)
|
||||||
|
|
||||||
if len(acl.peerRulesPairs) != 2 {
|
if fw.IsStateful() {
|
||||||
t.Errorf("firewall rules not applied: %v", acl.peerRulesPairs)
|
assert.Equal(t, 0, len(acl.peerRulesPairs))
|
||||||
return
|
} else {
|
||||||
|
assert.Equal(t, 2, len(acl.peerRulesPairs))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -92,14 +89,15 @@ func TestDefaultManager(t *testing.T) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
acl.ApplyFiltering(networkMap)
|
acl.ApplyFiltering(networkMap, false)
|
||||||
|
|
||||||
// we should have one old and one new rule in the existed rules
|
expectedRules := 2
|
||||||
if len(acl.peerRulesPairs) != 2 {
|
if fw.IsStateful() {
|
||||||
t.Errorf("firewall rules not applied")
|
expectedRules = 1 // only the inbound rule
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, expectedRules, len(acl.peerRulesPairs))
|
||||||
|
|
||||||
// check that old rule was removed
|
// check that old rule was removed
|
||||||
previousCount := 0
|
previousCount := 0
|
||||||
for id := range acl.peerRulesPairs {
|
for id := range acl.peerRulesPairs {
|
||||||
@@ -107,26 +105,86 @@ func TestDefaultManager(t *testing.T) {
|
|||||||
previousCount++
|
previousCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if previousCount != 1 {
|
|
||||||
t.Errorf("old rule was not removed")
|
expectedPreviousCount := 0
|
||||||
|
if !fw.IsStateful() {
|
||||||
|
expectedPreviousCount = 1
|
||||||
}
|
}
|
||||||
|
assert.Equal(t, expectedPreviousCount, previousCount)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("handle default rules", func(t *testing.T) {
|
t.Run("handle default rules", func(t *testing.T) {
|
||||||
networkMap.FirewallRules = networkMap.FirewallRules[:0]
|
networkMap.FirewallRules = networkMap.FirewallRules[:0]
|
||||||
|
|
||||||
networkMap.FirewallRulesIsEmpty = true
|
networkMap.FirewallRulesIsEmpty = true
|
||||||
if acl.ApplyFiltering(networkMap); len(acl.peerRulesPairs) != 0 {
|
acl.ApplyFiltering(networkMap, false)
|
||||||
t.Errorf("rules should be empty if FirewallRulesIsEmpty is set, got: %v", len(acl.peerRulesPairs))
|
assert.Equal(t, 0, len(acl.peerRulesPairs))
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
networkMap.FirewallRulesIsEmpty = false
|
networkMap.FirewallRulesIsEmpty = false
|
||||||
acl.ApplyFiltering(networkMap)
|
acl.ApplyFiltering(networkMap, false)
|
||||||
if len(acl.peerRulesPairs) != 1 {
|
|
||||||
t.Errorf("rules should contain 1 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.peerRulesPairs))
|
expectedRules := 1
|
||||||
return
|
if fw.IsStateful() {
|
||||||
|
expectedRules = 1 // only inbound allow-all rule
|
||||||
}
|
}
|
||||||
|
assert.Equal(t, expectedRules, len(acl.peerRulesPairs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultManagerStateless(t *testing.T) {
|
||||||
|
// stateless currently only in userspace, so we have to disable kernel
|
||||||
|
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
||||||
|
t.Setenv("NB_DISABLE_CONNTRACK", "true")
|
||||||
|
|
||||||
|
networkMap := &mgmProto.NetworkMap{
|
||||||
|
FirewallRules: []*mgmProto.FirewallRule{
|
||||||
|
{
|
||||||
|
PeerIP: "10.93.0.1",
|
||||||
|
Direction: mgmProto.RuleDirection_OUT,
|
||||||
|
Action: mgmProto.RuleAction_ACCEPT,
|
||||||
|
Protocol: mgmProto.RuleProtocol_TCP,
|
||||||
|
Port: "80",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
PeerIP: "10.93.0.2",
|
||||||
|
Direction: mgmProto.RuleDirection_IN,
|
||||||
|
Action: mgmProto.RuleAction_ACCEPT,
|
||||||
|
Protocol: mgmProto.RuleProtocol_UDP,
|
||||||
|
Port: "53",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
defer ctrl.Finish()
|
||||||
|
|
||||||
|
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
||||||
|
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
||||||
|
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
||||||
|
network := netip.MustParsePrefix("172.0.0.1/32")
|
||||||
|
|
||||||
|
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
||||||
|
ifaceMock.EXPECT().Address().Return(wgaddr.Address{
|
||||||
|
IP: network.Addr(),
|
||||||
|
Network: network,
|
||||||
|
}).AnyTimes()
|
||||||
|
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
||||||
|
|
||||||
|
fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err = fw.Close(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
acl := NewDefaultManager(fw)
|
||||||
|
|
||||||
|
t.Run("stateless firewall creates outbound rules", func(t *testing.T) {
|
||||||
|
acl.ApplyFiltering(networkMap, false)
|
||||||
|
|
||||||
|
// In stateless mode, we should have both inbound and outbound rules
|
||||||
|
assert.False(t, fw.IsStateful())
|
||||||
|
assert.Equal(t, 2, len(acl.peerRulesPairs))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,42 +250,19 @@ func TestDefaultManagerSquashRules(t *testing.T) {
|
|||||||
|
|
||||||
manager := &DefaultManager{}
|
manager := &DefaultManager{}
|
||||||
rules, _ := manager.squashAcceptRules(networkMap)
|
rules, _ := manager.squashAcceptRules(networkMap)
|
||||||
if len(rules) != 2 {
|
assert.Equal(t, 2, len(rules))
|
||||||
t.Errorf("rules should contain 2, got: %v", rules)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r := rules[0]
|
r := rules[0]
|
||||||
switch {
|
assert.Equal(t, "0.0.0.0", r.PeerIP)
|
||||||
case r.PeerIP != "0.0.0.0":
|
assert.Equal(t, mgmProto.RuleDirection_IN, r.Direction)
|
||||||
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
|
assert.Equal(t, mgmProto.RuleProtocol_ALL, r.Protocol)
|
||||||
return
|
assert.Equal(t, mgmProto.RuleAction_ACCEPT, r.Action)
|
||||||
case r.Direction != mgmProto.RuleDirection_IN:
|
|
||||||
t.Errorf("direction should be IN, got: %v", r.Direction)
|
|
||||||
return
|
|
||||||
case r.Protocol != mgmProto.RuleProtocol_ALL:
|
|
||||||
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
|
|
||||||
return
|
|
||||||
case r.Action != mgmProto.RuleAction_ACCEPT:
|
|
||||||
t.Errorf("action should be ACCEPT, got: %v", r.Action)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r = rules[1]
|
r = rules[1]
|
||||||
switch {
|
assert.Equal(t, "0.0.0.0", r.PeerIP)
|
||||||
case r.PeerIP != "0.0.0.0":
|
assert.Equal(t, mgmProto.RuleDirection_OUT, r.Direction)
|
||||||
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
|
assert.Equal(t, mgmProto.RuleProtocol_ALL, r.Protocol)
|
||||||
return
|
assert.Equal(t, mgmProto.RuleAction_ACCEPT, r.Action)
|
||||||
case r.Direction != mgmProto.RuleDirection_OUT:
|
|
||||||
t.Errorf("direction should be OUT, got: %v", r.Direction)
|
|
||||||
return
|
|
||||||
case r.Protocol != mgmProto.RuleProtocol_ALL:
|
|
||||||
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
|
|
||||||
return
|
|
||||||
case r.Action != mgmProto.RuleAction_ACCEPT:
|
|
||||||
t.Errorf("action should be ACCEPT, got: %v", r.Action)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultManagerSquashRulesNoAffect(t *testing.T) {
|
func TestDefaultManagerSquashRulesNoAffect(t *testing.T) {
|
||||||
@@ -291,9 +326,8 @@ func TestDefaultManagerSquashRulesNoAffect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
manager := &DefaultManager{}
|
manager := &DefaultManager{}
|
||||||
if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) {
|
rules, _ := manager.squashAcceptRules(networkMap)
|
||||||
t.Errorf("we should get the same amount of rules as output, got %v", len(rules))
|
assert.Equal(t, len(networkMap.FirewallRules), len(rules))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultManagerEnableSSHRules(t *testing.T) {
|
func TestDefaultManagerEnableSSHRules(t *testing.T) {
|
||||||
@@ -336,33 +370,29 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) {
|
|||||||
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
||||||
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
||||||
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
||||||
ip, network, err := net.ParseCIDR("172.0.0.1/32")
|
network := netip.MustParsePrefix("172.0.0.1/32")
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to parse IP address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
||||||
ifaceMock.EXPECT().Address().Return(wgaddr.Address{
|
ifaceMock.EXPECT().Address().Return(wgaddr.Address{
|
||||||
IP: ip,
|
IP: network.Addr(),
|
||||||
Network: network,
|
Network: network,
|
||||||
}).AnyTimes()
|
}).AnyTimes()
|
||||||
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
||||||
|
|
||||||
// we receive one rule from the management so for testing purposes ignore it
|
|
||||||
fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
|
fw, err := firewall.NewFirewall(ifaceMock, nil, flowLogger, false)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Errorf("create firewall: %v", err)
|
defer func() {
|
||||||
return
|
err = fw.Close(nil)
|
||||||
}
|
require.NoError(t, err)
|
||||||
defer func(fw manager.Manager) {
|
}()
|
||||||
_ = fw.Close(nil)
|
|
||||||
}(fw)
|
|
||||||
acl := NewDefaultManager(fw)
|
acl := NewDefaultManager(fw)
|
||||||
|
|
||||||
acl.ApplyFiltering(networkMap)
|
acl.ApplyFiltering(networkMap, false)
|
||||||
|
|
||||||
if len(acl.peerRulesPairs) != 3 {
|
expectedRules := 3
|
||||||
t.Errorf("expect 3 rules (last must be SSH), got: %d", len(acl.peerRulesPairs))
|
if fw.IsStateful() {
|
||||||
return
|
expectedRules = 3 // 2 inbound rules + SSH rule
|
||||||
}
|
}
|
||||||
|
assert.Equal(t, expectedRules, len(acl.peerRulesPairs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,13 +64,8 @@ func (t TokenInfo) GetTokenToUse() string {
|
|||||||
// and if that also fails, the authentication process is deemed unsuccessful
|
// and if that also fails, the authentication process is deemed unsuccessful
|
||||||
//
|
//
|
||||||
// On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow
|
// On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow
|
||||||
func NewOAuthFlow(ctx context.Context, config *internal.Config, isLinuxDesktopClient bool) (OAuthFlow, error) {
|
func NewOAuthFlow(ctx context.Context, config *internal.Config, isUnixDesktopClient bool) (OAuthFlow, error) {
|
||||||
if runtime.GOOS == "linux" && !isLinuxDesktopClient {
|
if (runtime.GOOS == "linux" || runtime.GOOS == "freebsd") && !isUnixDesktopClient {
|
||||||
return authenticateWithDeviceCodeFlow(ctx, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// On FreeBSD we currently do not support desktop environments and offer only Device Code Flow (#2384)
|
|
||||||
if runtime.GOOS == "freebsd" {
|
|
||||||
return authenticateWithDeviceCodeFlow(ctx, config)
|
return authenticateWithDeviceCodeFlow(ctx, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,12 @@ func (p *PKCEAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlowIn
|
|||||||
oauth2.SetAuthURLParam("audience", p.providerConfig.Audience),
|
oauth2.SetAuthURLParam("audience", p.providerConfig.Audience),
|
||||||
}
|
}
|
||||||
if !p.providerConfig.DisablePromptLogin {
|
if !p.providerConfig.DisablePromptLogin {
|
||||||
params = append(params, oauth2.SetAuthURLParam("prompt", "login"))
|
if p.providerConfig.LoginFlag.IsPromptLogin() {
|
||||||
|
params = append(params, oauth2.SetAuthURLParam("prompt", "login"))
|
||||||
|
}
|
||||||
|
if p.providerConfig.LoginFlag.IsMaxAge0Login() {
|
||||||
|
params = append(params, oauth2.SetAuthURLParam("max_age", "0"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authURL := p.oAuthConfig.AuthCodeURL(state, params...)
|
authURL := p.oAuthConfig.AuthCodeURL(state, params...)
|
||||||
|
|||||||
@@ -7,15 +7,36 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
mgm "github.com/netbirdio/netbird/management/client/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPromptLogin(t *testing.T) {
|
func TestPromptLogin(t *testing.T) {
|
||||||
|
const (
|
||||||
|
promptLogin = "prompt=login"
|
||||||
|
maxAge0 = "max_age=0"
|
||||||
|
)
|
||||||
|
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
prompt bool
|
loginFlag mgm.LoginFlag
|
||||||
|
disablePromptLogin bool
|
||||||
|
expect string
|
||||||
}{
|
}{
|
||||||
{"PromptLogin", true},
|
{
|
||||||
{"NoPromptLogin", false},
|
name: "Prompt login",
|
||||||
|
loginFlag: mgm.LoginFlagPrompt,
|
||||||
|
expect: promptLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Max age 0 login",
|
||||||
|
loginFlag: mgm.LoginFlagMaxAge0,
|
||||||
|
expect: maxAge0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Disable prompt login",
|
||||||
|
loginFlag: mgm.LoginFlagPrompt,
|
||||||
|
disablePromptLogin: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tt {
|
for _, tc := range tt {
|
||||||
@@ -28,7 +49,7 @@ func TestPromptLogin(t *testing.T) {
|
|||||||
AuthorizationEndpoint: "https://test-auth-endpoint.com/authorize",
|
AuthorizationEndpoint: "https://test-auth-endpoint.com/authorize",
|
||||||
RedirectURLs: []string{"http://127.0.0.1:33992/"},
|
RedirectURLs: []string{"http://127.0.0.1:33992/"},
|
||||||
UseIDToken: true,
|
UseIDToken: true,
|
||||||
DisablePromptLogin: !tc.prompt,
|
LoginFlag: tc.loginFlag,
|
||||||
}
|
}
|
||||||
pkce, err := NewPKCEAuthorizationFlow(config)
|
pkce, err := NewPKCEAuthorizationFlow(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,11 +59,12 @@ func TestPromptLogin(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to request auth info: %v", err)
|
t.Fatalf("Failed to request auth info: %v", err)
|
||||||
}
|
}
|
||||||
pattern := "prompt=login"
|
|
||||||
if tc.prompt {
|
if !tc.disablePromptLogin {
|
||||||
require.Contains(t, authInfo.VerificationURIComplete, pattern)
|
require.Contains(t, authInfo.VerificationURIComplete, tc.expect)
|
||||||
} else {
|
} else {
|
||||||
require.NotContains(t, authInfo.VerificationURIComplete, pattern)
|
require.Contains(t, authInfo.VerificationURIComplete, promptLogin)
|
||||||
|
require.NotContains(t, authInfo.VerificationURIComplete, maxAge0)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,14 @@ type ConfigInput struct {
|
|||||||
DisableServerRoutes *bool
|
DisableServerRoutes *bool
|
||||||
DisableDNS *bool
|
DisableDNS *bool
|
||||||
DisableFirewall *bool
|
DisableFirewall *bool
|
||||||
|
BlockLANAccess *bool
|
||||||
BlockLANAccess *bool
|
BlockInbound *bool
|
||||||
|
|
||||||
DisableNotifications *bool
|
DisableNotifications *bool
|
||||||
|
|
||||||
DNSLabels domain.List
|
DNSLabels domain.List
|
||||||
|
|
||||||
|
LazyConnectionEnabled *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config Configuration type
|
// Config Configuration type
|
||||||
@@ -96,8 +98,8 @@ type Config struct {
|
|||||||
DisableServerRoutes bool
|
DisableServerRoutes bool
|
||||||
DisableDNS bool
|
DisableDNS bool
|
||||||
DisableFirewall bool
|
DisableFirewall bool
|
||||||
|
BlockLANAccess bool
|
||||||
BlockLANAccess bool
|
BlockInbound bool
|
||||||
|
|
||||||
DisableNotifications *bool
|
DisableNotifications *bool
|
||||||
|
|
||||||
@@ -138,6 +140,8 @@ type Config struct {
|
|||||||
ClientCertKeyPath string
|
ClientCertKeyPath string
|
||||||
|
|
||||||
ClientCertKeyPair *tls.Certificate `json:"-"`
|
ClientCertKeyPair *tls.Certificate `json:"-"`
|
||||||
|
|
||||||
|
LazyConnectionEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
||||||
@@ -479,6 +483,16 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
|
|||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.BlockInbound != nil && *input.BlockInbound != config.BlockInbound {
|
||||||
|
if *input.BlockInbound {
|
||||||
|
log.Infof("blocking inbound connections")
|
||||||
|
} else {
|
||||||
|
log.Infof("allowing inbound connections")
|
||||||
|
}
|
||||||
|
config.BlockInbound = *input.BlockInbound
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
if input.DisableNotifications != nil && input.DisableNotifications != config.DisableNotifications {
|
if input.DisableNotifications != nil && input.DisableNotifications != config.DisableNotifications {
|
||||||
if *input.DisableNotifications {
|
if *input.DisableNotifications {
|
||||||
log.Infof("disabling notifications")
|
log.Infof("disabling notifications")
|
||||||
@@ -524,6 +538,12 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
|
|||||||
updated = true
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.LazyConnectionEnabled != nil && *input.LazyConnectionEnabled != config.LazyConnectionEnabled {
|
||||||
|
log.Infof("switching lazy connection to %t", *input.LazyConnectionEnabled)
|
||||||
|
config.LazyConnectionEnabled = *input.LazyConnectionEnabled
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
303
client/internal/conn_mgr.go
Normal file
303
client/internal/conn_mgr.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/lazyconn"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/lazyconn/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer/dispatcher"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peerstore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnMgr coordinates both lazy connections (established on-demand) and permanent peer connections.
|
||||||
|
//
|
||||||
|
// The connection manager is responsible for:
|
||||||
|
// - Managing lazy connections via the lazyConnManager
|
||||||
|
// - Maintaining a list of excluded peers that should always have permanent connections
|
||||||
|
// - Handling connection establishment based on peer signaling
|
||||||
|
//
|
||||||
|
// The implementation is not thread-safe; it is protected by engine.syncMsgMux.
|
||||||
|
type ConnMgr struct {
|
||||||
|
peerStore *peerstore.Store
|
||||||
|
statusRecorder *peer.Status
|
||||||
|
iface lazyconn.WGIface
|
||||||
|
dispatcher *dispatcher.ConnectionDispatcher
|
||||||
|
enabledLocally bool
|
||||||
|
|
||||||
|
lazyConnMgr *manager.Manager
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnMgr(engineConfig *EngineConfig, statusRecorder *peer.Status, peerStore *peerstore.Store, iface lazyconn.WGIface, dispatcher *dispatcher.ConnectionDispatcher) *ConnMgr {
|
||||||
|
e := &ConnMgr{
|
||||||
|
peerStore: peerStore,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
iface: iface,
|
||||||
|
dispatcher: dispatcher,
|
||||||
|
}
|
||||||
|
if engineConfig.LazyConnectionEnabled || lazyconn.IsLazyConnEnabledByEnv() {
|
||||||
|
e.enabledLocally = true
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initializes the connection manager and starts the lazy connection manager if enabled by env var or cmd line option.
|
||||||
|
func (e *ConnMgr) Start(ctx context.Context) {
|
||||||
|
if e.lazyConnMgr != nil {
|
||||||
|
log.Errorf("lazy connection manager is already started")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.enabledLocally {
|
||||||
|
log.Infof("lazy connection manager is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.initLazyManager(ctx)
|
||||||
|
e.statusRecorder.UpdateLazyConnection(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatedRemoteFeatureFlag is called when the remote feature flag is updated.
|
||||||
|
// If enabled, it initializes the lazy connection manager and start it. Do not need to call Start() again.
|
||||||
|
// If disabled, then it closes the lazy connection manager and open the connections to all peers.
|
||||||
|
func (e *ConnMgr) UpdatedRemoteFeatureFlag(ctx context.Context, enabled bool) error {
|
||||||
|
// do not disable lazy connection manager if it was enabled by env var
|
||||||
|
if e.enabledLocally {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
// if the lazy connection manager is already started, do not start it again
|
||||||
|
if e.lazyConnMgr != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("lazy connection manager is enabled by management feature flag")
|
||||||
|
e.initLazyManager(ctx)
|
||||||
|
e.statusRecorder.UpdateLazyConnection(true)
|
||||||
|
return e.addPeersToLazyConnManager(ctx)
|
||||||
|
} else {
|
||||||
|
if e.lazyConnMgr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Infof("lazy connection manager is disabled by management feature flag")
|
||||||
|
e.closeManager(ctx)
|
||||||
|
e.statusRecorder.UpdateLazyConnection(false)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetExcludeList sets the list of peer IDs that should always have permanent connections.
|
||||||
|
func (e *ConnMgr) SetExcludeList(peerIDs map[string]bool) {
|
||||||
|
if e.lazyConnMgr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
excludedPeers := make([]lazyconn.PeerConfig, 0, len(peerIDs))
|
||||||
|
|
||||||
|
for peerID := range peerIDs {
|
||||||
|
var peerConn *peer.Conn
|
||||||
|
var exists bool
|
||||||
|
if peerConn, exists = e.peerStore.PeerConn(peerID); !exists {
|
||||||
|
log.Warnf("failed to find peer conn for peerID: %s", peerID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyPeerCfg := lazyconn.PeerConfig{
|
||||||
|
PublicKey: peerID,
|
||||||
|
AllowedIPs: peerConn.WgConfig().AllowedIps,
|
||||||
|
PeerConnID: peerConn.ConnID(),
|
||||||
|
Log: peerConn.Log,
|
||||||
|
}
|
||||||
|
excludedPeers = append(excludedPeers, lazyPeerCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
added := e.lazyConnMgr.ExcludePeer(e.ctx, excludedPeers)
|
||||||
|
for _, peerID := range added {
|
||||||
|
var peerConn *peer.Conn
|
||||||
|
var exists bool
|
||||||
|
if peerConn, exists = e.peerStore.PeerConn(peerID); !exists {
|
||||||
|
// if the peer not exist in the store, it means that the engine will call the AddPeerConn in next step
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConn.Log.Infof("peer has been added to lazy connection exclude list, opening permanent connection")
|
||||||
|
if err := peerConn.Open(e.ctx); err != nil {
|
||||||
|
peerConn.Log.Errorf("failed to open connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) AddPeerConn(ctx context.Context, peerKey string, conn *peer.Conn) (exists bool) {
|
||||||
|
if success := e.peerStore.AddPeerConn(peerKey, conn); !success {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.isStartedWithLazyMgr() {
|
||||||
|
if err := conn.Open(ctx); err != nil {
|
||||||
|
conn.Log.Errorf("failed to open connection: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lazyconn.IsSupported(conn.AgentVersionString()) {
|
||||||
|
conn.Log.Warnf("peer does not support lazy connection (%s), open permanent connection", conn.AgentVersionString())
|
||||||
|
if err := conn.Open(ctx); err != nil {
|
||||||
|
conn.Log.Errorf("failed to open connection: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyPeerCfg := lazyconn.PeerConfig{
|
||||||
|
PublicKey: peerKey,
|
||||||
|
AllowedIPs: conn.WgConfig().AllowedIps,
|
||||||
|
PeerConnID: conn.ConnID(),
|
||||||
|
Log: conn.Log,
|
||||||
|
}
|
||||||
|
excluded, err := e.lazyConnMgr.AddPeer(lazyPeerCfg)
|
||||||
|
if err != nil {
|
||||||
|
conn.Log.Errorf("failed to add peer to lazyconn manager: %v", err)
|
||||||
|
if err := conn.Open(ctx); err != nil {
|
||||||
|
conn.Log.Errorf("failed to open connection: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if excluded {
|
||||||
|
conn.Log.Infof("peer is on lazy conn manager exclude list, opening connection")
|
||||||
|
if err := conn.Open(ctx); err != nil {
|
||||||
|
conn.Log.Errorf("failed to open connection: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.Log.Infof("peer added to lazy conn manager")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) RemovePeerConn(peerKey string) {
|
||||||
|
conn, ok := e.peerStore.Remove(peerKey)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
if !e.isStartedWithLazyMgr() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.lazyConnMgr.RemovePeer(peerKey)
|
||||||
|
conn.Log.Infof("removed peer from lazy conn manager")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) OnSignalMsg(ctx context.Context, peerKey string) (*peer.Conn, bool) {
|
||||||
|
conn, ok := e.peerStore.PeerConn(peerKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.isStartedWithLazyMgr() {
|
||||||
|
return conn, true
|
||||||
|
}
|
||||||
|
|
||||||
|
if found := e.lazyConnMgr.ActivatePeer(ctx, peerKey); found {
|
||||||
|
conn.Log.Infof("activated peer from inactive state")
|
||||||
|
if err := conn.Open(e.ctx); err != nil {
|
||||||
|
conn.Log.Errorf("failed to open connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return conn, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) Close() {
|
||||||
|
if !e.isStartedWithLazyMgr() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.ctxCancel()
|
||||||
|
e.wg.Wait()
|
||||||
|
e.lazyConnMgr = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) initLazyManager(parentCtx context.Context) {
|
||||||
|
cfg := manager.Config{
|
||||||
|
InactivityThreshold: inactivityThresholdEnv(),
|
||||||
|
}
|
||||||
|
e.lazyConnMgr = manager.NewManager(cfg, e.peerStore, e.iface, e.dispatcher)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
|
e.ctx = ctx
|
||||||
|
e.ctxCancel = cancel
|
||||||
|
|
||||||
|
e.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer e.wg.Done()
|
||||||
|
e.lazyConnMgr.Start(ctx)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) addPeersToLazyConnManager(ctx context.Context) error {
|
||||||
|
peers := e.peerStore.PeersPubKey()
|
||||||
|
lazyPeerCfgs := make([]lazyconn.PeerConfig, 0, len(peers))
|
||||||
|
for _, peerID := range peers {
|
||||||
|
var peerConn *peer.Conn
|
||||||
|
var exists bool
|
||||||
|
if peerConn, exists = e.peerStore.PeerConn(peerID); !exists {
|
||||||
|
log.Warnf("failed to find peer conn for peerID: %s", peerID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyPeerCfg := lazyconn.PeerConfig{
|
||||||
|
PublicKey: peerID,
|
||||||
|
AllowedIPs: peerConn.WgConfig().AllowedIps,
|
||||||
|
PeerConnID: peerConn.ConnID(),
|
||||||
|
Log: peerConn.Log,
|
||||||
|
}
|
||||||
|
lazyPeerCfgs = append(lazyPeerCfgs, lazyPeerCfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.lazyConnMgr.AddActivePeers(ctx, lazyPeerCfgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) closeManager(ctx context.Context) {
|
||||||
|
if e.lazyConnMgr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.ctxCancel()
|
||||||
|
e.wg.Wait()
|
||||||
|
e.lazyConnMgr = nil
|
||||||
|
|
||||||
|
for _, peerID := range e.peerStore.PeersPubKey() {
|
||||||
|
e.peerStore.PeerConnOpen(ctx, peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnMgr) isStartedWithLazyMgr() bool {
|
||||||
|
return e.lazyConnMgr != nil && e.ctxCancel != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func inactivityThresholdEnv() *time.Duration {
|
||||||
|
envValue := os.Getenv(lazyconn.EnvInactivityThreshold)
|
||||||
|
if envValue == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedMinutes, err := strconv.Atoi(envValue)
|
||||||
|
if err != nil || parsedMinutes <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d := time.Duration(parsedMinutes) * time.Minute
|
||||||
|
return &d
|
||||||
|
}
|
||||||
@@ -436,11 +436,13 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
|
|||||||
DNSRouteInterval: config.DNSRouteInterval,
|
DNSRouteInterval: config.DNSRouteInterval,
|
||||||
|
|
||||||
DisableClientRoutes: config.DisableClientRoutes,
|
DisableClientRoutes: config.DisableClientRoutes,
|
||||||
DisableServerRoutes: config.DisableServerRoutes,
|
DisableServerRoutes: config.DisableServerRoutes || config.BlockInbound,
|
||||||
DisableDNS: config.DisableDNS,
|
DisableDNS: config.DisableDNS,
|
||||||
DisableFirewall: config.DisableFirewall,
|
DisableFirewall: config.DisableFirewall,
|
||||||
|
BlockLANAccess: config.BlockLANAccess,
|
||||||
|
BlockInbound: config.BlockInbound,
|
||||||
|
|
||||||
BlockLANAccess: config.BlockLANAccess,
|
LazyConnectionEnabled: config.LazyConnectionEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.PreSharedKey != "" {
|
if config.PreSharedKey != "" {
|
||||||
@@ -481,7 +483,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourP
|
|||||||
return signalClient, nil
|
return signalClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc)
|
// loginToManagement creates Management ServiceDependencies client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc)
|
||||||
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) {
|
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) {
|
||||||
|
|
||||||
serverPublicKey, err := client.GetServerPublicKey()
|
serverPublicKey, err := client.GetServerPublicKey()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -269,11 +270,21 @@ func (g *BundleGenerator) createArchive() error {
|
|||||||
log.Errorf("Failed to add corrupted state files to debug bundle: %v", err)
|
log.Errorf("Failed to add corrupted state files to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.logFile != "console" {
|
if err := g.addWgShow(); err != nil {
|
||||||
if err := g.addLogfile(); err != nil {
|
log.Errorf("Failed to add wg show output: %v", err)
|
||||||
return fmt.Errorf("add log file: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if g.logFile != "console" && g.logFile != "" {
|
||||||
|
if err := g.addLogfile(); err != nil {
|
||||||
|
log.Errorf("Failed to add log file to debug bundle: %v", err)
|
||||||
|
if err := g.trySystemdLogFallback(); err != nil {
|
||||||
|
log.Errorf("Failed to add systemd logs as fallback: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if err := g.trySystemdLogFallback(); err != nil {
|
||||||
|
log.Errorf("Failed to add systemd logs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,17 +376,34 @@ func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder)
|
|||||||
configContent.WriteString(fmt.Sprintf("RosenpassEnabled: %v\n", g.internalConfig.RosenpassEnabled))
|
configContent.WriteString(fmt.Sprintf("RosenpassEnabled: %v\n", g.internalConfig.RosenpassEnabled))
|
||||||
configContent.WriteString(fmt.Sprintf("RosenpassPermissive: %v\n", g.internalConfig.RosenpassPermissive))
|
configContent.WriteString(fmt.Sprintf("RosenpassPermissive: %v\n", g.internalConfig.RosenpassPermissive))
|
||||||
if g.internalConfig.ServerSSHAllowed != nil {
|
if g.internalConfig.ServerSSHAllowed != nil {
|
||||||
configContent.WriteString(fmt.Sprintf("BundleGeneratorSSHAllowed: %v\n", *g.internalConfig.ServerSSHAllowed))
|
configContent.WriteString(fmt.Sprintf("ServerSSHAllowed: %v\n", *g.internalConfig.ServerSSHAllowed))
|
||||||
}
|
}
|
||||||
configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", g.internalConfig.DisableAutoConnect))
|
|
||||||
configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", g.internalConfig.DNSRouteInterval))
|
|
||||||
|
|
||||||
configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", g.internalConfig.DisableClientRoutes))
|
configContent.WriteString(fmt.Sprintf("DisableClientRoutes: %v\n", g.internalConfig.DisableClientRoutes))
|
||||||
configContent.WriteString(fmt.Sprintf("DisableBundleGeneratorRoutes: %v\n", g.internalConfig.DisableServerRoutes))
|
configContent.WriteString(fmt.Sprintf("DisableServerRoutes: %v\n", g.internalConfig.DisableServerRoutes))
|
||||||
configContent.WriteString(fmt.Sprintf("DisableDNS: %v\n", g.internalConfig.DisableDNS))
|
configContent.WriteString(fmt.Sprintf("DisableDNS: %v\n", g.internalConfig.DisableDNS))
|
||||||
configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", g.internalConfig.DisableFirewall))
|
configContent.WriteString(fmt.Sprintf("DisableFirewall: %v\n", g.internalConfig.DisableFirewall))
|
||||||
|
|
||||||
configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", g.internalConfig.BlockLANAccess))
|
configContent.WriteString(fmt.Sprintf("BlockLANAccess: %v\n", g.internalConfig.BlockLANAccess))
|
||||||
|
configContent.WriteString(fmt.Sprintf("BlockInbound: %v\n", g.internalConfig.BlockInbound))
|
||||||
|
|
||||||
|
if g.internalConfig.DisableNotifications != nil {
|
||||||
|
configContent.WriteString(fmt.Sprintf("DisableNotifications: %v\n", *g.internalConfig.DisableNotifications))
|
||||||
|
}
|
||||||
|
|
||||||
|
configContent.WriteString(fmt.Sprintf("DNSLabels: %v\n", g.internalConfig.DNSLabels))
|
||||||
|
|
||||||
|
configContent.WriteString(fmt.Sprintf("DisableAutoConnect: %v\n", g.internalConfig.DisableAutoConnect))
|
||||||
|
|
||||||
|
configContent.WriteString(fmt.Sprintf("DNSRouteInterval: %s\n", g.internalConfig.DNSRouteInterval))
|
||||||
|
|
||||||
|
if g.internalConfig.ClientCertPath != "" {
|
||||||
|
configContent.WriteString(fmt.Sprintf("ClientCertPath: %s\n", g.internalConfig.ClientCertPath))
|
||||||
|
}
|
||||||
|
if g.internalConfig.ClientCertKeyPath != "" {
|
||||||
|
configContent.WriteString(fmt.Sprintf("ClientCertKeyPath: %s\n", g.internalConfig.ClientCertKeyPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
configContent.WriteString(fmt.Sprintf("LazyConnectionEnabled: %v\n", g.internalConfig.LazyConnectionEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *BundleGenerator) addProf() (err error) {
|
func (g *BundleGenerator) addProf() (err error) {
|
||||||
@@ -533,6 +561,33 @@ func (g *BundleGenerator) addLogfile() error {
|
|||||||
return fmt.Errorf("add client log file to zip: %w", err)
|
return fmt.Errorf("add client log file to zip: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add latest rotated log file
|
||||||
|
pattern := filepath.Join(logDir, "client-*.log.gz")
|
||||||
|
files, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to glob rotated logs: %v", err)
|
||||||
|
} else if len(files) > 0 {
|
||||||
|
// pick the file with the latest ModTime
|
||||||
|
sort.Slice(files, func(i, j int) bool {
|
||||||
|
fi, err := os.Stat(files[i])
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to stat rotated log %s: %v", files[i], err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fj, err := os.Stat(files[j])
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to stat rotated log %s: %v", files[j], err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fi.ModTime().Before(fj.ModTime())
|
||||||
|
})
|
||||||
|
latest := files[len(files)-1]
|
||||||
|
name := filepath.Base(latest)
|
||||||
|
if err := g.addSingleLogFileGz(latest, name); err != nil {
|
||||||
|
log.Warnf("failed to add rotated log %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stdErrLogPath := filepath.Join(logDir, errorLogFile)
|
stdErrLogPath := filepath.Join(logDir, errorLogFile)
|
||||||
stdoutLogPath := filepath.Join(logDir, stdoutLogFile)
|
stdoutLogPath := filepath.Join(logDir, stdoutLogFile)
|
||||||
if runtime.GOOS == "darwin" {
|
if runtime.GOOS == "darwin" {
|
||||||
@@ -563,16 +618,13 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var logReader io.Reader
|
var logReader io.Reader = logFile
|
||||||
if g.anonymize {
|
if g.anonymize {
|
||||||
var writer *io.PipeWriter
|
var writer *io.PipeWriter
|
||||||
logReader, writer = io.Pipe()
|
logReader, writer = io.Pipe()
|
||||||
|
|
||||||
go anonymizeLog(logFile, writer, g.anonymizer)
|
go anonymizeLog(logFile, writer, g.anonymizer)
|
||||||
} else {
|
|
||||||
logReader = logFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.addFileToZip(logReader, targetName); err != nil {
|
if err := g.addFileToZip(logReader, targetName); err != nil {
|
||||||
return fmt.Errorf("add %s to zip: %w", targetName, err)
|
return fmt.Errorf("add %s to zip: %w", targetName, err)
|
||||||
}
|
}
|
||||||
@@ -580,6 +632,44 @@ func (g *BundleGenerator) addSingleLogfile(logPath, targetName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addSingleLogFileGz adds a single gzipped log file to the archive
|
||||||
|
func (g *BundleGenerator) addSingleLogFileGz(logPath, targetName string) error {
|
||||||
|
f, err := os.Open(logPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open gz log file %s: %w", targetName, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
gzr, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create gzip reader: %w", err)
|
||||||
|
}
|
||||||
|
defer gzr.Close()
|
||||||
|
|
||||||
|
var logReader io.Reader = gzr
|
||||||
|
if g.anonymize {
|
||||||
|
var pw *io.PipeWriter
|
||||||
|
logReader, pw = io.Pipe()
|
||||||
|
go anonymizeLog(gzr, pw, g.anonymizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
gw := gzip.NewWriter(&buf)
|
||||||
|
if _, err := io.Copy(gw, logReader); err != nil {
|
||||||
|
return fmt.Errorf("re-gzip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gw.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close gzip writer: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.addFileToZip(&buf, targetName); err != nil {
|
||||||
|
return fmt.Errorf("add anonymized gz: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error {
|
func (g *BundleGenerator) addFileToZip(reader io.Reader, filename string) error {
|
||||||
header := &zip.FileHeader{
|
header := &zip.FileHeader{
|
||||||
Name: filename,
|
Name: filename,
|
||||||
|
|||||||
@@ -4,17 +4,104 @@ package debug
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
"github.com/google/nftables/expr"
|
"github.com/google/nftables/expr"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxLogEntries = 100000
|
||||||
|
maxLogAge = 7 * 24 * time.Hour // Last 7 days
|
||||||
|
)
|
||||||
|
|
||||||
|
// trySystemdLogFallback attempts to get logs from systemd journal as fallback
|
||||||
|
func (g *BundleGenerator) trySystemdLogFallback() error {
|
||||||
|
log.Debug("Attempting to collect systemd journal logs")
|
||||||
|
|
||||||
|
serviceName := getServiceName()
|
||||||
|
journalLogs, err := getSystemdLogs(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get systemd logs for %s: %w", serviceName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(journalLogs, "No recent log entries found") {
|
||||||
|
log.Debug("No recent log entries found in systemd journal")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.anonymize {
|
||||||
|
journalLogs = g.anonymizer.AnonymizeString(journalLogs)
|
||||||
|
}
|
||||||
|
|
||||||
|
logReader := strings.NewReader(journalLogs)
|
||||||
|
fileName := fmt.Sprintf("systemd-%s.log", serviceName)
|
||||||
|
if err := g.addFileToZip(logReader, fileName); err != nil {
|
||||||
|
return fmt.Errorf("add systemd logs to bundle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("Added systemd journal logs for %s to debug bundle", serviceName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceName gets the service name from environment or defaults to netbird
|
||||||
|
func getServiceName() string {
|
||||||
|
if unitName := os.Getenv("SYSTEMD_UNIT"); unitName != "" {
|
||||||
|
log.Debugf("Detected SYSTEMD_UNIT environment variable: %s", unitName)
|
||||||
|
return unitName
|
||||||
|
}
|
||||||
|
|
||||||
|
return "netbird"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSystemdLogs retrieves logs from systemd journal for a specific service using journalctl
|
||||||
|
func getSystemdLogs(serviceName string) (string, error) {
|
||||||
|
args := []string{
|
||||||
|
"-u", fmt.Sprintf("%s.service", serviceName),
|
||||||
|
"--since", fmt.Sprintf("-%s", maxLogAge.String()),
|
||||||
|
"--lines", fmt.Sprintf("%d", maxLogEntries),
|
||||||
|
"--no-pager",
|
||||||
|
"--output", "short-iso",
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "journalctl", args...)
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||||
|
return "", fmt.Errorf("journalctl command timed out after 30 seconds")
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error(), "executable file not found") {
|
||||||
|
return "", fmt.Errorf("journalctl command not found: %w", err)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("execute journalctl: %w (stderr: %s)", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
logs := stdout.String()
|
||||||
|
if strings.TrimSpace(logs) == "" {
|
||||||
|
return "No recent log entries found in systemd journal", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
header := fmt.Sprintf("=== Systemd Journal Logs for %s.service (last %d entries, max %s) ===\n",
|
||||||
|
serviceName, maxLogEntries, maxLogAge.String())
|
||||||
|
|
||||||
|
return header + logs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// addFirewallRules collects and adds firewall rules to the archive
|
// addFirewallRules collects and adds firewall rules to the archive
|
||||||
func (g *BundleGenerator) addFirewallRules() error {
|
func (g *BundleGenerator) addFirewallRules() error {
|
||||||
log.Info("Collecting firewall rules")
|
log.Info("Collecting firewall rules")
|
||||||
@@ -59,6 +146,16 @@ func collectIPTablesRules() (string, error) {
|
|||||||
builder.WriteString("\n")
|
builder.WriteString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect ipset information
|
||||||
|
ipsetOutput, err := collectIPSets()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("Failed to collect ipset information: %v", err)
|
||||||
|
} else {
|
||||||
|
builder.WriteString("=== ipset list output ===\n")
|
||||||
|
builder.WriteString(ipsetOutput)
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
builder.WriteString("=== iptables -v -n -L output ===\n")
|
builder.WriteString("=== iptables -v -n -L output ===\n")
|
||||||
|
|
||||||
tables := []string{"filter", "nat", "mangle", "raw", "security"}
|
tables := []string{"filter", "nat", "mangle", "raw", "security"}
|
||||||
@@ -78,6 +175,28 @@ func collectIPTablesRules() (string, error) {
|
|||||||
return builder.String(), nil
|
return builder.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// collectIPSets collects information about ipsets
|
||||||
|
func collectIPSets() (string, error) {
|
||||||
|
cmd := exec.Command("ipset", "list")
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "executable file not found") {
|
||||||
|
return "", fmt.Errorf("ipset command not found: %w", err)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("execute ipset list: %w (stderr: %s)", err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
ipsets := stdout.String()
|
||||||
|
if strings.TrimSpace(ipsets) == "" {
|
||||||
|
return "No ipsets found", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipsets, nil
|
||||||
|
}
|
||||||
|
|
||||||
// collectIPTablesSave uses iptables-save to get rule definitions
|
// collectIPTablesSave uses iptables-save to get rule definitions
|
||||||
func collectIPTablesSave() (string, error) {
|
func collectIPTablesSave() (string, error) {
|
||||||
cmd := exec.Command("iptables-save")
|
cmd := exec.Command("iptables-save")
|
||||||
@@ -449,7 +568,7 @@ func formatExpr(exp expr.Any) string {
|
|||||||
case *expr.Fib:
|
case *expr.Fib:
|
||||||
return formatFib(e)
|
return formatFib(e)
|
||||||
case *expr.Target:
|
case *expr.Target:
|
||||||
return fmt.Sprintf("jump %s", e.Name) // Properly format jump targets
|
return fmt.Sprintf("jump %s", e.Name)
|
||||||
case *expr.Immediate:
|
case *expr.Immediate:
|
||||||
if e.Register == 1 {
|
if e.Register == 1 {
|
||||||
return formatImmediateData(e.Data)
|
return formatImmediateData(e.Data)
|
||||||
|
|||||||
@@ -6,3 +6,9 @@ package debug
|
|||||||
func (g *BundleGenerator) addFirewallRules() error {
|
func (g *BundleGenerator) addFirewallRules() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *BundleGenerator) trySystemdLogFallback() error {
|
||||||
|
// Systemd is only available on Linux
|
||||||
|
// TODO: Add BSD support
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
66
client/internal/debug/wgshow.go
Normal file
66
client/internal/debug/wgshow.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WGIface interface {
|
||||||
|
FullStats() (*configurer.Stats, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BundleGenerator) addWgShow() error {
|
||||||
|
result, err := g.statusRecorder.PeersStatus()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
output := g.toWGShowFormat(result)
|
||||||
|
reader := bytes.NewReader([]byte(output))
|
||||||
|
|
||||||
|
if err := g.addFileToZip(reader, "wgshow.txt"); err != nil {
|
||||||
|
return fmt.Errorf("add wg show to zip: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *BundleGenerator) toWGShowFormat(s *configurer.Stats) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
sb.WriteString(fmt.Sprintf("interface: %s\n", s.DeviceName))
|
||||||
|
sb.WriteString(fmt.Sprintf(" public key: %s\n", s.PublicKey))
|
||||||
|
sb.WriteString(fmt.Sprintf(" listen port: %d\n", s.ListenPort))
|
||||||
|
if s.FWMark != 0 {
|
||||||
|
sb.WriteString(fmt.Sprintf(" fwmark: %#x\n", s.FWMark))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, peer := range s.Peers {
|
||||||
|
sb.WriteString(fmt.Sprintf("\npeer: %s\n", peer.PublicKey))
|
||||||
|
if peer.Endpoint.IP != nil {
|
||||||
|
if g.anonymize {
|
||||||
|
anonEndpoint := g.anonymizer.AnonymizeUDPAddr(peer.Endpoint)
|
||||||
|
sb.WriteString(fmt.Sprintf(" endpoint: %s\n", anonEndpoint.String()))
|
||||||
|
} else {
|
||||||
|
sb.WriteString(fmt.Sprintf(" endpoint: %s\n", peer.Endpoint.String()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(peer.AllowedIPs) > 0 {
|
||||||
|
var ipStrings []string
|
||||||
|
for _, ipnet := range peer.AllowedIPs {
|
||||||
|
ipStrings = append(ipStrings, ipnet.String())
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf(" allowed ips: %s\n", strings.Join(ipStrings, ", ")))
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf(" latest handshake: %s\n", peer.LastHandshake.Format(time.RFC1123)))
|
||||||
|
sb.WriteString(fmt.Sprintf(" transfer: %d B received, %d B sent\n", peer.RxBytes, peer.TxBytes))
|
||||||
|
if peer.PresharedKey {
|
||||||
|
sb.WriteString(" preshared key: (hidden)\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -12,13 +12,14 @@ import (
|
|||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.SimpleRecord, bool) {
|
func createPTRRecord(aRecord nbdns.SimpleRecord, prefix netip.Prefix) (nbdns.SimpleRecord, bool) {
|
||||||
ip := net.ParseIP(aRecord.RData)
|
ip, err := netip.ParseAddr(aRecord.RData)
|
||||||
if ip == nil || ip.To4() == nil {
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse IP address %s: %v", aRecord.RData, err)
|
||||||
return nbdns.SimpleRecord{}, false
|
return nbdns.SimpleRecord{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ipNet.Contains(ip) {
|
if !prefix.Contains(ip) {
|
||||||
return nbdns.SimpleRecord{}, false
|
return nbdns.SimpleRecord{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,16 +37,19 @@ func createPTRRecord(aRecord nbdns.SimpleRecord, ipNet *net.IPNet) (nbdns.Simple
|
|||||||
}
|
}
|
||||||
|
|
||||||
// generateReverseZoneName creates the reverse DNS zone name for a given network
|
// generateReverseZoneName creates the reverse DNS zone name for a given network
|
||||||
func generateReverseZoneName(ipNet *net.IPNet) (string, error) {
|
func generateReverseZoneName(network netip.Prefix) (string, error) {
|
||||||
networkIP := ipNet.IP.Mask(ipNet.Mask)
|
networkIP := network.Masked().Addr()
|
||||||
maskOnes, _ := ipNet.Mask.Size()
|
|
||||||
|
if !networkIP.Is4() {
|
||||||
|
return "", fmt.Errorf("reverse DNS is only supported for IPv4 networks, got: %s", networkIP)
|
||||||
|
}
|
||||||
|
|
||||||
// round up to nearest byte
|
// round up to nearest byte
|
||||||
octetsToUse := (maskOnes + 7) / 8
|
octetsToUse := (network.Bits() + 7) / 8
|
||||||
|
|
||||||
octets := strings.Split(networkIP.String(), ".")
|
octets := strings.Split(networkIP.String(), ".")
|
||||||
if octetsToUse > len(octets) {
|
if octetsToUse > len(octets) {
|
||||||
return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", maskOnes)
|
return "", fmt.Errorf("invalid network mask size for reverse DNS: %d", network.Bits())
|
||||||
}
|
}
|
||||||
|
|
||||||
reverseOctets := make([]string, octetsToUse)
|
reverseOctets := make([]string, octetsToUse)
|
||||||
@@ -68,7 +72,7 @@ func zoneExists(config *nbdns.Config, zoneName string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// collectPTRRecords gathers all PTR records for the given network from A records
|
// collectPTRRecords gathers all PTR records for the given network from A records
|
||||||
func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRecord {
|
func collectPTRRecords(config *nbdns.Config, prefix netip.Prefix) []nbdns.SimpleRecord {
|
||||||
var records []nbdns.SimpleRecord
|
var records []nbdns.SimpleRecord
|
||||||
|
|
||||||
for _, zone := range config.CustomZones {
|
for _, zone := range config.CustomZones {
|
||||||
@@ -77,7 +81,7 @@ func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRec
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ptrRecord, ok := createPTRRecord(record, ipNet); ok {
|
if ptrRecord, ok := createPTRRecord(record, prefix); ok {
|
||||||
records = append(records, ptrRecord)
|
records = append(records, ptrRecord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,8 +91,8 @@ func collectPTRRecords(config *nbdns.Config, ipNet *net.IPNet) []nbdns.SimpleRec
|
|||||||
}
|
}
|
||||||
|
|
||||||
// addReverseZone adds a reverse DNS zone to the configuration for the given network
|
// addReverseZone adds a reverse DNS zone to the configuration for the given network
|
||||||
func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) {
|
func addReverseZone(config *nbdns.Config, network netip.Prefix) {
|
||||||
zoneName, err := generateReverseZoneName(ipNet)
|
zoneName, err := generateReverseZoneName(network)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn(err)
|
log.Warn(err)
|
||||||
return
|
return
|
||||||
@@ -99,7 +103,7 @@ func addReverseZone(config *nbdns.Config, ipNet *net.IPNet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
records := collectPTRRecords(config, ipNet)
|
records := collectPTRRecords(config, network)
|
||||||
|
|
||||||
reverseZone := nbdns.CustomZone{
|
reverseZone := nbdns.CustomZone{
|
||||||
Domain: zoneName,
|
Domain: zoneName,
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ func searchDomains(config HostDNSConfig) []string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
listOfDomains = append(listOfDomains, strings.TrimSuffix(dConf.Domain, "."))
|
listOfDomains = append(listOfDomains, strings.TrimSuffix(dConf.Domain.PunycodeString(), "."))
|
||||||
}
|
}
|
||||||
return listOfDomains
|
return listOfDomains
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -23,8 +26,8 @@ type SubdomainMatcher interface {
|
|||||||
type HandlerEntry struct {
|
type HandlerEntry struct {
|
||||||
Handler dns.Handler
|
Handler dns.Handler
|
||||||
Priority int
|
Priority int
|
||||||
Pattern string
|
Pattern domain.Domain
|
||||||
OrigPattern string
|
OrigPattern domain.Domain
|
||||||
IsWildcard bool
|
IsWildcard bool
|
||||||
MatchSubdomains bool
|
MatchSubdomains bool
|
||||||
}
|
}
|
||||||
@@ -38,7 +41,7 @@ type HandlerChain struct {
|
|||||||
// ResponseWriterChain wraps a dns.ResponseWriter to track if handler wants to continue chain
|
// ResponseWriterChain wraps a dns.ResponseWriter to track if handler wants to continue chain
|
||||||
type ResponseWriterChain struct {
|
type ResponseWriterChain struct {
|
||||||
dns.ResponseWriter
|
dns.ResponseWriter
|
||||||
origPattern string
|
origPattern domain.Domain
|
||||||
shouldContinue bool
|
shouldContinue bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,18 +61,18 @@ func NewHandlerChain() *HandlerChain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetOrigPattern returns the original pattern of the handler that wrote the response
|
// GetOrigPattern returns the original pattern of the handler that wrote the response
|
||||||
func (w *ResponseWriterChain) GetOrigPattern() string {
|
func (w *ResponseWriterChain) GetOrigPattern() domain.Domain {
|
||||||
return w.origPattern
|
return w.origPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddHandler adds a new handler to the chain, replacing any existing handler with the same pattern and priority
|
// AddHandler adds a new handler to the chain, replacing any existing handler with the same pattern and priority
|
||||||
func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int) {
|
func (c *HandlerChain) AddHandler(pattern domain.Domain, handler dns.Handler, priority int) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
pattern = strings.ToLower(dns.Fqdn(pattern))
|
pattern = domain.Domain(strings.ToLower(dns.Fqdn(pattern.PunycodeString())))
|
||||||
origPattern := pattern
|
origPattern := pattern
|
||||||
isWildcard := strings.HasPrefix(pattern, "*.")
|
isWildcard := strings.HasPrefix(pattern.PunycodeString(), "*.")
|
||||||
if isWildcard {
|
if isWildcard {
|
||||||
pattern = pattern[2:]
|
pattern = pattern[2:]
|
||||||
}
|
}
|
||||||
@@ -109,8 +112,8 @@ func (c *HandlerChain) findHandlerPosition(newEntry HandlerEntry) int {
|
|||||||
|
|
||||||
// domain specificity next
|
// domain specificity next
|
||||||
if h.Priority == newEntry.Priority {
|
if h.Priority == newEntry.Priority {
|
||||||
newDots := strings.Count(newEntry.Pattern, ".")
|
newDots := strings.Count(newEntry.Pattern.PunycodeString(), ".")
|
||||||
existingDots := strings.Count(h.Pattern, ".")
|
existingDots := strings.Count(h.Pattern.PunycodeString(), ".")
|
||||||
if newDots > existingDots {
|
if newDots > existingDots {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -122,20 +125,20 @@ func (c *HandlerChain) findHandlerPosition(newEntry HandlerEntry) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveHandler removes a handler for the given pattern and priority
|
// RemoveHandler removes a handler for the given pattern and priority
|
||||||
func (c *HandlerChain) RemoveHandler(pattern string, priority int) {
|
func (c *HandlerChain) RemoveHandler(pattern domain.Domain, priority int) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
pattern = dns.Fqdn(pattern)
|
pattern = domain.Domain(dns.Fqdn(pattern.PunycodeString()))
|
||||||
|
|
||||||
c.removeEntry(pattern, priority)
|
c.removeEntry(pattern, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HandlerChain) removeEntry(pattern string, priority int) {
|
func (c *HandlerChain) removeEntry(pattern domain.Domain, priority int) {
|
||||||
// Find and remove handlers matching both original pattern (case-insensitive) and priority
|
// Find and remove handlers matching both original pattern (case-insensitive) and priority
|
||||||
for i := len(c.handlers) - 1; i >= 0; i-- {
|
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||||
entry := c.handlers[i]
|
entry := c.handlers[i]
|
||||||
if strings.EqualFold(entry.OrigPattern, pattern) && entry.Priority == priority {
|
if strings.EqualFold(entry.OrigPattern.PunycodeString(), pattern.PunycodeString()) && entry.Priority == priority {
|
||||||
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -148,61 +151,42 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
qname := strings.ToLower(r.Question[0].Name)
|
qname := strings.ToLower(r.Question[0].Name)
|
||||||
log.Tracef("handling DNS request for domain=%s", qname)
|
|
||||||
|
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
handlers := slices.Clone(c.handlers)
|
handlers := slices.Clone(c.handlers)
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
|
|
||||||
if log.IsLevelEnabled(log.TraceLevel) {
|
if log.IsLevelEnabled(log.TraceLevel) {
|
||||||
log.Tracef("current handlers (%d):", len(handlers))
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("DNS request domain=%s, handlers (%d):\n", qname, len(handlers)))
|
||||||
for _, h := range handlers {
|
for _, h := range handlers {
|
||||||
log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d",
|
b.WriteString(fmt.Sprintf(" - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d\n",
|
||||||
h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority)
|
h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority))
|
||||||
}
|
}
|
||||||
|
log.Trace(strings.TrimSuffix(b.String(), "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try handlers in priority order
|
// Try handlers in priority order
|
||||||
for _, entry := range handlers {
|
for _, entry := range handlers {
|
||||||
var matched bool
|
matched := c.isHandlerMatch(qname, entry)
|
||||||
switch {
|
|
||||||
case entry.Pattern == ".":
|
if matched {
|
||||||
matched = true
|
log.Tracef("handler matched: domain=%s -> pattern=%s wildcard=%v match_subdomain=%v priority=%d",
|
||||||
case entry.IsWildcard:
|
qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority)
|
||||||
parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern), ".")
|
|
||||||
matched = len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern)
|
chainWriter := &ResponseWriterChain{
|
||||||
default:
|
ResponseWriter: w,
|
||||||
// For non-wildcard patterns:
|
origPattern: entry.OrigPattern,
|
||||||
// If handler wants subdomain matching, allow suffix match
|
|
||||||
// Otherwise require exact match
|
|
||||||
if entry.MatchSubdomains {
|
|
||||||
matched = strings.EqualFold(qname, entry.Pattern) || strings.HasSuffix(qname, "."+entry.Pattern)
|
|
||||||
} else {
|
|
||||||
matched = strings.EqualFold(qname, entry.Pattern)
|
|
||||||
}
|
}
|
||||||
}
|
entry.Handler.ServeDNS(chainWriter, r)
|
||||||
|
|
||||||
if !matched {
|
// If handler wants to continue, try next handler
|
||||||
log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d matched=false",
|
if chainWriter.shouldContinue {
|
||||||
qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard, entry.Priority)
|
log.Tracef("handler requested continue to next handler for domain=%s", qname)
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d",
|
|
||||||
qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority)
|
|
||||||
|
|
||||||
chainWriter := &ResponseWriterChain{
|
|
||||||
ResponseWriter: w,
|
|
||||||
origPattern: entry.OrigPattern,
|
|
||||||
}
|
|
||||||
entry.Handler.ServeDNS(chainWriter, r)
|
|
||||||
|
|
||||||
// If handler wants to continue, try next handler
|
|
||||||
if chainWriter.shouldContinue {
|
|
||||||
log.Tracef("handler requested continue to next handler")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// No handler matched or all handlers passed
|
// No handler matched or all handlers passed
|
||||||
@@ -213,3 +197,22 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
log.Errorf("failed to write DNS response: %v", err)
|
log.Errorf("failed to write DNS response: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool {
|
||||||
|
switch {
|
||||||
|
case entry.Pattern == ".":
|
||||||
|
return true
|
||||||
|
case entry.IsWildcard:
|
||||||
|
parts := strings.Split(strings.TrimSuffix(qname, entry.Pattern.PunycodeString()), ".")
|
||||||
|
return len(parts) >= 2 && strings.HasSuffix(qname, entry.Pattern.PunycodeString())
|
||||||
|
default:
|
||||||
|
// For non-wildcard patterns:
|
||||||
|
// If handler wants subdomain matching, allow suffix match
|
||||||
|
// Otherwise require exact match
|
||||||
|
if entry.MatchSubdomains {
|
||||||
|
return strings.EqualFold(qname, entry.Pattern.PunycodeString()) || strings.HasSuffix(qname, "."+entry.Pattern.PunycodeString())
|
||||||
|
} else {
|
||||||
|
return strings.EqualFold(qname, entry.Pattern.PunycodeString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dns_test
|
package dns_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -9,6 +8,8 @@ import (
|
|||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/client/internal/dns"
|
nbdns "github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestHandlerChain_ServeDNS_Priorities tests that handlers are executed in priority order
|
// TestHandlerChain_ServeDNS_Priorities tests that handlers are executed in priority order
|
||||||
@@ -30,7 +31,7 @@ func TestHandlerChain_ServeDNS_Priorities(t *testing.T) {
|
|||||||
r.SetQuestion("example.com.", dns.TypeA)
|
r.SetQuestion("example.com.", dns.TypeA)
|
||||||
|
|
||||||
// Create test writer
|
// Create test writer
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
|
|
||||||
// Setup expectations - only highest priority handler should be called
|
// Setup expectations - only highest priority handler should be called
|
||||||
dnsRouteHandler.On("ServeDNS", mock.Anything, r).Once()
|
dnsRouteHandler.On("ServeDNS", mock.Anything, r).Once()
|
||||||
@@ -50,8 +51,8 @@ func TestHandlerChain_ServeDNS_Priorities(t *testing.T) {
|
|||||||
func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
handlerDomain string
|
handlerDomain domain.Domain
|
||||||
queryDomain string
|
queryDomain domain.Domain
|
||||||
isWildcard bool
|
isWildcard bool
|
||||||
matchSubdomains bool
|
matchSubdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -141,8 +142,8 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
|||||||
chain.AddHandler(pattern, handler, nbdns.PriorityDefault)
|
chain.AddHandler(pattern, handler, nbdns.PriorityDefault)
|
||||||
|
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tt.queryDomain, dns.TypeA)
|
r.SetQuestion(tt.queryDomain.PunycodeString(), dns.TypeA)
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
|
|
||||||
chain.ServeDNS(w, r)
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
@@ -160,17 +161,17 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
handlers []struct {
|
handlers []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}
|
}
|
||||||
queryDomain string
|
queryDomain domain.Domain
|
||||||
expectedCalls int
|
expectedCalls int
|
||||||
expectedHandler int // index of the handler that should be called
|
expectedHandler int // index of the handler that should be called
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "wildcard and exact same priority - exact should win",
|
name: "wildcard and exact same priority - exact should win",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
||||||
@@ -183,7 +184,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "higher priority wildcard over lower priority exact",
|
name: "higher priority wildcard over lower priority exact",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{pattern: "example.com.", priority: nbdns.PriorityDefault},
|
{pattern: "example.com.", priority: nbdns.PriorityDefault},
|
||||||
@@ -196,7 +197,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "multiple wildcards different priorities",
|
name: "multiple wildcards different priorities",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
||||||
@@ -210,7 +211,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "subdomain with mix of patterns",
|
name: "subdomain with mix of patterns",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
{pattern: "*.example.com.", priority: nbdns.PriorityDefault},
|
||||||
@@ -224,7 +225,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "root zone with specific domain",
|
name: "root zone with specific domain",
|
||||||
handlers: []struct {
|
handlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{pattern: ".", priority: nbdns.PriorityDefault},
|
{pattern: ".", priority: nbdns.PriorityDefault},
|
||||||
@@ -258,8 +259,8 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
|
|
||||||
// Create and execute request
|
// Create and execute request
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tt.queryDomain, dns.TypeA)
|
r.SetQuestion(tt.queryDomain.PunycodeString(), dns.TypeA)
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
chain.ServeDNS(w, r)
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
// Verify expectations
|
// Verify expectations
|
||||||
@@ -316,7 +317,7 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) {
|
|||||||
}).Once()
|
}).Once()
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
chain.ServeDNS(w, r)
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
// Verify all handlers were called in order
|
// Verify all handlers were called in order
|
||||||
@@ -325,26 +326,12 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) {
|
|||||||
handler3.AssertExpectations(t)
|
handler3.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mockResponseWriter implements dns.ResponseWriter for testing
|
|
||||||
type mockResponseWriter struct {
|
|
||||||
mock.Mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *mockResponseWriter) LocalAddr() net.Addr { return nil }
|
|
||||||
func (m *mockResponseWriter) RemoteAddr() net.Addr { return nil }
|
|
||||||
func (m *mockResponseWriter) WriteMsg(*dns.Msg) error { return nil }
|
|
||||||
func (m *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
|
|
||||||
func (m *mockResponseWriter) Close() error { return nil }
|
|
||||||
func (m *mockResponseWriter) TsigStatus() error { return nil }
|
|
||||||
func (m *mockResponseWriter) TsigTimersOnly(bool) {}
|
|
||||||
func (m *mockResponseWriter) Hijack() {}
|
|
||||||
|
|
||||||
func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ops []struct {
|
ops []struct {
|
||||||
action string // "add" or "remove"
|
action string // "add" or "remove"
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}
|
}
|
||||||
query string
|
query string
|
||||||
@@ -354,7 +341,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
|||||||
name: "remove high priority keeps lower priority handler",
|
name: "remove high priority keeps lower priority handler",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
@@ -371,7 +358,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
|||||||
name: "remove lower priority keeps high priority handler",
|
name: "remove lower priority keeps high priority handler",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
@@ -388,7 +375,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
|||||||
name: "remove all handlers in order",
|
name: "remove all handlers in order",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
}{
|
}{
|
||||||
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
{"add", "example.com.", nbdns.PriorityDNSRoute},
|
||||||
@@ -425,7 +412,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
|||||||
// Create test request
|
// Create test request
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tt.query, dns.TypeA)
|
r.SetQuestion(tt.query, dns.TypeA)
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
|
|
||||||
// Setup expectations
|
// Setup expectations
|
||||||
for priority, handler := range handlers {
|
for priority, handler := range handlers {
|
||||||
@@ -450,7 +437,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
|||||||
func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
||||||
chain := nbdns.NewHandlerChain()
|
chain := nbdns.NewHandlerChain()
|
||||||
|
|
||||||
testDomain := "example.com."
|
testDomain := domain.Domain("example.com.")
|
||||||
testQuery := "test.example.com."
|
testQuery := "test.example.com."
|
||||||
|
|
||||||
// Create handlers with MatchSubdomains enabled
|
// Create handlers with MatchSubdomains enabled
|
||||||
@@ -471,7 +458,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
|||||||
chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain)
|
chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain)
|
||||||
|
|
||||||
// Test 1: Initial state
|
// Test 1: Initial state
|
||||||
w1 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w1 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
// Highest priority handler (routeHandler) should be called
|
// Highest priority handler (routeHandler) should be called
|
||||||
routeHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
routeHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
||||||
matchHandler.On("ServeDNS", mock.Anything, r).Maybe() // Ensure others are not expected yet
|
matchHandler.On("ServeDNS", mock.Anything, r).Maybe() // Ensure others are not expected yet
|
||||||
@@ -490,7 +477,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
|||||||
// Test 2: Remove highest priority handler
|
// Test 2: Remove highest priority handler
|
||||||
chain.RemoveHandler(testDomain, nbdns.PriorityDNSRoute)
|
chain.RemoveHandler(testDomain, nbdns.PriorityDNSRoute)
|
||||||
|
|
||||||
w2 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w2 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
// Now middle priority handler (matchHandler) should be called
|
// Now middle priority handler (matchHandler) should be called
|
||||||
matchHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
matchHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
||||||
defaultHandler.On("ServeDNS", mock.Anything, r).Maybe() // Ensure default is not expected yet
|
defaultHandler.On("ServeDNS", mock.Anything, r).Maybe() // Ensure default is not expected yet
|
||||||
@@ -506,7 +493,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
|||||||
// Test 3: Remove middle priority handler
|
// Test 3: Remove middle priority handler
|
||||||
chain.RemoveHandler(testDomain, nbdns.PriorityMatchDomain)
|
chain.RemoveHandler(testDomain, nbdns.PriorityMatchDomain)
|
||||||
|
|
||||||
w3 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w3 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
// Now lowest priority handler (defaultHandler) should be called
|
// Now lowest priority handler (defaultHandler) should be called
|
||||||
defaultHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
defaultHandler.On("ServeDNS", mock.Anything, r).Return().Once()
|
||||||
|
|
||||||
@@ -519,7 +506,7 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
|||||||
// Test 4: Remove last handler
|
// Test 4: Remove last handler
|
||||||
chain.RemoveHandler(testDomain, nbdns.PriorityDefault)
|
chain.RemoveHandler(testDomain, nbdns.PriorityDefault)
|
||||||
|
|
||||||
w4 := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w4 := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
chain.ServeDNS(w4, r) // Call ServeDNS on the now empty chain for this domain
|
chain.ServeDNS(w4, r) // Call ServeDNS on the now empty chain for this domain
|
||||||
|
|
||||||
for _, m := range mocks {
|
for _, m := range mocks {
|
||||||
@@ -532,7 +519,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
scenario string
|
scenario string
|
||||||
addHandlers []struct {
|
addHandlers []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomains bool
|
subdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -544,7 +531,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
name: "case insensitive exact match",
|
name: "case insensitive exact match",
|
||||||
scenario: "handler registered lowercase, query uppercase",
|
scenario: "handler registered lowercase, query uppercase",
|
||||||
addHandlers: []struct {
|
addHandlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomains bool
|
subdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -558,7 +545,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
name: "case insensitive wildcard match",
|
name: "case insensitive wildcard match",
|
||||||
scenario: "handler registered mixed case wildcard, query different case",
|
scenario: "handler registered mixed case wildcard, query different case",
|
||||||
addHandlers: []struct {
|
addHandlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomains bool
|
subdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -572,7 +559,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
name: "multiple handlers different case same domain",
|
name: "multiple handlers different case same domain",
|
||||||
scenario: "second handler should replace first despite case difference",
|
scenario: "second handler should replace first despite case difference",
|
||||||
addHandlers: []struct {
|
addHandlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomains bool
|
subdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -587,7 +574,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
name: "subdomain matching case insensitive",
|
name: "subdomain matching case insensitive",
|
||||||
scenario: "handler with MatchSubdomains true should match regardless of case",
|
scenario: "handler with MatchSubdomains true should match regardless of case",
|
||||||
addHandlers: []struct {
|
addHandlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomains bool
|
subdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -601,7 +588,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
name: "root zone case insensitive",
|
name: "root zone case insensitive",
|
||||||
scenario: "root zone handler should match regardless of case",
|
scenario: "root zone handler should match regardless of case",
|
||||||
addHandlers: []struct {
|
addHandlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomains bool
|
subdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -615,7 +602,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
name: "multiple handlers different priority",
|
name: "multiple handlers different priority",
|
||||||
scenario: "should call higher priority handler despite case differences",
|
scenario: "should call higher priority handler despite case differences",
|
||||||
addHandlers: []struct {
|
addHandlers: []struct {
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomains bool
|
subdomains bool
|
||||||
shouldMatch bool
|
shouldMatch bool
|
||||||
@@ -632,7 +619,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
chain := nbdns.NewHandlerChain()
|
chain := nbdns.NewHandlerChain()
|
||||||
handlerCalls := make(map[string]bool) // track which patterns were called
|
handlerCalls := make(map[domain.Domain]bool) // track which patterns were called
|
||||||
|
|
||||||
// Add handlers according to test case
|
// Add handlers according to test case
|
||||||
for _, h := range tt.addHandlers {
|
for _, h := range tt.addHandlers {
|
||||||
@@ -675,7 +662,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
// Execute request
|
// Execute request
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tt.query, dns.TypeA)
|
r.SetQuestion(tt.query, dns.TypeA)
|
||||||
chain.ServeDNS(&mockResponseWriter{}, r)
|
chain.ServeDNS(&test.MockResponseWriter{}, r)
|
||||||
|
|
||||||
// Verify each handler was called exactly as expected
|
// Verify each handler was called exactly as expected
|
||||||
for _, h := range tt.addHandlers {
|
for _, h := range tt.addHandlers {
|
||||||
@@ -700,19 +687,19 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
scenario string
|
scenario string
|
||||||
ops []struct {
|
ops []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomain bool
|
subdomain bool
|
||||||
}
|
}
|
||||||
query string
|
query domain.Domain
|
||||||
expectedMatch string
|
expectedMatch domain.Domain
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "more specific domain matches first",
|
name: "more specific domain matches first",
|
||||||
scenario: "sub.example.com should match before example.com",
|
scenario: "sub.example.com should match before example.com",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomain bool
|
subdomain bool
|
||||||
}{
|
}{
|
||||||
@@ -727,7 +714,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
scenario: "sub.example.com should match before example.com",
|
scenario: "sub.example.com should match before example.com",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomain bool
|
subdomain bool
|
||||||
}{
|
}{
|
||||||
@@ -742,7 +729,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
scenario: "after removing most specific, should fall back to less specific",
|
scenario: "after removing most specific, should fall back to less specific",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomain bool
|
subdomain bool
|
||||||
}{
|
}{
|
||||||
@@ -759,7 +746,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
scenario: "less specific domain with higher priority should match first",
|
scenario: "less specific domain with higher priority should match first",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomain bool
|
subdomain bool
|
||||||
}{
|
}{
|
||||||
@@ -774,7 +761,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
scenario: "with equal priority, more specific domain should match",
|
scenario: "with equal priority, more specific domain should match",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomain bool
|
subdomain bool
|
||||||
}{
|
}{
|
||||||
@@ -790,7 +777,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
scenario: "specific domain should match before wildcard at same priority",
|
scenario: "specific domain should match before wildcard at same priority",
|
||||||
ops: []struct {
|
ops: []struct {
|
||||||
action string
|
action string
|
||||||
pattern string
|
pattern domain.Domain
|
||||||
priority int
|
priority int
|
||||||
subdomain bool
|
subdomain bool
|
||||||
}{
|
}{
|
||||||
@@ -805,7 +792,7 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
chain := nbdns.NewHandlerChain()
|
chain := nbdns.NewHandlerChain()
|
||||||
handlers := make(map[string]*nbdns.MockSubdomainHandler)
|
handlers := make(map[domain.Domain]*nbdns.MockSubdomainHandler)
|
||||||
|
|
||||||
for _, op := range tt.ops {
|
for _, op := range tt.ops {
|
||||||
if op.action == "add" {
|
if op.action == "add" {
|
||||||
@@ -818,8 +805,8 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tt.query, dns.TypeA)
|
r.SetQuestion(tt.query.PunycodeString(), dns.TypeA)
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
|
|
||||||
// Setup handler expectations
|
// Setup handler expectations
|
||||||
for pattern, handler := range handlers {
|
for pattern, handler := range handlers {
|
||||||
@@ -850,9 +837,9 @@ func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
|||||||
func TestHandlerChain_AddRemoveRoundtrip(t *testing.T) {
|
func TestHandlerChain_AddRemoveRoundtrip(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
addPattern string
|
addPattern domain.Domain
|
||||||
removePattern string
|
removePattern domain.Domain
|
||||||
queryPattern string
|
queryPattern domain.Domain
|
||||||
shouldBeRemoved bool
|
shouldBeRemoved bool
|
||||||
description string
|
description string
|
||||||
}{
|
}{
|
||||||
@@ -968,8 +955,8 @@ func TestHandlerChain_AddRemoveRoundtrip(t *testing.T) {
|
|||||||
|
|
||||||
handler := &nbdns.MockHandler{}
|
handler := &nbdns.MockHandler{}
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tt.queryPattern, dns.TypeA)
|
r.SetQuestion(tt.queryPattern.PunycodeString(), dns.TypeA)
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
|
|
||||||
// First verify no handler is called before adding any
|
// First verify no handler is called before adding any
|
||||||
chain.ServeDNS(w, r)
|
chain.ServeDNS(w, r)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrRouteAllWithoutNameserverGroup = fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
|
var ErrRouteAllWithoutNameserverGroup = fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
|
||||||
@@ -39,9 +40,9 @@ type HostDNSConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DomainConfig struct {
|
type DomainConfig struct {
|
||||||
Disabled bool `json:"disabled"`
|
Disabled bool `json:"disabled"`
|
||||||
Domain string `json:"domain"`
|
Domain domain.Domain `json:"domain"`
|
||||||
MatchOnly bool `json:"matchOnly"`
|
MatchOnly bool `json:"matchOnly"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockHostConfigurator struct {
|
type mockHostConfigurator struct {
|
||||||
@@ -103,18 +104,20 @@ func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostD
|
|||||||
config.RouteAll = true
|
config.RouteAll = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range nsConfig.Domains {
|
for _, d := range nsConfig.Domains {
|
||||||
|
d := strings.ToLower(dns.Fqdn(d.PunycodeString()))
|
||||||
config.Domains = append(config.Domains, DomainConfig{
|
config.Domains = append(config.Domains, DomainConfig{
|
||||||
Domain: strings.ToLower(dns.Fqdn(domain)),
|
Domain: domain.Domain(d),
|
||||||
MatchOnly: !nsConfig.SearchDomainsEnabled,
|
MatchOnly: !nsConfig.SearchDomainsEnabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, customZone := range dnsConfig.CustomZones {
|
for _, customZone := range dnsConfig.CustomZones {
|
||||||
matchOnly := strings.HasSuffix(customZone.Domain, ipv4ReverseZone) || strings.HasSuffix(customZone.Domain, ipv6ReverseZone)
|
d := strings.ToLower(dns.Fqdn(customZone.Domain))
|
||||||
|
matchOnly := strings.HasSuffix(d, ipv4ReverseZone) || strings.HasSuffix(d, ipv6ReverseZone)
|
||||||
config.Domains = append(config.Domains, DomainConfig{
|
config.Domains = append(config.Domains, DomainConfig{
|
||||||
Domain: strings.ToLower(dns.Fqdn(customZone.Domain)),
|
Domain: domain.Domain(d),
|
||||||
MatchOnly: matchOnly,
|
MatchOnly: matchOnly,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ func (s *systemConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if dConf.MatchOnly {
|
if dConf.MatchOnly {
|
||||||
matchDomains = append(matchDomains, strings.TrimSuffix(dConf.Domain, "."))
|
matchDomains = append(matchDomains, strings.TrimSuffix(dConf.Domain.PunycodeString(), "."))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
searchDomains = append(searchDomains, strings.TrimSuffix(""+dConf.Domain, "."))
|
searchDomains = append(searchDomains, strings.TrimSuffix(""+dConf.Domain.PunycodeString(), "."))
|
||||||
}
|
}
|
||||||
|
|
||||||
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
matchKey := getKeyWithInput(netbirdDNSStateKeyFormat, matchSuffix)
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -41,6 +44,20 @@ const (
|
|||||||
interfaceConfigNameServerKey = "NameServer"
|
interfaceConfigNameServerKey = "NameServer"
|
||||||
interfaceConfigSearchListKey = "SearchList"
|
interfaceConfigSearchListKey = "SearchList"
|
||||||
|
|
||||||
|
// Network interface DNS registration settings
|
||||||
|
disableDynamicUpdateKey = "DisableDynamicUpdate"
|
||||||
|
registrationEnabledKey = "RegistrationEnabled"
|
||||||
|
maxNumberOfAddressesToRegisterKey = "MaxNumberOfAddressesToRegister"
|
||||||
|
|
||||||
|
// NetBIOS/WINS settings
|
||||||
|
netbtInterfacePath = `SYSTEM\CurrentControlSet\Services\NetBT\Parameters\Interfaces`
|
||||||
|
netbiosOptionsKey = "NetbiosOptions"
|
||||||
|
|
||||||
|
// NetBIOS option values: 0 = from DHCP, 1 = enabled, 2 = disabled
|
||||||
|
netbiosFromDHCP = 0
|
||||||
|
netbiosEnabled = 1
|
||||||
|
netbiosDisabled = 2
|
||||||
|
|
||||||
// RP_FORCE: Reapply all policies even if no policy change was detected
|
// RP_FORCE: Reapply all policies even if no policy change was detected
|
||||||
rpForce = 0x1
|
rpForce = 0x1
|
||||||
)
|
)
|
||||||
@@ -67,16 +84,85 @@ func newHostManager(wgInterface WGIface) (*registryConfigurator, error) {
|
|||||||
log.Infof("detected GPO DNS policy configuration, using policy store")
|
log.Infof("detected GPO DNS policy configuration, using policy store")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ®istryConfigurator{
|
configurator := ®istryConfigurator{
|
||||||
guid: guid,
|
guid: guid,
|
||||||
gpo: useGPO,
|
gpo: useGPO,
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
if err := configurator.configureInterface(); err != nil {
|
||||||
|
log.Errorf("failed to configure interface settings: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return configurator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) supportCustomPort() bool {
|
func (r *registryConfigurator) supportCustomPort() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) configureInterface() error {
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
if err := r.disableDNSRegistrationForInterface(); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("disable DNS registration: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.disableWINSForInterface(); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("disable WINS: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) disableDNSRegistrationForInterface() error {
|
||||||
|
regKey, err := r.getInterfaceRegistryKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get interface registry key: %w", err)
|
||||||
|
}
|
||||||
|
defer closer(regKey)
|
||||||
|
|
||||||
|
var merr *multierror.Error
|
||||||
|
|
||||||
|
if err := regKey.SetDWordValue(disableDynamicUpdateKey, 1); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("set %s: %w", disableDynamicUpdateKey, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := regKey.SetDWordValue(registrationEnabledKey, 0); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("set %s: %w", registrationEnabledKey, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := regKey.SetDWordValue(maxNumberOfAddressesToRegisterKey, 0); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("set %s: %w", maxNumberOfAddressesToRegisterKey, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if merr == nil || len(merr.Errors) == 0 {
|
||||||
|
log.Infof("disabled DNS registration for interface %s", r.guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) disableWINSForInterface() error {
|
||||||
|
netbtKeyPath := fmt.Sprintf(`%s\Tcpip_%s`, netbtInterfacePath, r.guid)
|
||||||
|
|
||||||
|
regKey, err := registry.OpenKey(registry.LOCAL_MACHINE, netbtKeyPath, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
regKey, _, err = registry.CreateKey(registry.LOCAL_MACHINE, netbtKeyPath, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create NetBT interface key %s: %w", netbtKeyPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer closer(regKey)
|
||||||
|
|
||||||
|
// NetbiosOptions: 2 = disabled
|
||||||
|
if err := regKey.SetDWordValue(netbiosOptionsKey, netbiosDisabled); err != nil {
|
||||||
|
return fmt.Errorf("set %s: %w", netbiosOptionsKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("disabled WINS/NetBIOS for interface %s", r.guid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
|
func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager *statemanager.Manager) error {
|
||||||
if config.RouteAll {
|
if config.RouteAll {
|
||||||
if err := r.addDNSSetupForAll(config.ServerIP); err != nil {
|
if err := r.addDNSSetupForAll(config.ServerIP); err != nil {
|
||||||
@@ -100,9 +186,9 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !dConf.MatchOnly {
|
if !dConf.MatchOnly {
|
||||||
searchDomains = append(searchDomains, strings.TrimSuffix(dConf.Domain, "."))
|
searchDomains = append(searchDomains, strings.TrimSuffix(dConf.Domain.PunycodeString(), "."))
|
||||||
}
|
}
|
||||||
matchDomains = append(matchDomains, "."+strings.TrimSuffix(dConf.Domain, "."))
|
matchDomains = append(matchDomains, "."+strings.TrimSuffix(dConf.Domain.PunycodeString(), "."))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(matchDomains) != 0 {
|
if len(matchDomains) != 0 {
|
||||||
@@ -119,9 +205,7 @@ func (r *registryConfigurator) applyDNSConfig(config HostDNSConfig, stateManager
|
|||||||
return fmt.Errorf("update search domains: %w", err)
|
return fmt.Errorf("update search domains: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.flushDNSCache(); err != nil {
|
go r.flushDNSCache()
|
||||||
log.Errorf("failed to flush DNS cache: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -191,7 +275,25 @@ func (r *registryConfigurator) string() string {
|
|||||||
return "registry"
|
return "registry"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) flushDNSCache() error {
|
func (r *registryConfigurator) registerDNS() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// nolint:misspell
|
||||||
|
cmd := exec.CommandContext(ctx, "ipconfig", "/registerdns")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to register DNS: %v, output: %s", err, out)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("registered DNS names")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *registryConfigurator) flushDNSCache() {
|
||||||
|
r.registerDNS()
|
||||||
|
|
||||||
// dnsFlushResolverCacheFn.Call() may panic if the func is not found
|
// dnsFlushResolverCacheFn.Call() may panic if the func is not found
|
||||||
defer func() {
|
defer func() {
|
||||||
if rec := recover(); rec != nil {
|
if rec := recover(); rec != nil {
|
||||||
@@ -202,13 +304,14 @@ func (r *registryConfigurator) flushDNSCache() error {
|
|||||||
ret, _, err := dnsFlushResolverCacheFn.Call()
|
ret, _, err := dnsFlushResolverCacheFn.Call()
|
||||||
if ret == 0 {
|
if ret == 0 {
|
||||||
if err != nil && !errors.Is(err, syscall.Errno(0)) {
|
if err != nil && !errors.Is(err, syscall.Errno(0)) {
|
||||||
return fmt.Errorf("DnsFlushResolverCache failed: %w", err)
|
log.Errorf("DnsFlushResolverCache failed: %v", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return fmt.Errorf("DnsFlushResolverCache failed")
|
log.Errorf("DnsFlushResolverCache failed")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("flushed DNS cache")
|
log.Info("flushed DNS cache")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *registryConfigurator) updateSearchDomains(domains []string) error {
|
func (r *registryConfigurator) updateSearchDomains(domains []string) error {
|
||||||
@@ -263,9 +366,7 @@ func (r *registryConfigurator) restoreHostDNS() error {
|
|||||||
return fmt.Errorf("remove interface registry key: %w", err)
|
return fmt.Errorf("remove interface registry key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.flushDNSCache(); err != nil {
|
go r.flushDNSCache()
|
||||||
log.Errorf("failed to flush DNS cache: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
type registrationMap map[string]struct{}
|
|
||||||
|
|
||||||
type localResolver struct {
|
|
||||||
registeredMap registrationMap
|
|
||||||
records sync.Map // key: string (domain_class_type), value: []dns.RR
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *localResolver) MatchSubdomains() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *localResolver) stop() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns a string representation of the local resolver
|
|
||||||
func (d *localResolver) String() string {
|
|
||||||
return fmt.Sprintf("local resolver [%d records]", len(d.registeredMap))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns the unique handler ID
|
|
||||||
func (d *localResolver) id() handlerID {
|
|
||||||
return "local-resolver"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeDNS handles a DNS request
|
|
||||||
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|
||||||
if len(r.Question) > 0 {
|
|
||||||
log.Tracef("received local question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
|
||||||
}
|
|
||||||
|
|
||||||
replyMessage := &dns.Msg{}
|
|
||||||
replyMessage.SetReply(r)
|
|
||||||
replyMessage.RecursionAvailable = true
|
|
||||||
|
|
||||||
// lookup all records matching the question
|
|
||||||
records := d.lookupRecords(r)
|
|
||||||
if len(records) > 0 {
|
|
||||||
replyMessage.Rcode = dns.RcodeSuccess
|
|
||||||
replyMessage.Answer = append(replyMessage.Answer, records...)
|
|
||||||
} else {
|
|
||||||
replyMessage.Rcode = dns.RcodeNameError
|
|
||||||
}
|
|
||||||
|
|
||||||
err := w.WriteMsg(replyMessage)
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("got an error while writing the local resolver response, error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// lookupRecords fetches *all* DNS records matching the first question in r.
|
|
||||||
func (d *localResolver) lookupRecords(r *dns.Msg) []dns.RR {
|
|
||||||
if len(r.Question) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
question := r.Question[0]
|
|
||||||
question.Name = strings.ToLower(question.Name)
|
|
||||||
key := buildRecordKey(question.Name, question.Qclass, question.Qtype)
|
|
||||||
|
|
||||||
value, found := d.records.Load(key)
|
|
||||||
if !found {
|
|
||||||
// alternatively check if we have a cname
|
|
||||||
if question.Qtype != dns.TypeCNAME {
|
|
||||||
r.Question[0].Qtype = dns.TypeCNAME
|
|
||||||
return d.lookupRecords(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
records, ok := value.([]dns.RR)
|
|
||||||
if !ok {
|
|
||||||
log.Errorf("failed to cast records to []dns.RR, records: %v", value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// if there's more than one record, rotate them (round-robin)
|
|
||||||
if len(records) > 1 {
|
|
||||||
first := records[0]
|
|
||||||
records = append(records[1:], first)
|
|
||||||
d.records.Store(key, records)
|
|
||||||
}
|
|
||||||
|
|
||||||
return records
|
|
||||||
}
|
|
||||||
|
|
||||||
// registerRecord stores a new record by appending it to any existing list
|
|
||||||
func (d *localResolver) registerRecord(record nbdns.SimpleRecord) (string, error) {
|
|
||||||
rr, err := dns.NewRR(record.String())
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("register record: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rr.Header().Rdlength = record.Len()
|
|
||||||
header := rr.Header()
|
|
||||||
key := buildRecordKey(header.Name, header.Class, header.Rrtype)
|
|
||||||
|
|
||||||
// load any existing slice of records, then append
|
|
||||||
existing, _ := d.records.LoadOrStore(key, []dns.RR{})
|
|
||||||
records := existing.([]dns.RR)
|
|
||||||
records = append(records, rr)
|
|
||||||
|
|
||||||
// store updated slice
|
|
||||||
d.records.Store(key, records)
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteRecord removes *all* records under the recordKey.
|
|
||||||
func (d *localResolver) deleteRecord(recordKey string) {
|
|
||||||
d.records.Delete(dns.Fqdn(recordKey))
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildRecordKey consistently generates a key: name_class_type
|
|
||||||
func buildRecordKey(name string, class, qType uint16) string {
|
|
||||||
return fmt.Sprintf("%s_%d_%d", dns.Fqdn(name), class, qType)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *localResolver) probeAvailability() {}
|
|
||||||
149
client/internal/dns/local/local.go
Normal file
149
client/internal/dns/local/local.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/types"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resolver struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
records map[dns.Question][]dns.RR
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResolver() *Resolver {
|
||||||
|
return &Resolver{
|
||||||
|
records: make(map[dns.Question][]dns.RR),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Resolver) MatchSubdomains() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the local resolver
|
||||||
|
func (d *Resolver) String() string {
|
||||||
|
return fmt.Sprintf("local resolver [%d records]", len(d.records))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Resolver) Stop() {}
|
||||||
|
|
||||||
|
// ID returns the unique handler ID
|
||||||
|
func (d *Resolver) ID() types.HandlerID {
|
||||||
|
return "local-resolver"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Resolver) ProbeAvailability() {}
|
||||||
|
|
||||||
|
// ServeDNS handles a DNS request
|
||||||
|
func (d *Resolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
if len(r.Question) == 0 {
|
||||||
|
log.Debugf("received local resolver request with no question")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
question := r.Question[0]
|
||||||
|
question.Name = strings.ToLower(dns.Fqdn(question.Name))
|
||||||
|
|
||||||
|
log.Tracef("received local question: domain=%s type=%v class=%v", r.Question[0].Name, question.Qtype, question.Qclass)
|
||||||
|
|
||||||
|
replyMessage := &dns.Msg{}
|
||||||
|
replyMessage.SetReply(r)
|
||||||
|
replyMessage.RecursionAvailable = true
|
||||||
|
|
||||||
|
// lookup all records matching the question
|
||||||
|
records := d.lookupRecords(question)
|
||||||
|
if len(records) > 0 {
|
||||||
|
replyMessage.Rcode = dns.RcodeSuccess
|
||||||
|
replyMessage.Answer = append(replyMessage.Answer, records...)
|
||||||
|
} else {
|
||||||
|
// TODO: return success if we have a different record type for the same name, relevant for search domains
|
||||||
|
replyMessage.Rcode = dns.RcodeNameError
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.WriteMsg(replyMessage); err != nil {
|
||||||
|
log.Warnf("failed to write the local resolver response: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lookupRecords fetches *all* DNS records matching the first question in r.
|
||||||
|
func (d *Resolver) lookupRecords(question dns.Question) []dns.RR {
|
||||||
|
d.mu.RLock()
|
||||||
|
records, found := d.records[question]
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
d.mu.RUnlock()
|
||||||
|
// alternatively check if we have a cname
|
||||||
|
if question.Qtype != dns.TypeCNAME {
|
||||||
|
question.Qtype = dns.TypeCNAME
|
||||||
|
return d.lookupRecords(question)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
recordsCopy := slices.Clone(records)
|
||||||
|
d.mu.RUnlock()
|
||||||
|
|
||||||
|
// if there's more than one record, rotate them (round-robin)
|
||||||
|
if len(recordsCopy) > 1 {
|
||||||
|
d.mu.Lock()
|
||||||
|
records = d.records[question]
|
||||||
|
if len(records) > 1 {
|
||||||
|
first := records[0]
|
||||||
|
records = append(records[1:], first)
|
||||||
|
d.records[question] = records
|
||||||
|
}
|
||||||
|
d.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordsCopy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Resolver) Update(update []nbdns.SimpleRecord) {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
maps.Clear(d.records)
|
||||||
|
|
||||||
|
for _, rec := range update {
|
||||||
|
if err := d.registerRecord(rec); err != nil {
|
||||||
|
log.Warnf("failed to register the record (%s): %v", rec, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRecord stores a new record by appending it to any existing list
|
||||||
|
func (d *Resolver) RegisterRecord(record nbdns.SimpleRecord) error {
|
||||||
|
d.mu.Lock()
|
||||||
|
defer d.mu.Unlock()
|
||||||
|
|
||||||
|
return d.registerRecord(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerRecord performs the registration with the lock already held
|
||||||
|
func (d *Resolver) registerRecord(record nbdns.SimpleRecord) error {
|
||||||
|
rr, err := dns.NewRR(record.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("register record: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr.Header().Rdlength = record.Len()
|
||||||
|
header := rr.Header()
|
||||||
|
q := dns.Question{
|
||||||
|
Name: strings.ToLower(dns.Fqdn(header.Name)),
|
||||||
|
Qtype: header.Rrtype,
|
||||||
|
Qclass: header.Class,
|
||||||
|
}
|
||||||
|
|
||||||
|
d.records[q] = append(d.records[q], rr)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
472
client/internal/dns/local/local_test.go
Normal file
472
client/internal/dns/local/local_test.go
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||||
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLocalResolver_ServeDNS(t *testing.T) {
|
||||||
|
recordA := nbdns.SimpleRecord{
|
||||||
|
Name: "peera.netbird.cloud.",
|
||||||
|
Type: 1,
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "1.2.3.4",
|
||||||
|
}
|
||||||
|
|
||||||
|
recordCNAME := nbdns.SimpleRecord{
|
||||||
|
Name: "peerb.netbird.cloud.",
|
||||||
|
Type: 5,
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "www.netbird.io",
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputRecord nbdns.SimpleRecord
|
||||||
|
inputMSG *dns.Msg
|
||||||
|
responseShouldBeNil bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Resolve A Record",
|
||||||
|
inputRecord: recordA,
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Resolve CNAME Record",
|
||||||
|
inputRecord: recordCNAME,
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Write When Not Found A Record",
|
||||||
|
inputRecord: recordA,
|
||||||
|
inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
|
||||||
|
responseShouldBeNil: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
_ = resolver.RegisterRecord(testCase.inputRecord)
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.ServeDNS(responseWriter, testCase.inputMSG)
|
||||||
|
|
||||||
|
if responseMSG == nil || len(responseMSG.Answer) == 0 {
|
||||||
|
if testCase.responseShouldBeNil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("should write a response message")
|
||||||
|
}
|
||||||
|
|
||||||
|
answerString := responseMSG.Answer[0].String()
|
||||||
|
if !strings.Contains(answerString, testCase.inputRecord.Name) {
|
||||||
|
t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
|
||||||
|
}
|
||||||
|
if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
|
||||||
|
t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
|
||||||
|
}
|
||||||
|
if !strings.Contains(answerString, testCase.inputRecord.RData) {
|
||||||
|
t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalResolver_Update_StaleRecord verifies that updating
|
||||||
|
// a record correctly replaces the old one, preventing stale entries.
|
||||||
|
func TestLocalResolver_Update_StaleRecord(t *testing.T) {
|
||||||
|
recordName := "host.example.com."
|
||||||
|
recordType := dns.TypeA
|
||||||
|
recordClass := dns.ClassINET
|
||||||
|
|
||||||
|
record1 := nbdns.SimpleRecord{
|
||||||
|
Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "1.1.1.1",
|
||||||
|
}
|
||||||
|
record2 := nbdns.SimpleRecord{
|
||||||
|
Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "2.2.2.2",
|
||||||
|
}
|
||||||
|
|
||||||
|
recordKey := dns.Question{Name: recordName, Qtype: uint16(recordClass), Qclass: recordType}
|
||||||
|
|
||||||
|
resolver := NewResolver()
|
||||||
|
|
||||||
|
update1 := []nbdns.SimpleRecord{record1}
|
||||||
|
update2 := []nbdns.SimpleRecord{record2}
|
||||||
|
|
||||||
|
// Apply first update
|
||||||
|
resolver.Update(update1)
|
||||||
|
|
||||||
|
// Verify first update
|
||||||
|
resolver.mu.RLock()
|
||||||
|
rrSlice1, found1 := resolver.records[recordKey]
|
||||||
|
resolver.mu.RUnlock()
|
||||||
|
|
||||||
|
require.True(t, found1, "Record key %s not found after first update", recordKey)
|
||||||
|
require.Len(t, rrSlice1, 1, "Should have exactly 1 record after first update")
|
||||||
|
assert.Contains(t, rrSlice1[0].String(), record1.RData, "Record after first update should be %s", record1.RData)
|
||||||
|
|
||||||
|
// Apply second update
|
||||||
|
resolver.Update(update2)
|
||||||
|
|
||||||
|
// Verify second update
|
||||||
|
resolver.mu.RLock()
|
||||||
|
rrSlice2, found2 := resolver.records[recordKey]
|
||||||
|
resolver.mu.RUnlock()
|
||||||
|
|
||||||
|
require.True(t, found2, "Record key %s not found after second update", recordKey)
|
||||||
|
require.Len(t, rrSlice2, 1, "Should have exactly 1 record after update overwriting the key")
|
||||||
|
assert.Contains(t, rrSlice2[0].String(), record2.RData, "The single record should be the updated one (%s)", record2.RData)
|
||||||
|
assert.NotContains(t, rrSlice2[0].String(), record1.RData, "The stale record (%s) should not be present", record1.RData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalResolver_MultipleRecords_SameQuestion verifies that multiple records
|
||||||
|
// with the same question are stored properly
|
||||||
|
func TestLocalResolver_MultipleRecords_SameQuestion(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
|
||||||
|
recordName := "multi.example.com."
|
||||||
|
recordType := dns.TypeA
|
||||||
|
|
||||||
|
// Create two records with the same name and type but different IPs
|
||||||
|
record1 := nbdns.SimpleRecord{
|
||||||
|
Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1",
|
||||||
|
}
|
||||||
|
record2 := nbdns.SimpleRecord{
|
||||||
|
Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.2",
|
||||||
|
}
|
||||||
|
|
||||||
|
update := []nbdns.SimpleRecord{record1, record2}
|
||||||
|
|
||||||
|
// Apply update with both records
|
||||||
|
resolver.Update(update)
|
||||||
|
|
||||||
|
// Create question that matches both records
|
||||||
|
question := dns.Question{
|
||||||
|
Name: recordName,
|
||||||
|
Qtype: recordType,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify both records are stored
|
||||||
|
resolver.mu.RLock()
|
||||||
|
records, found := resolver.records[question]
|
||||||
|
resolver.mu.RUnlock()
|
||||||
|
|
||||||
|
require.True(t, found, "Records for question %v not found", question)
|
||||||
|
require.Len(t, records, 2, "Should have exactly 2 records for the same question")
|
||||||
|
|
||||||
|
// Verify both record data values are present
|
||||||
|
recordStrings := []string{records[0].String(), records[1].String()}
|
||||||
|
assert.Contains(t, recordStrings[0]+recordStrings[1], record1.RData, "First record data should be present")
|
||||||
|
assert.Contains(t, recordStrings[0]+recordStrings[1], record2.RData, "Second record data should be present")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalResolver_RecordRotation verifies that records are rotated in a round-robin fashion
|
||||||
|
func TestLocalResolver_RecordRotation(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
|
||||||
|
recordName := "rotation.example.com."
|
||||||
|
recordType := dns.TypeA
|
||||||
|
|
||||||
|
// Create three records with the same name and type but different IPs
|
||||||
|
record1 := nbdns.SimpleRecord{
|
||||||
|
Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "192.168.1.1",
|
||||||
|
}
|
||||||
|
record2 := nbdns.SimpleRecord{
|
||||||
|
Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "192.168.1.2",
|
||||||
|
}
|
||||||
|
record3 := nbdns.SimpleRecord{
|
||||||
|
Name: recordName, Type: int(recordType), Class: nbdns.DefaultClass, TTL: 300, RData: "192.168.1.3",
|
||||||
|
}
|
||||||
|
|
||||||
|
update := []nbdns.SimpleRecord{record1, record2, record3}
|
||||||
|
|
||||||
|
// Apply update with all three records
|
||||||
|
resolver.Update(update)
|
||||||
|
|
||||||
|
msg := new(dns.Msg).SetQuestion(recordName, recordType)
|
||||||
|
|
||||||
|
// First lookup - should return the records in original order
|
||||||
|
var responses [3]*dns.Msg
|
||||||
|
|
||||||
|
// Perform three lookups to verify rotation
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responses[i] = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.ServeDNS(responseWriter, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all three responses contain answers
|
||||||
|
for i, resp := range responses {
|
||||||
|
require.NotNil(t, resp, "Response %d should not be nil", i)
|
||||||
|
require.Len(t, resp.Answer, 3, "Response %d should have 3 answers", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the first record in each response is different due to rotation
|
||||||
|
firstRecordIPs := []string{
|
||||||
|
responses[0].Answer[0].String(),
|
||||||
|
responses[1].Answer[0].String(),
|
||||||
|
responses[2].Answer[0].String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each record should be different (rotated)
|
||||||
|
assert.NotEqual(t, firstRecordIPs[0], firstRecordIPs[1], "First lookup should differ from second lookup due to rotation")
|
||||||
|
assert.NotEqual(t, firstRecordIPs[1], firstRecordIPs[2], "Second lookup should differ from third lookup due to rotation")
|
||||||
|
assert.NotEqual(t, firstRecordIPs[0], firstRecordIPs[2], "First lookup should differ from third lookup due to rotation")
|
||||||
|
|
||||||
|
// After three rotations, we should have cycled through all records
|
||||||
|
assert.Contains(t, firstRecordIPs[0]+firstRecordIPs[1]+firstRecordIPs[2], record1.RData)
|
||||||
|
assert.Contains(t, firstRecordIPs[0]+firstRecordIPs[1]+firstRecordIPs[2], record2.RData)
|
||||||
|
assert.Contains(t, firstRecordIPs[0]+firstRecordIPs[1]+firstRecordIPs[2], record3.RData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalResolver_CaseInsensitiveMatching verifies that DNS record lookups are case-insensitive
|
||||||
|
func TestLocalResolver_CaseInsensitiveMatching(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
|
||||||
|
// Create record with lowercase name
|
||||||
|
lowerCaseRecord := nbdns.SimpleRecord{
|
||||||
|
Name: "lower.example.com.",
|
||||||
|
Type: int(dns.TypeA),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "10.10.10.10",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create record with mixed case name
|
||||||
|
mixedCaseRecord := nbdns.SimpleRecord{
|
||||||
|
Name: "MiXeD.ExAmPlE.CoM.",
|
||||||
|
Type: int(dns.TypeA),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "20.20.20.20",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update resolver with the records
|
||||||
|
resolver.Update([]nbdns.SimpleRecord{lowerCaseRecord, mixedCaseRecord})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
queryName string
|
||||||
|
expectedRData string
|
||||||
|
shouldResolve bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Query lowercase with lowercase record",
|
||||||
|
queryName: "lower.example.com.",
|
||||||
|
expectedRData: "10.10.10.10",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query uppercase with lowercase record",
|
||||||
|
queryName: "LOWER.EXAMPLE.COM.",
|
||||||
|
expectedRData: "10.10.10.10",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query mixed case with lowercase record",
|
||||||
|
queryName: "LoWeR.eXaMpLe.CoM.",
|
||||||
|
expectedRData: "10.10.10.10",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query lowercase with mixed case record",
|
||||||
|
queryName: "mixed.example.com.",
|
||||||
|
expectedRData: "20.20.20.20",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query uppercase with mixed case record",
|
||||||
|
queryName: "MIXED.EXAMPLE.COM.",
|
||||||
|
expectedRData: "20.20.20.20",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query with different casing pattern",
|
||||||
|
queryName: "mIxEd.ExaMpLe.cOm.",
|
||||||
|
expectedRData: "20.20.20.20",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query non-existent domain",
|
||||||
|
queryName: "nonexistent.example.com.",
|
||||||
|
shouldResolve: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
|
||||||
|
// Create DNS query with the test case name
|
||||||
|
msg := new(dns.Msg).SetQuestion(tc.queryName, dns.TypeA)
|
||||||
|
|
||||||
|
// Create mock response writer to capture the response
|
||||||
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform DNS query
|
||||||
|
resolver.ServeDNS(responseWriter, msg)
|
||||||
|
|
||||||
|
// Check if we expect a successful resolution
|
||||||
|
if !tc.shouldResolve {
|
||||||
|
if responseMSG == nil || len(responseMSG.Answer) == 0 {
|
||||||
|
// Expected no answer, test passes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected no resolution for %s, but got answer: %v", tc.queryName, responseMSG.Answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we got a response
|
||||||
|
require.NotNil(t, responseMSG, "Should have received a response message")
|
||||||
|
require.Greater(t, len(responseMSG.Answer), 0, "Response should contain at least one answer")
|
||||||
|
|
||||||
|
// Verify the response contains the expected data
|
||||||
|
answerString := responseMSG.Answer[0].String()
|
||||||
|
assert.Contains(t, answerString, tc.expectedRData,
|
||||||
|
"Answer should contain the expected IP address %s, got: %s",
|
||||||
|
tc.expectedRData, answerString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLocalResolver_CNAMEFallback verifies that the resolver correctly falls back
|
||||||
|
// to checking for CNAME records when the requested record type isn't found
|
||||||
|
func TestLocalResolver_CNAMEFallback(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
|
||||||
|
// Create a CNAME record (but no A record for this name)
|
||||||
|
cnameRecord := nbdns.SimpleRecord{
|
||||||
|
Name: "alias.example.com.",
|
||||||
|
Type: int(dns.TypeCNAME),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "target.example.com.",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an A record for the CNAME target
|
||||||
|
targetRecord := nbdns.SimpleRecord{
|
||||||
|
Name: "target.example.com.",
|
||||||
|
Type: int(dns.TypeA),
|
||||||
|
Class: nbdns.DefaultClass,
|
||||||
|
TTL: 300,
|
||||||
|
RData: "192.168.100.100",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update resolver with both records
|
||||||
|
resolver.Update([]nbdns.SimpleRecord{cnameRecord, targetRecord})
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
queryName string
|
||||||
|
queryType uint16
|
||||||
|
expectedType string
|
||||||
|
expectedRData string
|
||||||
|
shouldResolve bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Directly query CNAME record",
|
||||||
|
queryName: "alias.example.com.",
|
||||||
|
queryType: dns.TypeCNAME,
|
||||||
|
expectedType: "CNAME",
|
||||||
|
expectedRData: "target.example.com.",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query A record but get CNAME fallback",
|
||||||
|
queryName: "alias.example.com.",
|
||||||
|
queryType: dns.TypeA,
|
||||||
|
expectedType: "CNAME",
|
||||||
|
expectedRData: "target.example.com.",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query AAAA record but get CNAME fallback",
|
||||||
|
queryName: "alias.example.com.",
|
||||||
|
queryType: dns.TypeAAAA,
|
||||||
|
expectedType: "CNAME",
|
||||||
|
expectedRData: "target.example.com.",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query direct A record",
|
||||||
|
queryName: "target.example.com.",
|
||||||
|
queryType: dns.TypeA,
|
||||||
|
expectedType: "A",
|
||||||
|
expectedRData: "192.168.100.100",
|
||||||
|
shouldResolve: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Query non-existent name",
|
||||||
|
queryName: "nonexistent.example.com.",
|
||||||
|
queryType: dns.TypeA,
|
||||||
|
shouldResolve: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var responseMSG *dns.Msg
|
||||||
|
|
||||||
|
// Create DNS query with the test case parameters
|
||||||
|
msg := new(dns.Msg).SetQuestion(tc.queryName, tc.queryType)
|
||||||
|
|
||||||
|
// Create mock response writer to capture the response
|
||||||
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform DNS query
|
||||||
|
resolver.ServeDNS(responseWriter, msg)
|
||||||
|
|
||||||
|
// Check if we expect a successful resolution
|
||||||
|
if !tc.shouldResolve {
|
||||||
|
if responseMSG == nil || len(responseMSG.Answer) == 0 || responseMSG.Rcode != dns.RcodeSuccess {
|
||||||
|
// Expected no resolution, test passes
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("Expected no resolution for %s, but got answer: %v", tc.queryName, responseMSG.Answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we got a successful response
|
||||||
|
require.NotNil(t, responseMSG, "Should have received a response message")
|
||||||
|
require.Equal(t, dns.RcodeSuccess, responseMSG.Rcode, "Response should have success status code")
|
||||||
|
require.Greater(t, len(responseMSG.Answer), 0, "Response should contain at least one answer")
|
||||||
|
|
||||||
|
// Verify the response contains the expected data
|
||||||
|
answerString := responseMSG.Answer[0].String()
|
||||||
|
assert.Contains(t, answerString, tc.expectedType,
|
||||||
|
"Answer should be of type %s, got: %s", tc.expectedType, answerString)
|
||||||
|
assert.Contains(t, answerString, tc.expectedRData,
|
||||||
|
"Answer should contain the expected data %s, got: %s", tc.expectedRData, answerString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLocalResolver_ServeDNS(t *testing.T) {
|
|
||||||
recordA := nbdns.SimpleRecord{
|
|
||||||
Name: "peera.netbird.cloud.",
|
|
||||||
Type: 1,
|
|
||||||
Class: nbdns.DefaultClass,
|
|
||||||
TTL: 300,
|
|
||||||
RData: "1.2.3.4",
|
|
||||||
}
|
|
||||||
|
|
||||||
recordCNAME := nbdns.SimpleRecord{
|
|
||||||
Name: "peerb.netbird.cloud.",
|
|
||||||
Type: 5,
|
|
||||||
Class: nbdns.DefaultClass,
|
|
||||||
TTL: 300,
|
|
||||||
RData: "www.netbird.io",
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
inputRecord nbdns.SimpleRecord
|
|
||||||
inputMSG *dns.Msg
|
|
||||||
responseShouldBeNil bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Should Resolve A Record",
|
|
||||||
inputRecord: recordA,
|
|
||||||
inputMSG: new(dns.Msg).SetQuestion(recordA.Name, dns.TypeA),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Resolve CNAME Record",
|
|
||||||
inputRecord: recordCNAME,
|
|
||||||
inputMSG: new(dns.Msg).SetQuestion(recordCNAME.Name, dns.TypeCNAME),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Should Not Write When Not Found A Record",
|
|
||||||
inputRecord: recordA,
|
|
||||||
inputMSG: new(dns.Msg).SetQuestion("not.found.com", dns.TypeA),
|
|
||||||
responseShouldBeNil: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
|
||||||
resolver := &localResolver{
|
|
||||||
registeredMap: make(registrationMap),
|
|
||||||
}
|
|
||||||
_, _ = resolver.registerRecord(testCase.inputRecord)
|
|
||||||
var responseMSG *dns.Msg
|
|
||||||
responseWriter := &mockResponseWriter{
|
|
||||||
WriteMsgFunc: func(m *dns.Msg) error {
|
|
||||||
responseMSG = m
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resolver.ServeDNS(responseWriter, testCase.inputMSG)
|
|
||||||
|
|
||||||
if responseMSG == nil || len(responseMSG.Answer) == 0 {
|
|
||||||
if testCase.responseShouldBeNil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatalf("should write a response message")
|
|
||||||
}
|
|
||||||
|
|
||||||
answerString := responseMSG.Answer[0].String()
|
|
||||||
if !strings.Contains(answerString, testCase.inputRecord.Name) {
|
|
||||||
t.Fatalf("answer doesn't contain the same domain name: \nWant: %s\nGot:%s", testCase.name, answerString)
|
|
||||||
}
|
|
||||||
if !strings.Contains(answerString, dns.Type(testCase.inputRecord.Type).String()) {
|
|
||||||
t.Fatalf("answer doesn't contain the correct type: \nWant: %s\nGot:%s", dns.Type(testCase.inputRecord.Type).String(), answerString)
|
|
||||||
}
|
|
||||||
if !strings.Contains(answerString, testCase.inputRecord.RData) {
|
|
||||||
t.Fatalf("answer doesn't contain the same address: \nWant: %s\nGot:%s", testCase.inputRecord.RData, answerString)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -62,8 +62,8 @@ func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
|
|||||||
return fmt.Errorf("method UpdateDNSServer is not implemented")
|
return fmt.Errorf("method UpdateDNSServer is not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockServer) SearchDomains() []string {
|
func (m *MockServer) SearchDomains() domain.List {
|
||||||
return make([]string, 0)
|
return make(domain.List, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProbeAvailability mocks implementation of ProbeAvailability from the Server interface
|
// ProbeAvailability mocks implementation of ProbeAvailability from the Server interface
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
type mockResponseWriter struct {
|
|
||||||
WriteMsgFunc func(m *dns.Msg) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *mockResponseWriter) WriteMsg(m *dns.Msg) error {
|
|
||||||
if rw.WriteMsgFunc != nil {
|
|
||||||
return rw.WriteMsgFunc(m)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *mockResponseWriter) LocalAddr() net.Addr { return nil }
|
|
||||||
func (rw *mockResponseWriter) RemoteAddr() net.Addr { return nil }
|
|
||||||
func (rw *mockResponseWriter) Write([]byte) (int, error) { return 0, nil }
|
|
||||||
func (rw *mockResponseWriter) Close() error { return nil }
|
|
||||||
func (rw *mockResponseWriter) TsigStatus() error { return nil }
|
|
||||||
func (rw *mockResponseWriter) TsigTimersOnly(bool) {}
|
|
||||||
func (rw *mockResponseWriter) Hijack() {}
|
|
||||||
@@ -125,10 +125,10 @@ func (n *networkManagerDbusConfigurator) applyDNSConfig(config HostDNSConfig, st
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if dConf.MatchOnly {
|
if dConf.MatchOnly {
|
||||||
matchDomains = append(matchDomains, "~."+dConf.Domain)
|
matchDomains = append(matchDomains, "~."+dConf.Domain.PunycodeString())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
searchDomains = append(searchDomains, dConf.Domain)
|
searchDomains = append(searchDomains, dConf.Domain.PunycodeString())
|
||||||
}
|
}
|
||||||
|
|
||||||
newDomainList := append(searchDomains, matchDomains...) //nolint:gocritic
|
newDomainList := append(searchDomains, matchDomains...) //nolint:gocritic
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/listener"
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
type notifier struct {
|
type notifier struct {
|
||||||
listener listener.NetworkChangeListener
|
listener listener.NetworkChangeListener
|
||||||
listenerMux sync.Mutex
|
listenerMux sync.Mutex
|
||||||
searchDomains []string
|
searchDomains domain.List
|
||||||
}
|
}
|
||||||
|
|
||||||
func newNotifier(initialSearchDomains []string) *notifier {
|
func newNotifier(initialSearchDomains domain.List) *notifier {
|
||||||
sort.Strings(initialSearchDomains)
|
|
||||||
return ¬ifier{
|
return ¬ifier{
|
||||||
searchDomains: initialSearchDomains,
|
searchDomains: initialSearchDomains,
|
||||||
}
|
}
|
||||||
@@ -27,16 +25,8 @@ func (n *notifier) setListener(listener listener.NetworkChangeListener) {
|
|||||||
n.listener = listener
|
n.listener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifier) onNewSearchDomains(searchDomains []string) {
|
func (n *notifier) onNewSearchDomains(searchDomains domain.List) {
|
||||||
sort.Strings(searchDomains)
|
if searchDomains.Equal(n.searchDomains) {
|
||||||
|
|
||||||
if len(n.searchDomains) != len(searchDomains) {
|
|
||||||
n.searchDomains = searchDomains
|
|
||||||
n.notify()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if reflect.DeepEqual(n.searchDomains, searchDomains) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import (
|
|||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/netstack"
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/local"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/types"
|
||||||
"github.com/netbirdio/netbird/client/internal/listener"
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
@@ -42,14 +44,12 @@ type Server interface {
|
|||||||
DnsIP() string
|
DnsIP() string
|
||||||
UpdateDNSServer(serial uint64, update nbdns.Config) error
|
UpdateDNSServer(serial uint64, update nbdns.Config) error
|
||||||
OnUpdatedHostDNSServer(strings []string)
|
OnUpdatedHostDNSServer(strings []string)
|
||||||
SearchDomains() []string
|
SearchDomains() domain.List
|
||||||
ProbeAvailability()
|
ProbeAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
type handlerID string
|
|
||||||
|
|
||||||
type nsGroupsByDomain struct {
|
type nsGroupsByDomain struct {
|
||||||
domain string
|
domain domain.Domain
|
||||||
groups []*nbdns.NameServerGroup
|
groups []*nbdns.NameServerGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ type DefaultServer struct {
|
|||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
service service
|
service service
|
||||||
dnsMuxMap registeredHandlerMap
|
dnsMuxMap registeredHandlerMap
|
||||||
localResolver *localResolver
|
localResolver *local.Resolver
|
||||||
wgInterface WGIface
|
wgInterface WGIface
|
||||||
hostManager hostManager
|
hostManager hostManager
|
||||||
updateSerial uint64
|
updateSerial uint64
|
||||||
@@ -84,18 +84,18 @@ type DefaultServer struct {
|
|||||||
|
|
||||||
type handlerWithStop interface {
|
type handlerWithStop interface {
|
||||||
dns.Handler
|
dns.Handler
|
||||||
stop()
|
Stop()
|
||||||
probeAvailability()
|
ProbeAvailability()
|
||||||
id() handlerID
|
ID() types.HandlerID
|
||||||
}
|
}
|
||||||
|
|
||||||
type handlerWrapper struct {
|
type handlerWrapper struct {
|
||||||
domain string
|
domain domain.Domain
|
||||||
handler handlerWithStop
|
handler handlerWithStop
|
||||||
priority int
|
priority int
|
||||||
}
|
}
|
||||||
|
|
||||||
type registeredHandlerMap map[handlerID]handlerWrapper
|
type registeredHandlerMap map[types.HandlerID]handlerWrapper
|
||||||
|
|
||||||
// NewDefaultServer returns a new dns server
|
// NewDefaultServer returns a new dns server
|
||||||
func NewDefaultServer(
|
func NewDefaultServer(
|
||||||
@@ -171,16 +171,14 @@ func newDefaultServer(
|
|||||||
handlerChain := NewHandlerChain()
|
handlerChain := NewHandlerChain()
|
||||||
ctx, stop := context.WithCancel(ctx)
|
ctx, stop := context.WithCancel(ctx)
|
||||||
defaultServer := &DefaultServer{
|
defaultServer := &DefaultServer{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
ctxCancel: stop,
|
ctxCancel: stop,
|
||||||
disableSys: disableSys,
|
disableSys: disableSys,
|
||||||
service: dnsService,
|
service: dnsService,
|
||||||
handlerChain: handlerChain,
|
handlerChain: handlerChain,
|
||||||
extraDomains: make(map[domain.Domain]int),
|
extraDomains: make(map[domain.Domain]int),
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
localResolver: &localResolver{
|
localResolver: local.NewResolver(),
|
||||||
registeredMap: make(registrationMap),
|
|
||||||
},
|
|
||||||
wgInterface: wgInterface,
|
wgInterface: wgInterface,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
stateManager: stateManager,
|
stateManager: stateManager,
|
||||||
@@ -199,7 +197,7 @@ func (s *DefaultServer) RegisterHandler(domains domain.List, handler dns.Handler
|
|||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
defer s.mux.Unlock()
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
s.registerHandler(domains.ToPunycodeList(), handler, priority)
|
s.registerHandler(domains, handler, priority)
|
||||||
|
|
||||||
// TODO: This will take over zones for non-wildcard domains, for which we might not have a handler in the chain
|
// TODO: This will take over zones for non-wildcard domains, for which we might not have a handler in the chain
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
@@ -209,7 +207,7 @@ func (s *DefaultServer) RegisterHandler(domains domain.List, handler dns.Handler
|
|||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, priority int) {
|
func (s *DefaultServer) registerHandler(domains domain.List, handler dns.Handler, priority int) {
|
||||||
log.Debugf("registering handler %s with priority %d", handler, priority)
|
log.Debugf("registering handler %s with priority %d", handler, priority)
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
@@ -226,7 +224,7 @@ func (s *DefaultServer) DeregisterHandler(domains domain.List, priority int) {
|
|||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
defer s.mux.Unlock()
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
s.deregisterHandler(domains.ToPunycodeList(), priority)
|
s.deregisterHandler(domains, priority)
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
zone := toZone(domain)
|
zone := toZone(domain)
|
||||||
s.extraDomains[zone]--
|
s.extraDomains[zone]--
|
||||||
@@ -237,7 +235,7 @@ func (s *DefaultServer) DeregisterHandler(domains domain.List, priority int) {
|
|||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) deregisterHandler(domains []string, priority int) {
|
func (s *DefaultServer) deregisterHandler(domains domain.List, priority int) {
|
||||||
log.Debugf("deregistering handler %v with priority %d", domains, priority)
|
log.Debugf("deregistering handler %v with priority %d", domains, priority)
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
@@ -380,8 +378,8 @@ func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) SearchDomains() []string {
|
func (s *DefaultServer) SearchDomains() domain.List {
|
||||||
var searchDomains []string
|
var searchDomains domain.List
|
||||||
|
|
||||||
for _, dConf := range s.currentConfig.Domains {
|
for _, dConf := range s.currentConfig.Domains {
|
||||||
if dConf.Disabled {
|
if dConf.Disabled {
|
||||||
@@ -403,7 +401,7 @@ func (s *DefaultServer) ProbeAvailability() {
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(mux handlerWithStop) {
|
go func(mux handlerWithStop) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
mux.probeAvailability()
|
mux.ProbeAvailability()
|
||||||
}(mux.handler)
|
}(mux.handler)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -420,7 +418,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
|||||||
s.service.Stop()
|
s.service.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
localMuxUpdates, localRecordsByDomain, err := s.buildLocalHandlerUpdate(update.CustomZones)
|
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("local handler updater: %w", err)
|
return fmt.Errorf("local handler updater: %w", err)
|
||||||
}
|
}
|
||||||
@@ -434,7 +432,7 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
|||||||
s.updateMux(muxUpdates)
|
s.updateMux(muxUpdates)
|
||||||
|
|
||||||
// register local records
|
// register local records
|
||||||
s.updateLocalResolver(localRecordsByDomain)
|
s.localResolver.Update(localRecords)
|
||||||
|
|
||||||
s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort())
|
s.currentConfig = dnsConfigToHostDNSConfig(update, s.service.RuntimeIP(), s.service.RuntimePort())
|
||||||
|
|
||||||
@@ -474,24 +472,22 @@ func (s *DefaultServer) applyHostConfig() {
|
|||||||
|
|
||||||
config := s.currentConfig
|
config := s.currentConfig
|
||||||
|
|
||||||
existingDomains := make(map[string]struct{})
|
existingDomains := make(map[domain.Domain]struct{})
|
||||||
for _, d := range config.Domains {
|
for _, d := range config.Domains {
|
||||||
existingDomains[d.Domain] = struct{}{}
|
existingDomains[d.Domain] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// add extra domains only if they're not already in the config
|
// add extra domains only if they're not already in the config
|
||||||
for domain := range s.extraDomains {
|
for d := range s.extraDomains {
|
||||||
domainStr := domain.PunycodeString()
|
if _, exists := existingDomains[d]; !exists {
|
||||||
|
|
||||||
if _, exists := existingDomains[domainStr]; !exists {
|
|
||||||
config.Domains = append(config.Domains, DomainConfig{
|
config.Domains = append(config.Domains, DomainConfig{
|
||||||
Domain: domainStr,
|
Domain: d,
|
||||||
MatchOnly: true,
|
MatchOnly: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("extra match domains: %v", s.extraDomains)
|
log.Debugf("extra match domains: %v", maps.Keys(s.extraDomains))
|
||||||
|
|
||||||
if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil {
|
if err := s.hostManager.applyDNSConfig(config, s.stateManager); err != nil {
|
||||||
log.Errorf("failed to apply DNS host manager update: %v", err)
|
log.Errorf("failed to apply DNS host manager update: %v", err)
|
||||||
@@ -516,11 +512,9 @@ func (s *DefaultServer) handleErrNoGroupaAll(err error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) buildLocalHandlerUpdate(
|
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, []nbdns.SimpleRecord, error) {
|
||||||
customZones []nbdns.CustomZone,
|
|
||||||
) ([]handlerWrapper, map[string][]nbdns.SimpleRecord, error) {
|
|
||||||
var muxUpdates []handlerWrapper
|
var muxUpdates []handlerWrapper
|
||||||
localRecords := make(map[string][]nbdns.SimpleRecord)
|
var localRecords []nbdns.SimpleRecord
|
||||||
|
|
||||||
for _, customZone := range customZones {
|
for _, customZone := range customZones {
|
||||||
if len(customZone.Records) == 0 {
|
if len(customZone.Records) == 0 {
|
||||||
@@ -529,22 +523,18 @@ func (s *DefaultServer) buildLocalHandlerUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
muxUpdates = append(muxUpdates, handlerWrapper{
|
muxUpdates = append(muxUpdates, handlerWrapper{
|
||||||
domain: customZone.Domain,
|
domain: domain.Domain(customZone.Domain),
|
||||||
handler: s.localResolver,
|
handler: s.localResolver,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
})
|
})
|
||||||
|
|
||||||
// group all records under this domain
|
|
||||||
for _, record := range customZone.Records {
|
for _, record := range customZone.Records {
|
||||||
var class uint16 = dns.ClassINET
|
|
||||||
if record.Class != nbdns.DefaultClass {
|
if record.Class != nbdns.DefaultClass {
|
||||||
log.Warnf("received an invalid class type: %s", record.Class)
|
log.Warnf("received an invalid class type: %s", record.Class)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// zone records contain the fqdn, so we can just flatten them
|
||||||
key := buildRecordKey(record.Name, class, uint16(record.Type))
|
localRecords = append(localRecords, record)
|
||||||
|
|
||||||
localRecords[key] = append(localRecords[key], record)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,7 +617,7 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(handler.upstreamServers) == 0 {
|
if len(handler.upstreamServers) == 0 {
|
||||||
handler.stop()
|
handler.Stop()
|
||||||
log.Errorf("received a nameserver group with an invalid nameserver list")
|
log.Errorf("received a nameserver group with an invalid nameserver list")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -655,8 +645,8 @@ func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomai
|
|||||||
func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
||||||
// this will introduce a short period of time when the server is not able to handle DNS requests
|
// this will introduce a short period of time when the server is not able to handle DNS requests
|
||||||
for _, existing := range s.dnsMuxMap {
|
for _, existing := range s.dnsMuxMap {
|
||||||
s.deregisterHandler([]string{existing.domain}, existing.priority)
|
s.deregisterHandler(domain.List{existing.domain}, existing.priority)
|
||||||
existing.handler.stop()
|
existing.handler.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
muxUpdateMap := make(registeredHandlerMap)
|
muxUpdateMap := make(registeredHandlerMap)
|
||||||
@@ -666,8 +656,8 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
|||||||
if update.domain == nbdns.RootZone {
|
if update.domain == nbdns.RootZone {
|
||||||
containsRootUpdate = true
|
containsRootUpdate = true
|
||||||
}
|
}
|
||||||
s.registerHandler([]string{update.domain}, update.handler, update.priority)
|
s.registerHandler(domain.List{update.domain}, update.handler, update.priority)
|
||||||
muxUpdateMap[update.handler.id()] = update
|
muxUpdateMap[update.handler.ID()] = update
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's no root update and we had a root handler, restore it
|
// If there's no root update and we had a root handler, restore it
|
||||||
@@ -683,33 +673,6 @@ func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
|||||||
s.dnsMuxMap = muxUpdateMap
|
s.dnsMuxMap = muxUpdateMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) updateLocalResolver(update map[string][]nbdns.SimpleRecord) {
|
|
||||||
// remove old records that are no longer present
|
|
||||||
for key := range s.localResolver.registeredMap {
|
|
||||||
_, found := update[key]
|
|
||||||
if !found {
|
|
||||||
s.localResolver.deleteRecord(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedMap := make(registrationMap)
|
|
||||||
for _, recs := range update {
|
|
||||||
for _, rec := range recs {
|
|
||||||
// convert the record to a dns.RR and register
|
|
||||||
key, err := s.localResolver.registerRecord(rec)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("got an error while registering the record (%s), error: %v",
|
|
||||||
rec.String(), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedMap[key] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.localResolver.registeredMap = updatedMap
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNSHostPort(ns nbdns.NameServer) string {
|
func getNSHostPort(ns nbdns.NameServer) string {
|
||||||
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
|
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
|
||||||
}
|
}
|
||||||
@@ -722,7 +685,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
handler dns.Handler,
|
handler dns.Handler,
|
||||||
priority int,
|
priority int,
|
||||||
) (deactivate func(error), reactivate func()) {
|
) (deactivate func(error), reactivate func()) {
|
||||||
var removeIndex map[string]int
|
var removeIndex map[domain.Domain]int
|
||||||
deactivate = func(err error) {
|
deactivate = func(err error) {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
defer s.mux.Unlock()
|
defer s.mux.Unlock()
|
||||||
@@ -730,20 +693,20 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
l := log.WithField("nameservers", nsGroup.NameServers)
|
l := log.WithField("nameservers", nsGroup.NameServers)
|
||||||
l.Info("Temporarily deactivating nameservers group due to timeout")
|
l.Info("Temporarily deactivating nameservers group due to timeout")
|
||||||
|
|
||||||
removeIndex = make(map[string]int)
|
removeIndex = make(map[domain.Domain]int)
|
||||||
for _, domain := range nsGroup.Domains {
|
for _, domain := range nsGroup.Domains {
|
||||||
removeIndex[domain] = -1
|
removeIndex[domain] = -1
|
||||||
}
|
}
|
||||||
if nsGroup.Primary {
|
if nsGroup.Primary {
|
||||||
removeIndex[nbdns.RootZone] = -1
|
removeIndex[nbdns.RootZone] = -1
|
||||||
s.currentConfig.RouteAll = false
|
s.currentConfig.RouteAll = false
|
||||||
s.deregisterHandler([]string{nbdns.RootZone}, priority)
|
s.deregisterHandler(domain.List{nbdns.RootZone}, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, item := range s.currentConfig.Domains {
|
for i, item := range s.currentConfig.Domains {
|
||||||
if _, found := removeIndex[item.Domain]; found {
|
if _, found := removeIndex[item.Domain]; found {
|
||||||
s.currentConfig.Domains[i].Disabled = true
|
s.currentConfig.Domains[i].Disabled = true
|
||||||
s.deregisterHandler([]string{item.Domain}, priority)
|
s.deregisterHandler(domain.List{item.Domain}, priority)
|
||||||
removeIndex[item.Domain] = i
|
removeIndex[item.Domain] = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -767,12 +730,12 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
defer s.mux.Unlock()
|
defer s.mux.Unlock()
|
||||||
|
|
||||||
for domain, i := range removeIndex {
|
for d, i := range removeIndex {
|
||||||
if i == -1 || i >= len(s.currentConfig.Domains) || s.currentConfig.Domains[i].Domain != domain {
|
if i == -1 || i >= len(s.currentConfig.Domains) || s.currentConfig.Domains[i].Domain != d{
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.currentConfig.Domains[i].Disabled = false
|
s.currentConfig.Domains[i].Disabled = false
|
||||||
s.registerHandler([]string{domain}, handler, priority)
|
s.registerHandler(domain.List{d}, handler, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := log.WithField("nameservers", nsGroup.NameServers)
|
l := log.WithField("nameservers", nsGroup.NameServers)
|
||||||
@@ -780,7 +743,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
|
|
||||||
if nsGroup.Primary {
|
if nsGroup.Primary {
|
||||||
s.currentConfig.RouteAll = true
|
s.currentConfig.RouteAll = true
|
||||||
s.registerHandler([]string{nbdns.RootZone}, handler, priority)
|
s.registerHandler(domain.List{nbdns.RootZone}, handler, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.applyHostConfig()
|
s.applyHostConfig()
|
||||||
@@ -812,7 +775,7 @@ func (s *DefaultServer) addHostRootZone() {
|
|||||||
handler.deactivate = func(error) {}
|
handler.deactivate = func(error) {}
|
||||||
handler.reactivate = func() {}
|
handler.reactivate = func() {}
|
||||||
|
|
||||||
s.registerHandler([]string{nbdns.RootZone}, handler, PriorityDefault)
|
s.registerHandler(domain.List{nbdns.RootZone}, handler, PriorityDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
|
func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
|
||||||
@@ -827,7 +790,7 @@ func (s *DefaultServer) updateNSGroupStates(groups []*nbdns.NameServerGroup) {
|
|||||||
state := peer.NSGroupState{
|
state := peer.NSGroupState{
|
||||||
ID: generateGroupKey(group),
|
ID: generateGroupKey(group),
|
||||||
Servers: servers,
|
Servers: servers,
|
||||||
Domains: group.Domains,
|
Domains: group.Domains.ToPunycodeList(),
|
||||||
// The probe will determine the state, default enabled
|
// The probe will determine the state, default enabled
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
Error: nil,
|
Error: nil,
|
||||||
@@ -860,7 +823,7 @@ func generateGroupKey(nsGroup *nbdns.NameServerGroup) string {
|
|||||||
|
|
||||||
// groupNSGroupsByDomain groups nameserver groups by their match domains
|
// groupNSGroupsByDomain groups nameserver groups by their match domains
|
||||||
func groupNSGroupsByDomain(nsGroups []*nbdns.NameServerGroup) []nsGroupsByDomain {
|
func groupNSGroupsByDomain(nsGroups []*nbdns.NameServerGroup) []nsGroupsByDomain {
|
||||||
domainMap := make(map[string][]*nbdns.NameServerGroup)
|
domainMap := make(map[domain.Domain][]*nbdns.NameServerGroup)
|
||||||
|
|
||||||
for _, group := range nsGroups {
|
for _, group := range nsGroups {
|
||||||
if group.Primary {
|
if group.Primary {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,6 +22,9 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
pfmock "github.com/netbirdio/netbird/client/iface/mocks"
|
pfmock "github.com/netbirdio/netbird/client/iface/mocks"
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/local"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/test"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/dns/types"
|
||||||
"github.com/netbirdio/netbird/client/internal/netflow"
|
"github.com/netbirdio/netbird/client/internal/netflow"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
@@ -43,10 +45,9 @@ func (w *mocWGIface) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *mocWGIface) Address() wgaddr.Address {
|
func (w *mocWGIface) Address() wgaddr.Address {
|
||||||
ip, network, _ := net.ParseCIDR("100.66.100.0/24")
|
|
||||||
return wgaddr.Address{
|
return wgaddr.Address{
|
||||||
IP: ip,
|
IP: netip.MustParseAddr("100.66.100.1"),
|
||||||
Network: network,
|
Network: netip.MustParsePrefix("100.66.100.0/24"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ func init() {
|
|||||||
formatter.SetTextFormatter(log.StandardLogger())
|
formatter.SetTextFormatter(log.StandardLogger())
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamResolverBase {
|
func generateDummyHandler(domain domain.Domain, servers []nbdns.NameServer) *upstreamResolverBase {
|
||||||
var srvs []string
|
var srvs []string
|
||||||
for _, srv := range servers {
|
for _, srv := range servers {
|
||||||
srvs = append(srvs, getNSHostPort(srv))
|
srvs = append(srvs, getNSHostPort(srv))
|
||||||
@@ -107,6 +108,7 @@ func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateDNSServer(t *testing.T) {
|
func TestUpdateDNSServer(t *testing.T) {
|
||||||
|
|
||||||
nameServers := []nbdns.NameServer{
|
nameServers := []nbdns.NameServer{
|
||||||
{
|
{
|
||||||
IP: netip.MustParseAddr("8.8.8.8"),
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
@@ -120,22 +122,21 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
dummyHandler := &localResolver{}
|
dummyHandler := local.NewResolver()
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
initUpstreamMap registeredHandlerMap
|
initUpstreamMap registeredHandlerMap
|
||||||
initLocalMap registrationMap
|
initLocalRecords []nbdns.SimpleRecord
|
||||||
initSerial uint64
|
initSerial uint64
|
||||||
inputSerial uint64
|
inputSerial uint64
|
||||||
inputUpdate nbdns.Config
|
inputUpdate nbdns.Config
|
||||||
shouldFail bool
|
shouldFail bool
|
||||||
expectedUpstreamMap registeredHandlerMap
|
expectedUpstreamMap registeredHandlerMap
|
||||||
expectedLocalMap registrationMap
|
expectedLocalQs []dns.Question
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Initial Config Should Succeed",
|
name: "Initial Config Should Succeed",
|
||||||
initLocalMap: make(registrationMap),
|
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
initUpstreamMap: make(registeredHandlerMap),
|
||||||
initSerial: 0,
|
initSerial: 0,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
@@ -149,7 +150,7 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
{
|
{
|
||||||
Domains: []string{"netbird.io"},
|
Domains: domain.List{"netbird.io"},
|
||||||
NameServers: nameServers,
|
NameServers: nameServers,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -159,30 +160,30 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedUpstreamMap: registeredHandlerMap{
|
expectedUpstreamMap: registeredHandlerMap{
|
||||||
generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{
|
generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{
|
||||||
domain: "netbird.io",
|
domain: "netbird.io",
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
dummyHandler.id(): handlerWrapper{
|
dummyHandler.ID(): handlerWrapper{
|
||||||
domain: "netbird.cloud",
|
domain: "netbird.cloud",
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
generateDummyHandler(".", nameServers).id(): handlerWrapper{
|
generateDummyHandler(".", nameServers).ID(): handlerWrapper{
|
||||||
domain: nbdns.RootZone,
|
domain: nbdns.RootZone,
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityDefault,
|
priority: PriorityDefault,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
expectedLocalQs: []dns.Question{{Name: "peera.netbird.cloud.", Qtype: dns.TypeA, Qclass: dns.ClassINET}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "New Config Should Succeed",
|
name: "New Config Should Succeed",
|
||||||
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
initLocalRecords: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: 1, Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}},
|
||||||
initUpstreamMap: registeredHandlerMap{
|
initUpstreamMap: registeredHandlerMap{
|
||||||
generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
|
generateDummyHandler(domain.Domain(zoneRecords[0].Name), nameServers).ID(): handlerWrapper{
|
||||||
domain: buildRecordKey(zoneRecords[0].Name, 1, 1),
|
domain: "netbird.cloud",
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
@@ -199,13 +200,13 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
{
|
{
|
||||||
Domains: []string{"netbird.io"},
|
Domains: domain.List{"netbird.io"},
|
||||||
NameServers: nameServers,
|
NameServers: nameServers,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedUpstreamMap: registeredHandlerMap{
|
expectedUpstreamMap: registeredHandlerMap{
|
||||||
generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{
|
generateDummyHandler("netbird.io", nameServers).ID(): handlerWrapper{
|
||||||
domain: "netbird.io",
|
domain: "netbird.io",
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
@@ -216,22 +217,22 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
expectedLocalQs: []dns.Question{{Name: zoneRecords[0].Name, Qtype: 1, Qclass: 1}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Smaller Config Serial Should Be Skipped",
|
name: "Smaller Config Serial Should Be Skipped",
|
||||||
initLocalMap: make(registrationMap),
|
initLocalRecords: []nbdns.SimpleRecord{},
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
initUpstreamMap: make(registeredHandlerMap),
|
||||||
initSerial: 2,
|
initSerial: 2,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
|
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
|
||||||
initLocalMap: make(registrationMap),
|
initLocalRecords: []nbdns.SimpleRecord{},
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
initUpstreamMap: make(registeredHandlerMap),
|
||||||
initSerial: 0,
|
initSerial: 0,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{
|
inputUpdate: nbdns.Config{
|
||||||
ServiceEnable: true,
|
ServiceEnable: true,
|
||||||
CustomZones: []nbdns.CustomZone{
|
CustomZones: []nbdns.CustomZone{
|
||||||
@@ -249,11 +250,11 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid NS Group Nameservers list Should Fail",
|
name: "Invalid NS Group Nameservers list Should Fail",
|
||||||
initLocalMap: make(registrationMap),
|
initLocalRecords: []nbdns.SimpleRecord{},
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
initUpstreamMap: make(registeredHandlerMap),
|
||||||
initSerial: 0,
|
initSerial: 0,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{
|
inputUpdate: nbdns.Config{
|
||||||
ServiceEnable: true,
|
ServiceEnable: true,
|
||||||
CustomZones: []nbdns.CustomZone{
|
CustomZones: []nbdns.CustomZone{
|
||||||
@@ -271,11 +272,11 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid Custom Zone Records list Should Skip",
|
name: "Invalid Custom Zone Records list Should Skip",
|
||||||
initLocalMap: make(registrationMap),
|
initLocalRecords: []nbdns.SimpleRecord{},
|
||||||
initUpstreamMap: make(registeredHandlerMap),
|
initUpstreamMap: make(registeredHandlerMap),
|
||||||
initSerial: 0,
|
initSerial: 0,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{
|
inputUpdate: nbdns.Config{
|
||||||
ServiceEnable: true,
|
ServiceEnable: true,
|
||||||
CustomZones: []nbdns.CustomZone{
|
CustomZones: []nbdns.CustomZone{
|
||||||
@@ -290,18 +291,18 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).id(): handlerWrapper{
|
expectedUpstreamMap: registeredHandlerMap{generateDummyHandler(".", nameServers).ID(): handlerWrapper{
|
||||||
domain: ".",
|
domain: ".",
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityDefault,
|
priority: PriorityDefault,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Empty Config Should Succeed and Clean Maps",
|
name: "Empty Config Should Succeed and Clean Maps",
|
||||||
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
initLocalRecords: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}},
|
||||||
initUpstreamMap: registeredHandlerMap{
|
initUpstreamMap: registeredHandlerMap{
|
||||||
generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
|
generateDummyHandler(domain.Domain(zoneRecords[0].Name), nameServers).ID(): handlerWrapper{
|
||||||
domain: zoneRecords[0].Name,
|
domain: domain.Domain(zoneRecords[0].Name),
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
@@ -310,14 +311,14 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{ServiceEnable: true},
|
inputUpdate: nbdns.Config{ServiceEnable: true},
|
||||||
expectedUpstreamMap: make(registeredHandlerMap),
|
expectedUpstreamMap: make(registeredHandlerMap),
|
||||||
expectedLocalMap: make(registrationMap),
|
expectedLocalQs: []dns.Question{},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Disabled Service Should clean map",
|
name: "Disabled Service Should clean map",
|
||||||
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
initLocalRecords: []nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}},
|
||||||
initUpstreamMap: registeredHandlerMap{
|
initUpstreamMap: registeredHandlerMap{
|
||||||
generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
|
generateDummyHandler(domain.Domain(zoneRecords[0].Name), nameServers).ID(): handlerWrapper{
|
||||||
domain: zoneRecords[0].Name,
|
domain: domain.Domain(zoneRecords[0].Name),
|
||||||
handler: dummyHandler,
|
handler: dummyHandler,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
@@ -326,7 +327,7 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{ServiceEnable: false},
|
inputUpdate: nbdns.Config{ServiceEnable: false},
|
||||||
expectedUpstreamMap: make(registeredHandlerMap),
|
expectedUpstreamMap: make(registeredHandlerMap),
|
||||||
expectedLocalMap: make(registrationMap),
|
expectedLocalQs: []dns.Question{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +378,7 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
dnsServer.dnsMuxMap = testCase.initUpstreamMap
|
dnsServer.dnsMuxMap = testCase.initUpstreamMap
|
||||||
dnsServer.localResolver.registeredMap = testCase.initLocalMap
|
dnsServer.localResolver.Update(testCase.initLocalRecords)
|
||||||
dnsServer.updateSerial = testCase.initSerial
|
dnsServer.updateSerial = testCase.initSerial
|
||||||
|
|
||||||
err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
|
err = dnsServer.UpdateDNSServer(testCase.inputSerial, testCase.inputUpdate)
|
||||||
@@ -399,15 +400,23 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(dnsServer.localResolver.registeredMap) != len(testCase.expectedLocalMap) {
|
var responseMSG *dns.Msg
|
||||||
t.Fatalf("update local failed, registered map size is different than expected, want %d, got %d", len(testCase.expectedLocalMap), len(dnsServer.localResolver.registeredMap))
|
responseWriter := &test.MockResponseWriter{
|
||||||
|
WriteMsgFunc: func(m *dns.Msg) error {
|
||||||
|
responseMSG = m
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, q := range testCase.expectedLocalQs {
|
||||||
|
dnsServer.localResolver.ServeDNS(responseWriter, &dns.Msg{
|
||||||
|
Question: []dns.Question{q},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for key := range testCase.expectedLocalMap {
|
if len(testCase.expectedLocalQs) > 0 {
|
||||||
_, found := dnsServer.localResolver.registeredMap[key]
|
assert.NotNil(t, responseMSG, "response message should not be nil")
|
||||||
if !found {
|
assert.Equal(t, dns.RcodeSuccess, responseMSG.Rcode, "response code should be success")
|
||||||
t.Fatalf("update local failed, key %s was not found in the localResolver.registeredMap: %#v", key, dnsServer.localResolver.registeredMap)
|
assert.NotEmpty(t, responseMSG.Answer, "response message should have answers")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -453,17 +462,10 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
_, ipNet, err := net.ParseCIDR("100.66.100.1/32")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("parse CIDR: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
packetfilter := pfmock.NewMockPacketFilter(ctrl)
|
packetfilter := pfmock.NewMockPacketFilter(ctrl)
|
||||||
packetfilter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).AnyTimes()
|
packetfilter.EXPECT().DropOutgoing(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
|
packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
|
||||||
packetfilter.EXPECT().RemovePacketHook(gomock.Any())
|
packetfilter.EXPECT().RemovePacketHook(gomock.Any())
|
||||||
packetfilter.EXPECT().SetNetwork(ipNet)
|
|
||||||
|
|
||||||
if err := wgIface.SetFilter(packetfilter); err != nil {
|
if err := wgIface.SetFilter(packetfilter); err != nil {
|
||||||
t.Errorf("set packet filter: %v", err)
|
t.Errorf("set packet filter: %v", err)
|
||||||
@@ -490,12 +492,13 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|||||||
|
|
||||||
dnsServer.dnsMuxMap = registeredHandlerMap{
|
dnsServer.dnsMuxMap = registeredHandlerMap{
|
||||||
"id1": handlerWrapper{
|
"id1": handlerWrapper{
|
||||||
domain: zoneRecords[0].Name,
|
domain: domain.Domain(zoneRecords[0].Name),
|
||||||
handler: &localResolver{},
|
handler: &local.Resolver{},
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
dnsServer.localResolver.registeredMap = registrationMap{"netbird.cloud": struct{}{}}
|
//dnsServer.localResolver.RegisteredMap = local.RegistrationMap{local.BuildRecordKey("netbird.cloud", dns.ClassINET, dns.TypeA): struct{}{}}
|
||||||
|
dnsServer.localResolver.Update([]nbdns.SimpleRecord{{Name: "netbird.cloud", Type: int(dns.TypeA), Class: nbdns.DefaultClass, TTL: 300, RData: "10.0.0.1"}})
|
||||||
dnsServer.updateSerial = 0
|
dnsServer.updateSerial = 0
|
||||||
|
|
||||||
nameServers := []nbdns.NameServer{
|
nameServers := []nbdns.NameServer{
|
||||||
@@ -521,7 +524,7 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|||||||
},
|
},
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
{
|
{
|
||||||
Domains: []string{"netbird.io"},
|
Domains: domain.List{"netbird.io"},
|
||||||
NameServers: nameServers,
|
NameServers: nameServers,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -582,12 +585,12 @@ func TestDNSServerStartStop(t *testing.T) {
|
|||||||
}
|
}
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
defer dnsServer.Stop()
|
defer dnsServer.Stop()
|
||||||
_, err = dnsServer.localResolver.registerRecord(zoneRecords[0])
|
err = dnsServer.localResolver.RegisterRecord(zoneRecords[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsServer.registerHandler([]string{"netbird.cloud"}, dnsServer.localResolver, 1)
|
dnsServer.registerHandler(domain.List{"netbird.cloud"}, dnsServer.localResolver, 1)
|
||||||
|
|
||||||
resolver := &net.Resolver{
|
resolver := &net.Resolver{
|
||||||
PreferGo: true,
|
PreferGo: true,
|
||||||
@@ -630,13 +633,11 @@ func TestDNSServerStartStop(t *testing.T) {
|
|||||||
func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
|
func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
|
||||||
hostManager := &mockHostConfigurator{}
|
hostManager := &mockHostConfigurator{}
|
||||||
server := DefaultServer{
|
server := DefaultServer{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
service: NewServiceViaMemory(&mocWGIface{}),
|
service: NewServiceViaMemory(&mocWGIface{}),
|
||||||
localResolver: &localResolver{
|
localResolver: local.NewResolver(),
|
||||||
registeredMap: make(registrationMap),
|
handlerChain: NewHandlerChain(),
|
||||||
},
|
hostManager: hostManager,
|
||||||
handlerChain: NewHandlerChain(),
|
|
||||||
hostManager: hostManager,
|
|
||||||
currentConfig: HostDNSConfig{
|
currentConfig: HostDNSConfig{
|
||||||
Domains: []DomainConfig{
|
Domains: []DomainConfig{
|
||||||
{false, "domain0", false},
|
{false, "domain0", false},
|
||||||
@@ -649,48 +650,48 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
|
|||||||
|
|
||||||
var domainsUpdate string
|
var domainsUpdate string
|
||||||
hostManager.applyDNSConfigFunc = func(config HostDNSConfig, statemanager *statemanager.Manager) error {
|
hostManager.applyDNSConfigFunc = func(config HostDNSConfig, statemanager *statemanager.Manager) error {
|
||||||
domains := []string{}
|
domains := domain.List{}
|
||||||
for _, item := range config.Domains {
|
for _, item := range config.Domains {
|
||||||
if item.Disabled {
|
if item.Disabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains = append(domains, item.Domain)
|
domains = append(domains, item.Domain)
|
||||||
}
|
}
|
||||||
domainsUpdate = strings.Join(domains, ",")
|
domainsUpdate = domains.PunycodeString()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate, reactivate := server.upstreamCallbacks(&nbdns.NameServerGroup{
|
deactivate, reactivate := server.upstreamCallbacks(&nbdns.NameServerGroup{
|
||||||
Domains: []string{"domain1"},
|
Domains: domain.List{"domain1"},
|
||||||
NameServers: []nbdns.NameServer{
|
NameServers: []nbdns.NameServer{
|
||||||
{IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53},
|
{IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||||
},
|
},
|
||||||
}, nil, 0)
|
}, nil, 0)
|
||||||
|
|
||||||
deactivate(nil)
|
deactivate(nil)
|
||||||
expected := "domain0,domain2"
|
expected := "domain0, domain2"
|
||||||
domains := []string{}
|
domains := domain.List{}
|
||||||
for _, item := range server.currentConfig.Domains {
|
for _, item := range server.currentConfig.Domains {
|
||||||
if item.Disabled {
|
if item.Disabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains = append(domains, item.Domain)
|
domains = append(domains, item.Domain)
|
||||||
}
|
}
|
||||||
got := strings.Join(domains, ",")
|
got := domains.PunycodeString()
|
||||||
if expected != got {
|
if expected != got {
|
||||||
t.Errorf("expected domains list: %q, got %q", expected, got)
|
t.Errorf("expected domains list: %q, got %q", expected, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
reactivate()
|
reactivate()
|
||||||
expected = "domain0,domain1,domain2"
|
expected = "domain0, domain1, domain2"
|
||||||
domains = []string{}
|
domains = domain.List{}
|
||||||
for _, item := range server.currentConfig.Domains {
|
for _, item := range server.currentConfig.Domains {
|
||||||
if item.Disabled {
|
if item.Disabled {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domains = append(domains, item.Domain)
|
domains = append(domains, item.Domain)
|
||||||
}
|
}
|
||||||
got = strings.Join(domains, ",")
|
got = domains.PunycodeString()
|
||||||
if expected != got {
|
if expected != got {
|
||||||
t.Errorf("expected domains list: %q, got %q", expected, domainsUpdate)
|
t.Errorf("expected domains list: %q, got %q", expected, domainsUpdate)
|
||||||
}
|
}
|
||||||
@@ -858,7 +859,7 @@ func TestDNSPermanent_matchOnly(t *testing.T) {
|
|||||||
Port: 53,
|
Port: 53,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Domains: []string{"google.com"},
|
Domains: domain.List{"google.com"},
|
||||||
Primary: false,
|
Primary: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1004,7 +1005,7 @@ func TestHandlerChain_DomainPriorities(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tc.query, dns.TypeA)
|
r.SetQuestion(tc.query, dns.TypeA)
|
||||||
w := &ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &ResponseWriterChain{ResponseWriter: &test.MockResponseWriter{}}
|
||||||
|
|
||||||
if mh, ok := tc.expectedHandler.(*MockHandler); ok {
|
if mh, ok := tc.expectedHandler.(*MockHandler); ok {
|
||||||
mh.On("ServeDNS", mock.Anything, r).Once()
|
mh.On("ServeDNS", mock.Anything, r).Once()
|
||||||
@@ -1037,9 +1038,9 @@ type mockHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {}
|
func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {}
|
||||||
func (m *mockHandler) stop() {}
|
func (m *mockHandler) Stop() {}
|
||||||
func (m *mockHandler) probeAvailability() {}
|
func (m *mockHandler) ProbeAvailability() {}
|
||||||
func (m *mockHandler) id() handlerID { return handlerID(m.Id) }
|
func (m *mockHandler) ID() types.HandlerID { return types.HandlerID(m.Id) }
|
||||||
|
|
||||||
type mockService struct{}
|
type mockService struct{}
|
||||||
|
|
||||||
@@ -1113,7 +1114,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
initialHandlers registeredHandlerMap
|
initialHandlers registeredHandlerMap
|
||||||
updates []handlerWrapper
|
updates []handlerWrapper
|
||||||
expectedHandlers map[string]string // map[handlerID]domain
|
expectedHandlers map[string]domain.Domain // map[HandlerID]domain
|
||||||
description string
|
description string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1129,7 +1130,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityMatchDomain - 1,
|
priority: PriorityMatchDomain - 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-group2": "example.com",
|
"upstream-group2": "example.com",
|
||||||
},
|
},
|
||||||
description: "When group1 is not included in the update, it should be removed while group2 remains",
|
description: "When group1 is not included in the update, it should be removed while group2 remains",
|
||||||
@@ -1147,7 +1148,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-group1": "example.com",
|
"upstream-group1": "example.com",
|
||||||
},
|
},
|
||||||
description: "When group2 is not included in the update, it should be removed while group1 remains",
|
description: "When group2 is not included in the update, it should be removed while group1 remains",
|
||||||
@@ -1180,7 +1181,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityMatchDomain - 1,
|
priority: PriorityMatchDomain - 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-group1": "example.com",
|
"upstream-group1": "example.com",
|
||||||
"upstream-group2": "example.com",
|
"upstream-group2": "example.com",
|
||||||
"upstream-group3": "example.com",
|
"upstream-group3": "example.com",
|
||||||
@@ -1215,7 +1216,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityMatchDomain - 2,
|
priority: PriorityMatchDomain - 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-group1": "example.com",
|
"upstream-group1": "example.com",
|
||||||
"upstream-group2": "example.com",
|
"upstream-group2": "example.com",
|
||||||
"upstream-group3": "example.com",
|
"upstream-group3": "example.com",
|
||||||
@@ -1235,7 +1236,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityDefault - 1,
|
priority: PriorityDefault - 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-root2": ".",
|
"upstream-root2": ".",
|
||||||
},
|
},
|
||||||
description: "When root1 is not included in the update, it should be removed while root2 remains",
|
description: "When root1 is not included in the update, it should be removed while root2 remains",
|
||||||
@@ -1252,7 +1253,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityDefault,
|
priority: PriorityDefault,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-root1": ".",
|
"upstream-root1": ".",
|
||||||
},
|
},
|
||||||
description: "When root2 is not included in the update, it should be removed while root1 remains",
|
description: "When root2 is not included in the update, it should be removed while root1 remains",
|
||||||
@@ -1283,7 +1284,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityDefault - 1,
|
priority: PriorityDefault - 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-root1": ".",
|
"upstream-root1": ".",
|
||||||
"upstream-root2": ".",
|
"upstream-root2": ".",
|
||||||
"upstream-root3": ".",
|
"upstream-root3": ".",
|
||||||
@@ -1316,7 +1317,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityDefault - 2,
|
priority: PriorityDefault - 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-root1": ".",
|
"upstream-root1": ".",
|
||||||
"upstream-root2": ".",
|
"upstream-root2": ".",
|
||||||
"upstream-root3": ".",
|
"upstream-root3": ".",
|
||||||
@@ -1343,7 +1344,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-group1": "example.com",
|
"upstream-group1": "example.com",
|
||||||
"upstream-other": "other.com",
|
"upstream-other": "other.com",
|
||||||
},
|
},
|
||||||
@@ -1382,7 +1383,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedHandlers: map[string]string{
|
expectedHandlers: map[string]domain.Domain{
|
||||||
"upstream-group1": "example.com",
|
"upstream-group1": "example.com",
|
||||||
"upstream-group2": "example.com",
|
"upstream-group2": "example.com",
|
||||||
"upstream-other": "other.com",
|
"upstream-other": "other.com",
|
||||||
@@ -1409,7 +1410,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
|
|
||||||
// Check each expected handler
|
// Check each expected handler
|
||||||
for id, expectedDomain := range tt.expectedHandlers {
|
for id, expectedDomain := range tt.expectedHandlers {
|
||||||
handler, exists := server.dnsMuxMap[handlerID(id)]
|
handler, exists := server.dnsMuxMap[types.HandlerID(id)]
|
||||||
assert.True(t, exists, "Expected handler %s not found", id)
|
assert.True(t, exists, "Expected handler %s not found", id)
|
||||||
if exists {
|
if exists {
|
||||||
assert.Equal(t, expectedDomain, handler.domain,
|
assert.Equal(t, expectedDomain, handler.domain,
|
||||||
@@ -1418,9 +1419,9 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify no unexpected handlers exist
|
// Verify no unexpected handlers exist
|
||||||
for handlerID := range server.dnsMuxMap {
|
for HandlerID := range server.dnsMuxMap {
|
||||||
_, expected := tt.expectedHandlers[string(handlerID)]
|
_, expected := tt.expectedHandlers[string(HandlerID)]
|
||||||
assert.True(t, expected, "Unexpected handler found: %s", handlerID)
|
assert.True(t, expected, "Unexpected handler found: %s", HandlerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the handlerChain state and order
|
// Verify the handlerChain state and order
|
||||||
@@ -1438,7 +1439,7 @@ func TestDefaultServer_UpdateMux(t *testing.T) {
|
|||||||
for _, muxEntry := range server.dnsMuxMap {
|
for _, muxEntry := range server.dnsMuxMap {
|
||||||
if chainEntry.Handler == muxEntry.handler &&
|
if chainEntry.Handler == muxEntry.handler &&
|
||||||
chainEntry.Priority == muxEntry.priority &&
|
chainEntry.Priority == muxEntry.priority &&
|
||||||
chainEntry.Pattern == dns.Fqdn(muxEntry.domain) {
|
chainEntry.Pattern.PunycodeString() == dns.Fqdn(muxEntry.domain.PunycodeString()) {
|
||||||
foundInMux = true
|
foundInMux = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -1457,8 +1458,8 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
registerDomains []domain.List
|
registerDomains []domain.List
|
||||||
deregisterDomains []domain.List
|
deregisterDomains []domain.List
|
||||||
finalConfig nbdns.Config
|
finalConfig nbdns.Config
|
||||||
expectedDomains []string
|
expectedDomains domain.List
|
||||||
expectedMatchOnly []string
|
expectedMatchOnly domain.List
|
||||||
applyHostConfigCall int
|
applyHostConfigCall int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -1472,12 +1473,12 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
{Domain: "config.example.com"},
|
{Domain: "config.example.com"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"config.example.com.",
|
"config.example.com.",
|
||||||
"extra1.example.com.",
|
"extra1.example.com.",
|
||||||
"extra2.example.com.",
|
"extra2.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"extra1.example.com.",
|
"extra1.example.com.",
|
||||||
"extra2.example.com.",
|
"extra2.example.com.",
|
||||||
},
|
},
|
||||||
@@ -1494,12 +1495,12 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
registerDomains: []domain.List{
|
registerDomains: []domain.List{
|
||||||
{"extra1.example.com", "extra2.example.com"},
|
{"extra1.example.com", "extra2.example.com"},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"config.example.com.",
|
"config.example.com.",
|
||||||
"extra1.example.com.",
|
"extra1.example.com.",
|
||||||
"extra2.example.com.",
|
"extra2.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"extra1.example.com.",
|
"extra1.example.com.",
|
||||||
"extra2.example.com.",
|
"extra2.example.com.",
|
||||||
},
|
},
|
||||||
@@ -1517,12 +1518,12 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
registerDomains: []domain.List{
|
registerDomains: []domain.List{
|
||||||
{"extra.example.com", "overlap.example.com"},
|
{"extra.example.com", "overlap.example.com"},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"config.example.com.",
|
"config.example.com.",
|
||||||
"overlap.example.com.",
|
"overlap.example.com.",
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
},
|
},
|
||||||
applyHostConfigCall: 2,
|
applyHostConfigCall: 2,
|
||||||
@@ -1542,12 +1543,12 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
deregisterDomains: []domain.List{
|
deregisterDomains: []domain.List{
|
||||||
{"extra1.example.com", "extra3.example.com"},
|
{"extra1.example.com", "extra3.example.com"},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"config.example.com.",
|
"config.example.com.",
|
||||||
"extra2.example.com.",
|
"extra2.example.com.",
|
||||||
"extra4.example.com.",
|
"extra4.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"extra2.example.com.",
|
"extra2.example.com.",
|
||||||
"extra4.example.com.",
|
"extra4.example.com.",
|
||||||
},
|
},
|
||||||
@@ -1568,13 +1569,13 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
deregisterDomains: []domain.List{
|
deregisterDomains: []domain.List{
|
||||||
{"duplicate.example.com"},
|
{"duplicate.example.com"},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"config.example.com.",
|
"config.example.com.",
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
"other.example.com.",
|
"other.example.com.",
|
||||||
"duplicate.example.com.",
|
"duplicate.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
"other.example.com.",
|
"other.example.com.",
|
||||||
"duplicate.example.com.",
|
"duplicate.example.com.",
|
||||||
@@ -1599,13 +1600,13 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
{Domain: "newconfig.example.com"},
|
{Domain: "newconfig.example.com"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"config.example.com.",
|
"config.example.com.",
|
||||||
"newconfig.example.com.",
|
"newconfig.example.com.",
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
"duplicate.example.com.",
|
"duplicate.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
"duplicate.example.com.",
|
"duplicate.example.com.",
|
||||||
},
|
},
|
||||||
@@ -1626,12 +1627,12 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
deregisterDomains: []domain.List{
|
deregisterDomains: []domain.List{
|
||||||
{"protected.example.com"},
|
{"protected.example.com"},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
"config.example.com.",
|
"config.example.com.",
|
||||||
"protected.example.com.",
|
"protected.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
},
|
},
|
||||||
applyHostConfigCall: 3,
|
applyHostConfigCall: 3,
|
||||||
@@ -1642,7 +1643,7 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
ServiceEnable: true,
|
ServiceEnable: true,
|
||||||
NameServerGroups: []*nbdns.NameServerGroup{
|
NameServerGroups: []*nbdns.NameServerGroup{
|
||||||
{
|
{
|
||||||
Domains: []string{"ns.example.com", "overlap.ns.example.com"},
|
Domains: domain.List{"ns.example.com", "overlap.ns.example.com"},
|
||||||
NameServers: []nbdns.NameServer{
|
NameServers: []nbdns.NameServer{
|
||||||
{
|
{
|
||||||
IP: netip.MustParseAddr("8.8.8.8"),
|
IP: netip.MustParseAddr("8.8.8.8"),
|
||||||
@@ -1656,12 +1657,12 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
registerDomains: []domain.List{
|
registerDomains: []domain.List{
|
||||||
{"extra.example.com", "overlap.ns.example.com"},
|
{"extra.example.com", "overlap.ns.example.com"},
|
||||||
},
|
},
|
||||||
expectedDomains: []string{
|
expectedDomains: domain.List{
|
||||||
"ns.example.com.",
|
"ns.example.com.",
|
||||||
"overlap.ns.example.com.",
|
"overlap.ns.example.com.",
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
},
|
},
|
||||||
expectedMatchOnly: []string{
|
expectedMatchOnly: domain.List{
|
||||||
"ns.example.com.",
|
"ns.example.com.",
|
||||||
"overlap.ns.example.com.",
|
"overlap.ns.example.com.",
|
||||||
"extra.example.com.",
|
"extra.example.com.",
|
||||||
@@ -1696,7 +1697,7 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
wgInterface: &mocWGIface{},
|
wgInterface: &mocWGIface{},
|
||||||
hostManager: mockHostConfig,
|
hostManager: mockHostConfig,
|
||||||
localResolver: &localResolver{},
|
localResolver: &local.Resolver{},
|
||||||
service: mockSvc,
|
service: mockSvc,
|
||||||
statusRecorder: peer.NewRecorder("test"),
|
statusRecorder: peer.NewRecorder("test"),
|
||||||
extraDomains: make(map[domain.Domain]int),
|
extraDomains: make(map[domain.Domain]int),
|
||||||
@@ -1732,8 +1733,8 @@ func TestExtraDomains(t *testing.T) {
|
|||||||
lastConfig := capturedConfigs[len(capturedConfigs)-1]
|
lastConfig := capturedConfigs[len(capturedConfigs)-1]
|
||||||
|
|
||||||
// Check all expected domains are present
|
// Check all expected domains are present
|
||||||
domainMap := make(map[string]bool)
|
domainMap := make(map[domain.Domain]bool)
|
||||||
matchOnlyMap := make(map[string]bool)
|
matchOnlyMap := make(map[domain.Domain]bool)
|
||||||
|
|
||||||
for _, d := range lastConfig.Domains {
|
for _, d := range lastConfig.Domains {
|
||||||
domainMap[d.Domain] = true
|
domainMap[d.Domain] = true
|
||||||
@@ -1781,7 +1782,7 @@ func TestExtraDomainsRefCounting(t *testing.T) {
|
|||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
hostManager: mockHostConfig,
|
hostManager: mockHostConfig,
|
||||||
localResolver: &localResolver{},
|
localResolver: &local.Resolver{},
|
||||||
service: mockSvc,
|
service: mockSvc,
|
||||||
statusRecorder: peer.NewRecorder("test"),
|
statusRecorder: peer.NewRecorder("test"),
|
||||||
extraDomains: make(map[domain.Domain]int),
|
extraDomains: make(map[domain.Domain]int),
|
||||||
@@ -1833,7 +1834,7 @@ func TestUpdateConfigWithExistingExtraDomains(t *testing.T) {
|
|||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
hostManager: mockHostConfig,
|
hostManager: mockHostConfig,
|
||||||
localResolver: &localResolver{},
|
localResolver: &local.Resolver{},
|
||||||
service: mockSvc,
|
service: mockSvc,
|
||||||
statusRecorder: peer.NewRecorder("test"),
|
statusRecorder: peer.NewRecorder("test"),
|
||||||
extraDomains: make(map[domain.Domain]int),
|
extraDomains: make(map[domain.Domain]int),
|
||||||
@@ -1850,12 +1851,12 @@ func TestUpdateConfigWithExistingExtraDomains(t *testing.T) {
|
|||||||
err := server.applyConfiguration(initialConfig)
|
err := server.applyConfiguration(initialConfig)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var domains []string
|
var domains domain.List
|
||||||
for _, d := range capturedConfig.Domains {
|
for _, d := range capturedConfig.Domains {
|
||||||
domains = append(domains, d.Domain)
|
domains = append(domains, d.Domain)
|
||||||
}
|
}
|
||||||
assert.Contains(t, domains, "config.example.com.")
|
assert.Contains(t, domains, domain.Domain("config.example.com."))
|
||||||
assert.Contains(t, domains, "extra.example.com.")
|
assert.Contains(t, domains, domain.Domain("extra.example.com."))
|
||||||
|
|
||||||
// Now apply a new configuration with overlapping domain
|
// Now apply a new configuration with overlapping domain
|
||||||
updatedConfig := nbdns.Config{
|
updatedConfig := nbdns.Config{
|
||||||
@@ -1869,7 +1870,7 @@ func TestUpdateConfigWithExistingExtraDomains(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Verify both domains are in config, but no duplicates
|
// Verify both domains are in config, but no duplicates
|
||||||
domains = []string{}
|
domains = domain.List{}
|
||||||
matchOnlyCount := 0
|
matchOnlyCount := 0
|
||||||
for _, d := range capturedConfig.Domains {
|
for _, d := range capturedConfig.Domains {
|
||||||
domains = append(domains, d.Domain)
|
domains = append(domains, d.Domain)
|
||||||
@@ -1878,12 +1879,12 @@ func TestUpdateConfigWithExistingExtraDomains(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Contains(t, domains, "config.example.com.")
|
assert.Contains(t, domains, domain.Domain("config.example.com."))
|
||||||
assert.Contains(t, domains, "extra.example.com.")
|
assert.Contains(t, domains, domain.Domain("extra.example.com."))
|
||||||
assert.Equal(t, 2, len(domains), "Should have exactly 2 domains with no duplicates")
|
assert.Equal(t, 2, len(domains), "Should have exactly 2 domains with no duplicates")
|
||||||
|
|
||||||
// Extra domain should no longer be marked as match-only when in config
|
// Extra domain should no longer be marked as match-only when in config
|
||||||
matchOnlyDomain := ""
|
var matchOnlyDomain domain.Domain
|
||||||
for _, d := range capturedConfig.Domains {
|
for _, d := range capturedConfig.Domains {
|
||||||
if d.Domain == "extra.example.com." && d.MatchOnly {
|
if d.Domain == "extra.example.com." && d.MatchOnly {
|
||||||
matchOnlyDomain = d.Domain
|
matchOnlyDomain = d.Domain
|
||||||
@@ -1916,7 +1917,7 @@ func TestDomainCaseHandling(t *testing.T) {
|
|||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
hostManager: mockHostConfig,
|
hostManager: mockHostConfig,
|
||||||
localResolver: &localResolver{},
|
localResolver: &local.Resolver{},
|
||||||
service: mockSvc,
|
service: mockSvc,
|
||||||
statusRecorder: peer.NewRecorder("test"),
|
statusRecorder: peer.NewRecorder("test"),
|
||||||
extraDomains: make(map[domain.Domain]int),
|
extraDomains: make(map[domain.Domain]int),
|
||||||
@@ -1936,10 +1937,10 @@ func TestDomainCaseHandling(t *testing.T) {
|
|||||||
err := server.applyConfiguration(config)
|
err := server.applyConfiguration(config)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
var domains []string
|
var domains domain.List
|
||||||
for _, d := range capturedConfig.Domains {
|
for _, d := range capturedConfig.Domains {
|
||||||
domains = append(domains, d.Domain)
|
domains = append(domains, d.Domain)
|
||||||
}
|
}
|
||||||
assert.Contains(t, domains, "config.example.com.", "Mixed case domain should be normalized and pre.sent")
|
assert.Contains(t, domains, domain.Domain("config.example.com."), "Mixed case domain should be normalized and pre.sent")
|
||||||
assert.Contains(t, domains, "mixed.example.com.", "Mixed case domain should be normalized and present")
|
assert.Contains(t, domains, domain.Domain("mixed.example.com."), "Mixed case domain should be normalized and present")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,15 @@ type ServiceViaMemory struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
|
func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
|
||||||
|
lastIP, err := nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get last ip from network: %v", err)
|
||||||
|
}
|
||||||
s := &ServiceViaMemory{
|
s := &ServiceViaMemory{
|
||||||
wgInterface: wgIface,
|
wgInterface: wgIface,
|
||||||
dnsMux: dns.NewServeMux(),
|
dnsMux: dns.NewServeMux(),
|
||||||
|
|
||||||
runtimeIP: nbnet.GetLastIPFromNetwork(wgIface.Address().Network, 1).String(),
|
runtimeIP: lastIP.String(),
|
||||||
runtimePort: defaultPort,
|
runtimePort: defaultPort,
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
@@ -91,7 +95,7 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
firstLayerDecoder := layers.LayerTypeIPv4
|
firstLayerDecoder := layers.LayerTypeIPv4
|
||||||
if s.wgInterface.Address().Network.IP.To4() == nil {
|
if s.wgInterface.Address().IP.Is6() {
|
||||||
firstLayerDecoder = layers.LayerTypeIPv6
|
firstLayerDecoder = layers.LayerTypeIPv6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
nbnet "github.com/netbirdio/netbird/util/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetLastIPFromNetwork(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
addr string
|
|
||||||
ip string
|
|
||||||
}{
|
|
||||||
{"2001:db8::/32", "2001:db8:ffff:ffff:ffff:ffff:ffff:fffe"},
|
|
||||||
{"192.168.0.0/30", "192.168.0.2"},
|
|
||||||
{"192.168.0.0/16", "192.168.255.254"},
|
|
||||||
{"192.168.0.0/24", "192.168.0.254"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
_, ipnet, err := net.ParseCIDR(tt.addr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Error parsing CIDR: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastIP := nbnet.GetLastIPFromNetwork(ipnet, 1).String()
|
|
||||||
if lastIP != tt.ip {
|
|
||||||
t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -30,9 +30,12 @@ const (
|
|||||||
systemdDbusSetDNSMethodSuffix = systemdDbusLinkInterface + ".SetDNS"
|
systemdDbusSetDNSMethodSuffix = systemdDbusLinkInterface + ".SetDNS"
|
||||||
systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute"
|
systemdDbusSetDefaultRouteMethodSuffix = systemdDbusLinkInterface + ".SetDefaultRoute"
|
||||||
systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains"
|
systemdDbusSetDomainsMethodSuffix = systemdDbusLinkInterface + ".SetDomains"
|
||||||
|
systemdDbusSetDNSSECMethodSuffix = systemdDbusLinkInterface + ".SetDNSSEC"
|
||||||
systemdDbusResolvConfModeForeign = "foreign"
|
systemdDbusResolvConfModeForeign = "foreign"
|
||||||
|
|
||||||
dbusErrorUnknownObject = "org.freedesktop.DBus.Error.UnknownObject"
|
dbusErrorUnknownObject = "org.freedesktop.DBus.Error.UnknownObject"
|
||||||
|
|
||||||
|
dnsSecDisabled = "no"
|
||||||
)
|
)
|
||||||
|
|
||||||
type systemdDbusConfigurator struct {
|
type systemdDbusConfigurator struct {
|
||||||
@@ -95,9 +98,13 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
|
|||||||
Family: unix.AF_INET,
|
Family: unix.AF_INET,
|
||||||
Address: ipAs4[:],
|
Address: ipAs4[:],
|
||||||
}
|
}
|
||||||
err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput})
|
if err = s.callLinkMethod(systemdDbusSetDNSMethodSuffix, []systemdDbusDNSInput{defaultLinkInput}); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("set interface DNS server %s:%d: %w", config.ServerIP, config.ServerPort, err)
|
||||||
return fmt.Errorf("setting the interface DNS server %s:%d failed with error: %w", config.ServerIP, config.ServerPort, err)
|
}
|
||||||
|
|
||||||
|
// We don't support dnssec. On some machines this is default on so we explicitly set it to off
|
||||||
|
if err = s.callLinkMethod(systemdDbusSetDNSSECMethodSuffix, dnsSecDisabled); err != nil {
|
||||||
|
log.Warnf("failed to set DNSSEC to 'no': %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -110,15 +117,15 @@ func (s *systemdDbusConfigurator) applyDNSConfig(config HostDNSConfig, stateMana
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
|
domainsInput = append(domainsInput, systemdDbusLinkDomainsInput{
|
||||||
Domain: dConf.Domain,
|
Domain: dConf.Domain.PunycodeString(),
|
||||||
MatchOnly: dConf.MatchOnly,
|
MatchOnly: dConf.MatchOnly,
|
||||||
})
|
})
|
||||||
|
|
||||||
if dConf.MatchOnly {
|
if dConf.MatchOnly {
|
||||||
matchDomains = append(matchDomains, dConf.Domain)
|
matchDomains = append(matchDomains, dConf.Domain.PunycodeString())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
searchDomains = append(searchDomains, dConf.Domain)
|
searchDomains = append(searchDomains, dConf.Domain.PunycodeString())
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.RouteAll {
|
if config.RouteAll {
|
||||||
|
|||||||
26
client/internal/dns/test/mock.go
Normal file
26
client/internal/dns/test/mock.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockResponseWriter struct {
|
||||||
|
WriteMsgFunc func(m *dns.Msg) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *MockResponseWriter) WriteMsg(m *dns.Msg) error {
|
||||||
|
if rw.WriteMsgFunc != nil {
|
||||||
|
return rw.WriteMsgFunc(m)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rw *MockResponseWriter) LocalAddr() net.Addr { return nil }
|
||||||
|
func (rw *MockResponseWriter) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (rw *MockResponseWriter) Write([]byte) (int, error) { return 0, nil }
|
||||||
|
func (rw *MockResponseWriter) Close() error { return nil }
|
||||||
|
func (rw *MockResponseWriter) TsigStatus() error { return nil }
|
||||||
|
func (rw *MockResponseWriter) TsigTimersOnly(bool) {}
|
||||||
|
func (rw *MockResponseWriter) Hijack() {}
|
||||||
3
client/internal/dns/types/types.go
Normal file
3
client/internal/dns/types/types.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type HandlerID string
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user