Compare commits

..

89 Commits

Author SHA1 Message Date
Zoltan Papp
ca10541f50 Proposal for code cleaning 2023-04-11 17:06:55 +02:00
braginini
9b9f5fb64b Merge branch 'main' into feature/bind 2023-04-11 16:06:00 +02:00
Maycon Santos
3d3de0f1dd remove wintun.dll and use new sign-pipelines version 2023-04-11 15:27:45 +02:00
braginini
94c6d8c55b Reduce log level for kernel module check 2023-04-11 11:21:40 +02:00
braginini
0f449f64ec Fix NPE in windows tunnel 2023-04-10 11:34:30 +02:00
braginini
a7953cef58 Correct bind error handling 2023-04-10 11:16:01 +02:00
braginini
2bf846b183 Close UAPI in Windows tun 2023-04-10 11:13:40 +02:00
braginini
2a04175238 Fix windows tun GUID 2023-04-10 10:44:31 +02:00
Maycon Santos
84467c1531 test all and remove files 2023-04-09 01:11:48 +02:00
Maycon Santos
44b7afc06b use cmd 2023-04-09 00:58:15 +02:00
Maycon Santos
dea45633a6 store the output 2023-04-09 00:45:05 +02:00
Maycon Santos
14939202f0 add timeout and p 1 2023-04-09 00:17:04 +02:00
Maycon Santos
c5a8cc59a8 use id 2023-04-08 23:57:50 +02:00
Maycon Santos
3d3400ab43 use windwos latest 2023-04-08 23:54:14 +02:00
Maycon Santos
cdf5368d20 run with psexec 2023-04-08 23:52:12 +02:00
Maycon Santos
0803f16a49 debug update powershell.config.json 2023-04-08 22:20:21 +02:00
Maycon Santos
b6aafd8f09 debug update powershell.config.json 2023-04-08 22:19:11 +02:00
Maycon Santos
4c9fc9850c debug update powershell.config.json 2023-04-08 22:15:02 +02:00
Maycon Santos
0e9d5807d6 debug update powershell.config.json 2023-04-08 22:13:50 +02:00
Maycon Santos
b5a3248f9f debug remove powershell.config.json 2023-04-08 22:06:42 +02:00
Maycon Santos
5e1dfb28c0 debug 2023-04-08 21:25:33 +02:00
Maycon Santos
d1fe03a2d4 debug 2023-04-08 21:23:00 +02:00
Maycon Santos
bb453d334b remove DisableWinCompat file 2023-04-08 21:10:30 +02:00
Maycon Santos
56737bab6c remove DisableWinCompat file 2023-04-08 21:08:09 +02:00
Maycon Santos
37d20671a9 remove step 2023-04-08 20:58:39 +02:00
Maycon Santos
3f717eb759 Merge remote-tracking branch 'origin/feature/bind' into feature/bind 2023-04-08 20:51:51 +02:00
Maycon Santos
f9f8cbdcaa test windows 2019 2023-04-08 20:51:38 +02:00
braginini
a3849b978b Fix shoulduseproxy tests 2023-04-08 17:39:38 +02:00
braginini
1c071e4981 Revert "Only consider /16 network when using direct mode"
This reverts commit 189321f09d.
2023-04-08 17:38:13 +02:00
braginini
189321f09d Only consider /16 network when using direct mode 2023-04-08 17:37:48 +02:00
braginini
db5f931373 use direct mode only when in the same private /16 network 2023-04-08 17:22:53 +02:00
Maycon Santos
b556736b31 build cache 2023-04-08 13:29:47 +02:00
Maycon Santos
01e19a7c67 set unrestricted policy 2023-04-08 13:12:13 +02:00
Maycon Santos
9494cbdf24 import PSScheduledJob 2023-04-08 13:04:16 +02:00
Maycon Santos
9c09b13a25 import module in the same step 2023-04-08 12:56:23 +02:00
Maycon Santos
8bb999cf2a using https://github.com/mkellerman/Invoke-CommandAs 2023-04-08 12:53:03 +02:00
Maycon Santos
565b8ce1c7 use paexec and use always to build cache 2023-04-08 11:25:57 +02:00
Maycon Santos
e96a975737 extension 2023-04-08 11:14:48 +02:00
Maycon Santos
8585e3ccf7 test moving the bin to temp folder 2023-04-08 11:09:21 +02:00
Maycon Santos
cd002c6400 building a test bin 2023-04-08 11:03:31 +02:00
Maycon Santos
0629697db1 use sysinternals choco package 2023-04-08 10:59:29 +02:00
Maycon Santos
25a92a0052 check other command 2023-04-08 00:48:55 +02:00
Maycon Santos
9c51d85cb4 copy acl 2023-04-07 19:25:21 +02:00
Maycon Santos
9865179207 get acl 2023-04-07 19:17:42 +02:00
Maycon Santos
56f10085f4 dir out 2023-04-07 19:06:28 +02:00
Maycon Santos
a7574907ae use powershell command 2023-04-07 19:04:25 +02:00
Maycon Santos
71e81533bc list workspace dir 2023-04-07 18:45:52 +02:00
Maycon Santos
23b92e2615 add concurrency setting to workflows
it helps minimize github action views and stop outdated jobs
2023-04-07 18:41:05 +02:00
Maycon Santos
9158a4653a use workspace path 2023-04-07 18:28:22 +02:00
Maycon Santos
ccbf749171 psexec accept eula 2023-04-07 18:24:30 +02:00
Maycon Santos
dea7e8d4e7 download wintun and psexec
use rsrc tool to generate syso files
2023-04-07 18:20:08 +02:00
braginini
a0441e7d04 Commit unused code 2023-04-07 16:58:26 +02:00
braginini
9702946474 move dll to 2023-04-07 16:42:01 +02:00
braginini
e262f3536e move dll to 2023-04-07 16:34:26 +02:00
braginini
addfed3af0 upload artifact to root folder 2023-04-07 16:21:50 +02:00
braginini
bf723ec66f remove lazy dll load 2023-04-07 16:13:50 +02:00
braginini
10afc8eeb8 Try workflow with DLL 2023-04-07 16:04:02 +02:00
braginini
0b21e05a52 Revert "disable workflows"
This reverts commit 94c646f1e5.
2023-04-07 15:39:01 +02:00
braginini
94c646f1e5 disable workflows 2023-04-07 15:37:56 +02:00
braginini
4f7d34c5c7 try fixing ci/cd 2023-04-07 15:37:03 +02:00
braginini
0455e574b8 Add license comment for the copied dll code 2023-04-07 15:15:34 +02:00
braginini
965ba8837f Fix tun address assignment on windows 2023-04-07 15:11:21 +02:00
braginini
61146a51d0 Load wintun.dll 2023-04-07 14:21:54 +02:00
braginini
4f8a156cb2 Load wintun lib 2023-04-06 18:54:06 +02:00
braginini
ff0b395fc5 Trying to fix wintun.dll 2023-04-06 18:45:44 +02:00
braginini
237bfde1f2 Try loading wintun.dll 2023-04-06 16:54:57 +02:00
braginini
bfff0c36aa Fix windows tun device 2023-04-06 11:34:19 +02:00
braginini
41458a09e9 Use Bind implementation on Windows 2023-04-06 11:01:51 +02:00
Zoltan Papp
abd8287da8 Separate discover methods (#787) 2023-04-06 10:39:23 +02:00
Zoltan Papp
d3c49c71f2 Fix config reference in stdnet_android 2023-04-05 21:23:13 +02:00
braginini
49030ab71e Merge branch 'main' into feature/bind 2023-04-05 18:38:41 +02:00
braginini
7548780f8f Fix docker test workflow 2023-04-05 18:07:38 +02:00
braginini
277b65b833 Fix Windows compilation errros 2023-04-05 18:00:44 +02:00
braginini
071ad2b993 Fix Codacy and lint issues 2023-04-05 17:57:18 +02:00
braginini
0e8a552334 Fix Codacy 2023-04-05 16:54:50 +02:00
braginini
005c4dd44a Make usage of WireGuard kernel configurable from env 2023-04-05 16:30:52 +02:00
braginini
367eff493a Ignore loopback interface 2023-04-04 17:49:28 +02:00
braginini
73a5bc33b3 Use bind no proxy when possible 2023-04-04 17:40:25 +02:00
braginini
87cbff1e7a Glue up all together 2023-04-04 16:50:35 +02:00
braginini
24cc5c4ef2 Rename WireGuard proxy 2023-04-04 14:52:57 +02:00
braginini
a42f7d2c3b Fix iface test 2023-04-03 18:34:28 +02:00
braginini
e2a3fc7558 Fix engine tests 2023-04-03 18:27:33 +02:00
braginini
5d191a8b9d Use custom stdnet 2023-04-03 17:37:05 +02:00
braginini
79a8109d5e Extend stdnet to have an interface filter 2023-03-31 18:25:23 +02:00
braginini
ea44c1b723 Distinguish UDPMux and UDPMuxUniversal 2023-03-30 15:13:14 +02:00
braginini
430f92094e Upgrade WireGuard 2023-03-29 13:58:42 +02:00
braginini
d6c2b46019 Make it work 2023-03-27 19:09:01 +02:00
braginini
2e7d199a6d Merge remote-tracking branch 'origin/main' into feature/bind 2023-03-27 16:01:13 +02:00
braginini
8ac7eaf833 Bind init 2023-03-27 14:57:47 +02:00
233 changed files with 6213 additions and 17210 deletions

View File

@@ -17,7 +17,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: "1.20.x"
go-version: 1.19.x
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -20,7 +20,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: "1.20.x"
go-version: 1.19.x
- name: Cache Go modules
@@ -49,7 +49,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: "1.20.x"
go-version: 1.19.x
- name: Cache Go modules
@@ -70,40 +70,27 @@ jobs:
run: go mod tidy
- name: Generate Iface Test bin
run: CGO_ENABLED=0 go test -c -o iface-testing.bin ./iface/
- name: Generate Shared Sock Test bin
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
run: go test -c -o iface-testing.bin ./iface/
- name: Generate RouteManager Test bin
run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
- name: Generate nftables Manager Test bin
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
run: go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
- name: Generate Engine Test bin
run: CGO_ENABLED=0 go test -c -o engine-testing.bin ./client/internal
run: 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: 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 /busybox/sh gcr.io/distroless/base:debug -c /ci/sharedsock-testing.bin -test.timeout 5m -test.parallel 1
- name: Run Iface tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/iface --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/iface-testing.bin -test.timeout 5m -test.parallel 1
- 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 /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -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 /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1
- name: Run Engine tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -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 /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1

View File

@@ -23,7 +23,7 @@ jobs:
uses: actions/setup-go@v4
id: go
with:
go-version: "1.20.x"
go-version: 1.19.x
- name: Download wintun
uses: carlosperate/download-file-action@v2

View File

@@ -12,7 +12,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: "1.20.x"
go-version: 1.19.x
- name: Install dependencies
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
- name: golangci-lint

View File

@@ -9,7 +9,7 @@ on:
pull_request:
env:
SIGN_PIPE_VER: "v0.0.8"
SIGN_PIPE_VER: "v0.0.6"
GORELEASER_VER: "v1.14.1"
concurrency:
@@ -29,7 +29,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: 1.19
-
name: Cache Go modules
uses: actions/cache@v1
@@ -100,7 +100,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: 1.19
- name: Cache Go modules
uses: actions/cache@v1
with:
@@ -150,7 +150,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: "1.20"
go-version: 1.19
-
name: Cache Go modules
uses: actions/cache@v1
@@ -195,7 +195,7 @@ jobs:
trigger_darwin_signer:
runs-on: ubuntu-latest
needs: [release,release_ui_darwin]
needs: release_ui_darwin
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Trigger Darwin App binaries sign pipeline
@@ -205,4 +205,4 @@ jobs:
repo: netbirdio/sign-pipelines
ref: ${{ env.SIGN_PIPE_VER }}
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'
inputs: '{ "tag": "${{ github.ref }}" }'

View File

@@ -6,7 +6,6 @@ on:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
@@ -24,7 +23,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: "1.20.x"
go-version: 1.19.x
- name: Cache Go modules
uses: actions/cache@v2
@@ -46,20 +45,15 @@ jobs:
env:
CI_NETBIRD_DOMAIN: localhost
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
CI_NETBIRD_AUTH_CLIENT_SECRET: testing.client.secret
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
CI_NETBIRD_USE_AUTH0: true
CI_NETBIRD_MGMT_IDP: "none"
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
- name: check values
working-directory: infrastructure_files
env:
CI_NETBIRD_DOMAIN: localhost
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
CI_NETBIRD_AUTH_CLIENT_SECRET: testing.client.secret
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
CI_NETBIRD_USE_AUTH0: true
@@ -73,13 +67,9 @@ jobs:
CI_NETBIRD_AUTH_USER_ID_CLAIM: "email"
CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE: "super"
CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE: "openid email"
CI_NETBIRD_MGMT_IDP: "none"
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
run: |
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
grep AUTH_CLIENT_SECRET docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_SECRET
grep AUTH_AUTHORITY docker-compose.yml | grep $CI_NETBIRD_AUTH_AUTHORITY
grep AUTH_AUDIENCE docker-compose.yml | grep $CI_NETBIRD_AUTH_AUDIENCE
grep AUTH_SUPPORTED_SCOPES docker-compose.yml | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
@@ -93,16 +83,10 @@ jobs:
grep -A 1 ProviderConfig management.json | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
grep Scope management.json | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE"
grep UseIDToken management.json | grep false
grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP
grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY
grep -A 4 IdpManagerConfig management.json | grep -A 2 ClientConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
grep -A 5 IdpManagerConfig management.json | grep -A 3 ClientConfig | grep ClientID | grep $CI_NETBIRD_IDP_MGMT_CLIENT_ID
grep -A 6 IdpManagerConfig management.json | grep -A 4 ClientConfig | grep ClientSecret | grep $CI_NETBIRD_IDP_MGMT_CLIENT_SECRET
grep -A 7 IdpManagerConfig management.json | grep -A 5 ClientConfig | grep GrantType | grep client_credentials
- name: run docker compose up
working-directory: infrastructure_files
run: |
run: |
docker-compose up -d
sleep 5
docker-compose ps

View File

@@ -1,22 +0,0 @@
name: update docs
on:
push:
tags:
- 'v*'
paths:
- 'management/server/http/api/openapi.yml'
jobs:
trigger_docs_api_update:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Trigger API pages generation
uses: benc-uk/workflow-dispatch@v1
with:
workflow: generate api pages
repo: netbirdio/docs
ref: "refs/heads/main"
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
inputs: '{ "tag": "${{ github.ref }}" }'

7
.gitignore vendored
View File

@@ -7,15 +7,8 @@ bin/
conf.json
http-cmds.sh
infrastructure_files/management.json
infrastructure_files/management-*.json
infrastructure_files/docker-compose.yml
infrastructure_files/openid-configuration.json
infrastructure_files/turnserver.conf
management/management
client/client
client/client.exe
*.syso
client/.distfiles/
infrastructure_files/setup.env
infrastructure_files/setup-*.env
.vscode

View File

@@ -12,7 +12,11 @@ builds:
- arm
- amd64
- arm64
- mips
- 386
gomips:
- hardfloat
- softfloat
ignore:
- goos: windows
goarch: arm64
@@ -26,26 +30,6 @@ builds:
tags:
- load_wgnt_from_rsrc
- id: netbird-static
dir: client
binary: netbird
env: [CGO_ENABLED=0]
goos:
- linux
goarch:
- mips
- mipsle
- mips64
- mips64le
gomips:
- hardfloat
- softfloat
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 }}'
tags:
- load_wgnt_from_rsrc
- id: netbird-mgmt
dir: management
env:
@@ -83,7 +67,6 @@ builds:
archives:
- builds:
- netbird
- netbird-static
nfpms:
@@ -376,4 +359,4 @@ uploads:
mode: archive
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
username: dev@wiretrustee.com
method: PUT
method: PUT

View File

@@ -1,59 +0,0 @@
package base62
import (
"fmt"
"math"
"strings"
)
const (
alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
base = uint32(len(alphabet))
)
// Encode encodes a uint32 value to a base62 string.
func Encode(num uint32) string {
if num == 0 {
return string(alphabet[0])
}
var encoded strings.Builder
remainder := uint32(0)
for num > 0 {
remainder = num % base
encoded.WriteByte(alphabet[remainder])
num /= base
}
// Reverse the encoded string
encodedString := encoded.String()
reversed := reverse(encodedString)
return reversed
}
// Decode decodes a base62 string to a uint32 value.
func Decode(encoded string) (uint32, error) {
var decoded uint32
strLen := len(encoded)
for i, char := range encoded {
index := strings.IndexRune(alphabet, char)
if index < 0 {
return 0, fmt.Errorf("invalid character: %c", char)
}
decoded += uint32(index) * uint32(math.Pow(float64(base), float64(strLen-i-1)))
}
return decoded, nil
}
// Reverse a string.
func reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}

View File

@@ -1,31 +0,0 @@
package base62
import (
"testing"
)
func TestEncodeDecode(t *testing.T) {
tests := []struct {
num uint32
}{
{0},
{1},
{42},
{12345},
{99999},
{123456789},
}
for _, tt := range tests {
encoded := Encode(tt.num)
decoded, err := Decode(encoded)
if err != nil {
t.Errorf("Decode error: %v", err)
}
if decoded != tt.num {
t.Errorf("Decode(%v) = %v, want %v", encoded, decoded, tt.num)
}
}
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/formatter"
@@ -30,11 +29,6 @@ type IFaceDiscover interface {
stdnet.ExternalIFaceDiscover
}
// RouteListener export internal RouteListener for mobile
type RouteListener interface {
routemanager.RouteListener
}
func init() {
formatter.SetLogcatFormatter(log.StandardLogger())
}
@@ -48,11 +42,13 @@ type Client struct {
ctxCancel context.CancelFunc
ctxCancelLock *sync.Mutex
deviceName string
routeListener routemanager.RouteListener
}
// NewClient instantiate a new Client
func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, routeListener RouteListener) *Client {
func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover) *Client {
lvl, _ := log.ParseLevel("trace")
log.SetLevel(lvl)
return &Client{
cfgFile: cfgFile,
deviceName: deviceName,
@@ -60,7 +56,6 @@ func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover
iFaceDiscover: iFaceDiscover,
recorder: peer.NewRecorder(""),
ctxCancelLock: &sync.Mutex{},
routeListener: routeListener,
}
}
@@ -90,7 +85,7 @@ func (c *Client) Run(urlOpener URLOpener) error {
// todo do not throw error in case of cancelled context
ctx = internal.CtxInitState(ctx)
return internal.RunClient(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener)
return internal.RunClient(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover)
}
// Stop the internal client and free the resources
@@ -104,11 +99,6 @@ func (c *Client) Stop() {
c.ctxCancel()
}
// SetTraceLogLevel configure the logger to trace level
func (c *Client) SetTraceLogLevel() {
log.SetLevel(log.TraceLevel)
}
// PeersList return with the list of the PeerInfos
func (c *Client) PeersList() *PeerInfoArray {
@@ -120,9 +110,11 @@ func (c *Client) PeersList() *PeerInfoArray {
p.IP,
p.FQDN,
p.ConnStatus.String(),
p.Direct,
}
peerInfos[n] = pi
}
return &PeerInfoArray{items: peerInfos}
}

View File

@@ -1,5 +0,0 @@
package android
import _ "golang.org/x/mobile/bind"
// to keep our CI/CD that checks go.mod and go.sum files happy, we need to import the package above

View File

@@ -5,6 +5,7 @@ type PeerInfo struct {
IP string
FQDN string
ConnStatus string // Todo replace to enum
Direct bool
}
// PeerInfoCollection made for Java layer to get non default types as collection

View File

@@ -3,7 +3,6 @@ package cmd
import (
"context"
"fmt"
"strings"
"time"
"github.com/skratchdot/open-golang/open"
@@ -16,7 +15,6 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system"
)
var loginCmd = &cobra.Command{
@@ -34,11 +32,6 @@ var loginCmd = &cobra.Command{
ctx := internal.CtxInitState(context.Background())
if hostName != "" {
// nolint
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
}
// workaround to run without service
if logFile == "console" {
err = handleRebrand(cmd)
@@ -46,16 +39,12 @@ var loginCmd = &cobra.Command{
return err
}
ic := internal.ConfigInput{
config, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
}
if preSharedKey != "" {
ic.PreSharedKey = &preSharedKey
}
config, err := internal.UpdateOrCreateConfig(ic)
PreSharedKey: &preSharedKey,
})
if err != nil {
return fmt.Errorf("get config file: %v", err)
}
@@ -111,7 +100,7 @@ var loginCmd = &cobra.Command{
}
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode)
openURL(cmd, loginResp.VerificationURIComplete)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
if err != nil {
@@ -190,7 +179,7 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
return nil, fmt.Errorf("getting a request device code failed: %v", err)
}
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode)
openURL(cmd, flowInfo.VerificationURIComplete)
waitTimeout := time.Duration(flowInfo.ExpiresIn)
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
@@ -204,16 +193,11 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
return &tokenInfo, nil
}
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
var codeMsg string
if !strings.Contains(verificationURIComplete, userCode) {
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
}
func openURL(cmd *cobra.Command, verificationURIComplete string) {
err := open.Run(verificationURIComplete)
cmd.Printf("Please do the SSO login in your browser. \n" +
"If your browser didn't open automatically, use this URL to log in:\n\n" +
" " + verificationURIComplete + " " + codeMsg + " \n\n")
" " + verificationURIComplete + " \n\n")
if err != nil {
cmd.Printf("Alternatively, you may want to use a setup key, see:\n\n https://www.netbird.io/docs/overview/setup-keys\n")
}

View File

@@ -45,7 +45,6 @@ var (
managementURL string
adminURL string
setupKey string
hostName string
preSharedKey string
natExternalIPs []string
customDNSAddress string
@@ -95,7 +94,6 @@ func init() {
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the the log will be output to stdout")
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
rootCmd.PersistentFlags().StringVar(&preSharedKey, "preshared-key", "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
rootCmd.AddCommand(serviceCmd)
rootCmd.AddCommand(upCmd)
rootCmd.AddCommand(downCmd)

View File

@@ -249,7 +249,7 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
IP: pbPeerState.GetIP(),
PubKey: pbPeerState.GetPubKey(),
Status: pbPeerState.GetConnStatus(),
LastStatusUpdate: timeLocal,
LastStatusUpdate: timeLocal.UTC(),
ConnType: connType,
Direct: pbPeerState.GetDirect(),
IceCandidateType: iceCandidateType{

View File

@@ -11,15 +11,6 @@ import (
"github.com/netbirdio/netbird/version"
)
func init() {
loc, err := time.LoadLocation("UTC")
if err != nil {
panic(err)
}
time.Local = loc
}
var resp = &proto.StatusResponse{
Status: "Connected",
FullStatus: &proto.FullStatus{

View File

@@ -2,23 +2,21 @@ package cmd
import (
"context"
"github.com/netbirdio/netbird/management/server/activity"
"net"
"path/filepath"
"testing"
"time"
"github.com/netbirdio/netbird/management/server/activity"
"github.com/netbirdio/netbird/util"
"google.golang.org/grpc"
clientProto "github.com/netbirdio/netbird/client/proto"
client "github.com/netbirdio/netbird/client/server"
mgmtProto "github.com/netbirdio/netbird/management/proto"
mgmt "github.com/netbirdio/netbird/management/server"
sigProto "github.com/netbirdio/netbird/signal/proto"
sig "github.com/netbirdio/netbird/signal/server"
"google.golang.org/grpc"
)
func startTestingServices(t *testing.T) string {
@@ -65,7 +63,7 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
t.Fatal(err)
}
s := grpc.NewServer()
store, err := mgmt.NewFileStore(config.Datadir, nil)
store, err := mgmt.NewFileStore(config.Datadir)
if err != nil {
t.Fatal(err)
}

View File

@@ -15,7 +15,6 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/proto"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/util"
)
@@ -56,11 +55,6 @@ func upFunc(cmd *cobra.Command, args []string) error {
ctx := internal.CtxInitState(cmd.Context())
if hostName != "" {
// nolint
ctx = context.WithValue(ctx, system.DeviceNameCtxKey, hostName)
}
if foregroundMode {
return runInForegroundMode(ctx, cmd)
}
@@ -78,18 +72,14 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
return err
}
ic := internal.ConfigInput{
config, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
ManagementURL: managementURL,
AdminURL: adminURL,
ConfigPath: configPath,
PreSharedKey: &preSharedKey,
NATExternalIPs: natExternalIPs,
CustomDNSAddress: customDNSAddressConverted,
}
if preSharedKey != "" {
ic.PreSharedKey = &preSharedKey
}
config, err := internal.UpdateOrCreateConfig(ic)
})
if err != nil {
return fmt.Errorf("get config file: %v", err)
}
@@ -104,7 +94,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
var cancel context.CancelFunc
ctx, cancel = context.WithCancel(ctx)
SetupCloseHandler(ctx, cancel)
return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()), nil, nil, nil)
return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()), nil, nil)
}
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
@@ -176,7 +166,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
if loginResp.NeedsSSOLogin {
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode)
openURL(cmd, loginResp.VerificationURIComplete)
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
if err != nil {

View File

@@ -13,24 +13,22 @@ type Rule interface {
GetRuleID() string
}
// RuleDirection is the traffic direction which a rule is applied
type RuleDirection int
// Direction is the direction of the traffic
type Direction int
const (
// RuleDirectionIN applies to filters that handlers incoming traffic
RuleDirectionIN RuleDirection = iota
// RuleDirectionOUT applies to filters that handlers outgoing traffic
RuleDirectionOUT
// DirectionSrc is the direction of the traffic from the source
DirectionSrc Direction = iota
// DirectionDst is the direction of the traffic from the destination
DirectionDst
)
// Action is the action to be taken on a rule
type Action int
const (
// ActionUnknown is a unknown action
ActionUnknown Action = iota
// ActionAccept is the action to accept a packet
ActionAccept
ActionAccept Action = iota
// ActionDrop is the action to drop a packet
ActionDrop
)
@@ -41,15 +39,10 @@ const (
// Netbird client for ACL and routing functionality
type Manager interface {
// AddFiltering rule to the firewall
//
// If comment argument is empty firewall manager should set
// rule ID as comment for the rule
AddFiltering(
ip net.IP,
proto Protocol,
sPort *Port,
dPort *Port,
direction RuleDirection,
port *Port,
direction Direction,
action Action,
comment string,
) (Rule, error)

View File

@@ -8,50 +8,26 @@ import (
"github.com/coreos/go-iptables/iptables"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
fw "github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/iface"
)
const (
// ChainInputFilterName is the name of the chain that is used for filtering incoming packets
ChainInputFilterName = "NETBIRD-ACL-INPUT"
// ChainOutputFilterName is the name of the chain that is used for filtering outgoing packets
ChainOutputFilterName = "NETBIRD-ACL-OUTPUT"
// ChainFilterName is the name of the chain that is used for filtering by the Netbird client
ChainFilterName = "NETBIRD-ACL"
)
// dropAllDefaultRule in the Netbird chain
var dropAllDefaultRule = []string{"-j", "DROP"}
// Manager of iptables firewall
type Manager struct {
mutex sync.Mutex
ipv4Client *iptables.IPTables
ipv6Client *iptables.IPTables
inputDefaultRuleSpecs []string
outputDefaultRuleSpecs []string
wgIface iFaceMapper
}
// iFaceMapper defines subset methods of interface required for manager
type iFaceMapper interface {
Name() string
Address() iface.WGAddress
}
// Create iptables firewall manager
func Create(wgIface iFaceMapper) (*Manager, error) {
m := &Manager{
wgIface: wgIface,
inputDefaultRuleSpecs: []string{
"-i", wgIface.Name(), "-j", ChainInputFilterName, "-s", wgIface.Address().String()},
outputDefaultRuleSpecs: []string{
"-o", wgIface.Name(), "-j", ChainOutputFilterName, "-d", wgIface.Address().String()},
}
func Create() (*Manager, error) {
m := &Manager{}
// init clients for booth ipv4 and ipv6
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
@@ -62,271 +38,118 @@ func Create(wgIface iFaceMapper) (*Manager, error) {
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
log.Errorf("ip6tables is not installed in the system or not supported: %v", err)
} else {
m.ipv6Client = ipv6Client
return nil, fmt.Errorf("ip6tables is not installed in the system or not supported")
}
m.ipv6Client = ipv6Client
if err := m.Reset(); err != nil {
return nil, fmt.Errorf("failed to reset firewall: %v", err)
return nil, fmt.Errorf("failed to reset firewall: %s", err)
}
return m, nil
}
// AddFiltering rule to the firewall
//
// If comment is empty rule ID is used as comment
func (m *Manager) AddFiltering(
ip net.IP,
protocol fw.Protocol,
sPort *fw.Port,
dPort *fw.Port,
direction fw.RuleDirection,
port *fw.Port,
direction fw.Direction,
action fw.Action,
comment string,
) (fw.Rule, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
client, err := m.client(ip)
client := m.client(ip)
ok, err := client.ChainExists("filter", ChainFilterName)
if err != nil {
return nil, fmt.Errorf("failed to check if chain exists: %s", err)
}
if !ok {
if err := client.NewChain("filter", ChainFilterName); err != nil {
return nil, fmt.Errorf("failed to create chain: %s", err)
}
}
if port == nil || port.Values == nil || (port.IsRange && len(port.Values) != 2) {
return nil, fmt.Errorf("invalid port definition")
}
pv := strconv.Itoa(port.Values[0])
if port.IsRange {
pv += ":" + strconv.Itoa(port.Values[1])
}
specs := m.filterRuleSpecs("filter", ChainFilterName, ip, pv, direction, action, comment)
if err := client.AppendUnique("filter", ChainFilterName, specs...); err != nil {
return nil, err
}
var dPortVal, sPortVal string
if dPort != nil && dPort.Values != nil {
// TODO: we support only one port per rule in current implementation of ACLs
dPortVal = strconv.Itoa(dPort.Values[0])
}
if sPort != nil && sPort.Values != nil {
sPortVal = strconv.Itoa(sPort.Values[0])
}
ruleID := uuid.New().String()
if comment == "" {
comment = ruleID
}
specs := m.filterRuleSpecs(
"filter",
ip,
string(protocol),
sPortVal,
dPortVal,
direction,
action,
comment,
)
if direction == fw.RuleDirectionOUT {
ok, err := client.Exists("filter", ChainOutputFilterName, specs...)
if err != nil {
return nil, fmt.Errorf("check is output rule already exists: %w", err)
}
if ok {
return nil, fmt.Errorf("input rule already exists")
}
if err := client.Insert("filter", ChainOutputFilterName, 1, specs...); err != nil {
return nil, err
}
} else {
ok, err := client.Exists("filter", ChainInputFilterName, specs...)
if err != nil {
return nil, fmt.Errorf("check is input rule already exists: %w", err)
}
if ok {
return nil, fmt.Errorf("input rule already exists")
}
if err := client.Insert("filter", ChainInputFilterName, 1, specs...); err != nil {
return nil, err
}
}
return &Rule{
id: ruleID,
rule := &Rule{
id: uuid.New().String(),
specs: specs,
dst: direction == fw.RuleDirectionOUT,
v6: ip.To4() == nil,
}, nil
}
return rule, nil
}
// DeleteRule from the firewall by rule definition
func (m *Manager) DeleteRule(rule fw.Rule) error {
m.mutex.Lock()
defer m.mutex.Unlock()
r, ok := rule.(*Rule)
if !ok {
return fmt.Errorf("invalid rule type")
}
client := m.ipv4Client
if r.v6 {
if m.ipv6Client == nil {
return fmt.Errorf("ipv6 is not supported")
}
client = m.ipv6Client
}
if r.dst {
return client.Delete("filter", ChainOutputFilterName, r.specs...)
}
return client.Delete("filter", ChainInputFilterName, r.specs...)
return client.Delete("filter", ChainFilterName, r.specs...)
}
// Reset firewall to the default state
func (m *Manager) Reset() error {
m.mutex.Lock()
defer m.mutex.Unlock()
if err := m.reset(m.ipv4Client, "filter"); err != nil {
return fmt.Errorf("clean ipv4 firewall ACL input chain: %w", err)
if err := m.reset(m.ipv4Client, "filter", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv4 firewall ACL chain: %w", err)
}
if m.ipv6Client != nil {
if err := m.reset(m.ipv6Client, "filter"); err != nil {
return fmt.Errorf("clean ipv6 firewall ACL input chain: %w", err)
}
if err := m.reset(m.ipv6Client, "filter", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv6 firewall ACL chain: %w", err)
}
return nil
}
// reset firewall chain, clear it and drop it
func (m *Manager) reset(client *iptables.IPTables, table string) error {
ok, err := client.ChainExists(table, ChainInputFilterName)
func (m *Manager) reset(client *iptables.IPTables, table, chain string) error {
ok, err := client.ChainExists(table, chain)
if err != nil {
return fmt.Errorf("failed to check if input chain exists: %w", err)
return fmt.Errorf("failed to check if chain exists: %w", err)
}
if ok {
if ok, err := client.Exists("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
return err
} else if ok {
if err := client.Delete("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
log.WithError(err).Errorf("failed to delete default input rule: %v", err)
}
}
}
ok, err = client.ChainExists(table, ChainOutputFilterName)
if err != nil {
return fmt.Errorf("failed to check if output chain exists: %w", err)
}
if ok {
if ok, err := client.Exists("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
return err
} else if ok {
if err := client.Delete("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
log.WithError(err).Errorf("failed to delete default output rule: %v", err)
}
}
}
if err := client.ClearAndDeleteChain(table, ChainInputFilterName); err != nil {
log.Errorf("failed to clear and delete input chain: %v", err)
if !ok {
return nil
}
if err := client.ClearAndDeleteChain(table, ChainOutputFilterName); err != nil {
log.Errorf("failed to clear and delete input chain: %v", err)
return nil
if err := client.ClearChain(table, ChainFilterName); err != nil {
return fmt.Errorf("failed to clear chain: %w", err)
}
return nil
return client.DeleteChain(table, ChainFilterName)
}
// filterRuleSpecs returns the specs of a filtering rule
func (m *Manager) filterRuleSpecs(
table string, ip net.IP, protocol string, sPort, dPort string,
direction fw.RuleDirection, action fw.Action, comment string,
table string, chain string, ip net.IP, port string,
direction fw.Direction, action fw.Action, comment string,
) (specs []string) {
matchByIP := true
// don't use IP matching if IP is ip 0.0.0.0
if s := ip.String(); s == "0.0.0.0" || s == "::" {
matchByIP = false
}
switch direction {
case fw.RuleDirectionIN:
if matchByIP {
specs = append(specs, "-s", ip.String())
}
case fw.RuleDirectionOUT:
if matchByIP {
specs = append(specs, "-d", ip.String())
}
}
if protocol != "all" {
specs = append(specs, "-p", protocol)
}
if sPort != "" {
specs = append(specs, "--sport", sPort)
}
if dPort != "" {
specs = append(specs, "--dport", dPort)
if direction == fw.DirectionSrc {
specs = append(specs, "-s", ip.String())
}
specs = append(specs, "-p", "tcp", "--dport", port)
specs = append(specs, "-j", m.actionToStr(action))
return append(specs, "-m", "comment", "--comment", comment)
}
// rawClient returns corresponding iptables client for the given ip
func (m *Manager) rawClient(ip net.IP) (*iptables.IPTables, error) {
// client returns corresponding iptables client for the given ip
func (m *Manager) client(ip net.IP) *iptables.IPTables {
if ip.To4() != nil {
return m.ipv4Client, nil
return m.ipv4Client
}
if m.ipv6Client == nil {
return nil, fmt.Errorf("ipv6 is not supported")
}
return m.ipv6Client, nil
}
// client returns client with initialized chain and default rules
func (m *Manager) client(ip net.IP) (*iptables.IPTables, error) {
client, err := m.rawClient(ip)
if err != nil {
return nil, err
}
ok, err := client.ChainExists("filter", ChainInputFilterName)
if err != nil {
return nil, fmt.Errorf("failed to check if chain exists: %w", err)
}
if !ok {
if err := client.NewChain("filter", ChainInputFilterName); err != nil {
return nil, fmt.Errorf("failed to create input chain: %w", err)
}
if err := client.AppendUnique("filter", ChainInputFilterName, dropAllDefaultRule...); err != nil {
return nil, fmt.Errorf("failed to create default drop all in netbird input chain: %w", err)
}
if err := client.AppendUnique("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
return nil, fmt.Errorf("failed to create input chain jump rule: %w", err)
}
}
ok, err = client.ChainExists("filter", ChainOutputFilterName)
if err != nil {
return nil, fmt.Errorf("failed to check if chain exists: %w", err)
}
if !ok {
if err := client.NewChain("filter", ChainOutputFilterName); err != nil {
return nil, fmt.Errorf("failed to create output chain: %w", err)
}
if err := client.AppendUnique("filter", ChainOutputFilterName, dropAllDefaultRule...); err != nil {
return nil, fmt.Errorf("failed to create default drop all in netbird output chain: %w", err)
}
if err := client.AppendUnique("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
return nil, fmt.Errorf("failed to create output chain jump rule: %w", err)
}
}
return client, nil
return m.ipv6Client
}
func (m *Manager) actionToStr(action fw.Action) string {

View File

@@ -1,180 +1,105 @@
package iptables
import (
"fmt"
"net"
"testing"
"time"
"github.com/coreos/go-iptables/iptables"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/iface"
)
// iFaceMapper defines subset methods of interface required for manager
type iFaceMock struct {
NameFunc func() string
AddressFunc func() iface.WGAddress
}
func (i *iFaceMock) Name() string {
if i.NameFunc != nil {
return i.NameFunc()
}
panic("NameFunc is not set")
}
func (i *iFaceMock) Address() iface.WGAddress {
if i.AddressFunc != nil {
return i.AddressFunc()
}
panic("AddressFunc is not set")
}
func TestIptablesManager(t *testing.T) {
func TestNewManager(t *testing.T) {
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
require.NoError(t, err)
mock := &iFaceMock{
NameFunc: func() string {
return "lo"
},
AddressFunc: func() iface.WGAddress {
return iface.WGAddress{
IP: net.ParseIP("10.20.0.1"),
Network: &net.IPNet{
IP: net.ParseIP("10.20.0.0"),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
}
},
if err != nil {
t.Fatal(err)
}
// just check on the local interface
manager, err := Create(mock)
require.NoError(t, err)
time.Sleep(time.Second)
defer func() {
if err := manager.Reset(); err != nil {
t.Errorf("clear the manager state: %v", err)
}
time.Sleep(time.Second)
}()
manager, err := Create()
if err != nil {
t.Fatal(err)
}
var rule1 fw.Rule
t.Run("add first rule", func(t *testing.T) {
ip := net.ParseIP("10.20.0.2")
port := &fw.Port{Values: []int{8080}}
rule1, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic")
require.NoError(t, err, "failed to add rule")
port := &fw.Port{Proto: fw.PortProtocolTCP, Values: []int{8080}}
rule1, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTP traffic")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, true, rule1.(*Rule).specs...)
checkRuleSpecs(t, ipv4Client, true, rule1.(*Rule).specs...)
})
var rule2 fw.Rule
t.Run("add second rule", func(t *testing.T) {
ip := net.ParseIP("10.20.0.3")
port := &fw.Port{
Proto: fw.PortProtocolTCP,
Values: []int{8043: 8046},
}
rule2, err = manager.AddFiltering(
ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTPS traffic from ports range")
require.NoError(t, err, "failed to add rule")
ip, port, fw.DirectionDst, fw.ActionAccept, "accept HTTPS traffic from ports range")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, ChainInputFilterName, true, rule2.(*Rule).specs...)
checkRuleSpecs(t, ipv4Client, true, rule2.(*Rule).specs...)
})
t.Run("delete first rule", func(t *testing.T) {
if err := manager.DeleteRule(rule1); err != nil {
require.NoError(t, err, "failed to delete rule")
t.Errorf("failed to delete rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, false, rule1.(*Rule).specs...)
checkRuleSpecs(t, ipv4Client, false, rule1.(*Rule).specs...)
})
t.Run("delete second rule", func(t *testing.T) {
if err := manager.DeleteRule(rule2); err != nil {
require.NoError(t, err, "failed to delete rule")
t.Errorf("failed to delete rule: %v", err)
}
checkRuleSpecs(t, ipv4Client, ChainInputFilterName, false, rule2.(*Rule).specs...)
checkRuleSpecs(t, ipv4Client, false, rule2.(*Rule).specs...)
})
t.Run("reset check", func(t *testing.T) {
// add second rule
ip := net.ParseIP("10.20.0.3")
port := &fw.Port{Values: []int{5353}}
_, err = manager.AddFiltering(ip, "udp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept Fake DNS traffic")
require.NoError(t, err, "failed to add rule")
port := &fw.Port{Proto: fw.PortProtocolUDP, Values: []int{5353}}
_, err = manager.AddFiltering(ip, port, fw.DirectionDst, fw.ActionAccept, "accept Fake DNS traffic")
if err != nil {
t.Errorf("failed to add rule: %v", err)
}
err = manager.Reset()
require.NoError(t, err, "failed to reset")
if err := manager.Reset(); err != nil {
t.Errorf("failed to reset: %v", err)
}
ok, err := ipv4Client.ChainExists("filter", ChainInputFilterName)
require.NoError(t, err, "failed check chain exists")
ok, err := ipv4Client.ChainExists("filter", ChainFilterName)
if err != nil {
t.Errorf("failed to drop chain: %v", err)
}
if ok {
require.NoErrorf(t, err, "chain '%v' still exists after Reset", ChainInputFilterName)
t.Errorf("chain '%v' still exists after Reset", ChainFilterName)
}
})
}
func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName string, mustExists bool, rulespec ...string) {
exists, err := ipv4Client.Exists("filter", chainName, rulespec...)
require.NoError(t, err, "failed to check rule")
require.Falsef(t, !exists && mustExists, "rule '%v' does not exist", rulespec)
require.Falsef(t, exists && !mustExists, "rule '%v' exist", rulespec)
}
func TestIptablesCreatePerformance(t *testing.T) {
mock := &iFaceMock{
NameFunc: func() string {
return "lo"
},
AddressFunc: func() iface.WGAddress {
return iface.WGAddress{
IP: net.ParseIP("10.20.0.1"),
Network: &net.IPNet{
IP: net.ParseIP("10.20.0.0"),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
}
},
func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, mustExists bool, rulespec ...string) {
exists, err := ipv4Client.Exists("filter", ChainFilterName, rulespec...)
if err != nil {
t.Errorf("failed to check rule: %v", err)
return
}
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
// just check on the local interface
manager, err := Create(mock)
require.NoError(t, err)
time.Sleep(time.Second)
defer func() {
if err := manager.Reset(); err != nil {
t.Errorf("clear the manager state: %v", err)
}
time.Sleep(time.Second)
}()
_, err = manager.client(net.ParseIP("10.20.0.100"))
require.NoError(t, err)
ip := net.ParseIP("10.20.0.100")
start := time.Now()
for i := 0; i < testMax; i++ {
port := &fw.Port{Values: []int{1000 + i}}
if i%2 == 0 {
_, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic")
} else {
_, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic")
}
require.NoError(t, err, "failed to add rule")
}
t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax))
})
if !exists && mustExists {
t.Errorf("rule '%v' does not exist", rulespec)
return
}
if exists && !mustExists {
t.Errorf("rule '%v' exist", rulespec)
return
}
}

View File

@@ -4,7 +4,6 @@ package iptables
type Rule struct {
id string
specs []string
dst bool
v6 bool
}

View File

@@ -1,511 +0,0 @@
package nftables
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"net/netip"
"strings"
"sync"
"github.com/google/nftables"
"github.com/google/nftables/expr"
"github.com/google/uuid"
"golang.org/x/sys/unix"
fw "github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/iface"
)
const (
// FilterTableName is the name of the table that is used for filtering by the Netbird client
FilterTableName = "netbird-acl"
// FilterInputChainName is the name of the chain that is used for filtering incoming packets
FilterInputChainName = "netbird-acl-input-filter"
// FilterOutputChainName is the name of the chain that is used for filtering outgoing packets
FilterOutputChainName = "netbird-acl-output-filter"
)
// Manager of iptables firewall
type Manager struct {
mutex sync.Mutex
conn *nftables.Conn
tableIPv4 *nftables.Table
tableIPv6 *nftables.Table
filterInputChainIPv4 *nftables.Chain
filterOutputChainIPv4 *nftables.Chain
filterInputChainIPv6 *nftables.Chain
filterOutputChainIPv6 *nftables.Chain
wgIface iFaceMapper
}
// iFaceMapper defines subset methods of interface required for manager
type iFaceMapper interface {
Name() string
Address() iface.WGAddress
}
// Create nftables firewall manager
func Create(wgIface iFaceMapper) (*Manager, error) {
m := &Manager{
conn: &nftables.Conn{},
wgIface: wgIface,
}
if err := m.Reset(); err != nil {
return nil, err
}
return m, nil
}
// AddFiltering rule to the firewall
//
// If comment argument is empty firewall manager should set
// rule ID as comment for the rule
func (m *Manager) AddFiltering(
ip net.IP,
proto fw.Protocol,
sPort *fw.Port,
dPort *fw.Port,
direction fw.RuleDirection,
action fw.Action,
comment string,
) (fw.Rule, error) {
m.mutex.Lock()
defer m.mutex.Unlock()
var (
err error
table *nftables.Table
chain *nftables.Chain
)
if direction == fw.RuleDirectionOUT {
table, chain, err = m.chain(
ip,
FilterOutputChainName,
nftables.ChainHookOutput,
nftables.ChainPriorityFilter,
nftables.ChainTypeFilter)
} else {
table, chain, err = m.chain(
ip,
FilterInputChainName,
nftables.ChainHookInput,
nftables.ChainPriorityFilter,
nftables.ChainTypeFilter)
}
if err != nil {
return nil, err
}
ifaceKey := expr.MetaKeyIIFNAME
if direction == fw.RuleDirectionOUT {
ifaceKey = expr.MetaKeyOIFNAME
}
expressions := []expr.Any{
&expr.Meta{Key: ifaceKey, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: ifname(m.wgIface.Name()),
},
}
if proto != "all" {
expressions = append(expressions, &expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: uint32(9),
Len: uint32(1),
})
var protoData []byte
switch proto {
case fw.ProtocolTCP:
protoData = []byte{unix.IPPROTO_TCP}
case fw.ProtocolUDP:
protoData = []byte{unix.IPPROTO_UDP}
case fw.ProtocolICMP:
protoData = []byte{unix.IPPROTO_ICMP}
default:
return nil, fmt.Errorf("unsupported protocol: %s", proto)
}
expressions = append(expressions, &expr.Cmp{
Register: 1,
Op: expr.CmpOpEq,
Data: protoData,
})
}
// don't use IP matching if IP is ip 0.0.0.0
if s := ip.String(); s != "0.0.0.0" && s != "::" {
// source address position
var adrLen, adrOffset uint32
if ip.To4() == nil {
adrLen = 16
adrOffset = 8
} else {
adrLen = 4
adrOffset = 12
}
// change to destination address position if need
if direction == fw.RuleDirectionOUT {
adrOffset += adrLen
}
ipToAdd, _ := netip.AddrFromSlice(ip)
add := ipToAdd.Unmap()
expressions = append(expressions,
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: adrOffset,
Len: adrLen,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: add.AsSlice(),
},
)
}
if sPort != nil && len(sPort.Values) != 0 {
expressions = append(expressions,
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 0,
Len: 2,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: encodePort(*sPort),
},
)
}
if dPort != nil && len(dPort.Values) != 0 {
expressions = append(expressions,
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: encodePort(*dPort),
},
)
}
if action == fw.ActionAccept {
expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictAccept})
} else {
expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictDrop})
}
id := uuid.New().String()
userData := []byte(strings.Join([]string{id, comment}, " "))
_ = m.conn.InsertRule(&nftables.Rule{
Table: table,
Chain: chain,
Position: 0,
Exprs: expressions,
UserData: userData,
})
if err := m.conn.Flush(); err != nil {
return nil, err
}
list, err := m.conn.GetRules(table, chain)
if err != nil {
return nil, err
}
// Add the rule to the chain
rule := &Rule{id: id}
for _, r := range list {
if bytes.Equal(r.UserData, userData) {
rule.Rule = r
break
}
}
if rule.Rule == nil {
return nil, fmt.Errorf("rule not found")
}
return rule, nil
}
// chain returns the chain for the given IP address with specific settings
func (m *Manager) chain(
ip net.IP,
name string,
hook nftables.ChainHook,
priority nftables.ChainPriority,
cType nftables.ChainType,
) (*nftables.Table, *nftables.Chain, error) {
var err error
getChain := func(c *nftables.Chain, tf nftables.TableFamily) (*nftables.Chain, error) {
if c != nil {
return c, nil
}
return m.createChainIfNotExists(tf, name, hook, priority, cType)
}
if ip.To4() != nil {
if name == FilterInputChainName {
m.filterInputChainIPv4, err = getChain(m.filterInputChainIPv4, nftables.TableFamilyIPv4)
return m.tableIPv4, m.filterInputChainIPv4, err
}
m.filterOutputChainIPv4, err = getChain(m.filterOutputChainIPv4, nftables.TableFamilyIPv4)
return m.tableIPv4, m.filterOutputChainIPv4, err
}
if name == FilterInputChainName {
m.filterInputChainIPv6, err = getChain(m.filterInputChainIPv6, nftables.TableFamilyIPv6)
return m.tableIPv4, m.filterInputChainIPv6, err
}
m.filterOutputChainIPv6, err = getChain(m.filterOutputChainIPv6, nftables.TableFamilyIPv6)
return m.tableIPv4, m.filterOutputChainIPv6, err
}
// table returns the table for the given family of the IP address
func (m *Manager) table(family nftables.TableFamily) (*nftables.Table, error) {
if family == nftables.TableFamilyIPv4 {
if m.tableIPv4 != nil {
return m.tableIPv4, nil
}
table, err := m.createTableIfNotExists(nftables.TableFamilyIPv4)
if err != nil {
return nil, err
}
m.tableIPv4 = table
return m.tableIPv4, nil
}
if m.tableIPv6 != nil {
return m.tableIPv6, nil
}
table, err := m.createTableIfNotExists(nftables.TableFamilyIPv6)
if err != nil {
return nil, err
}
m.tableIPv6 = table
return m.tableIPv6, nil
}
func (m *Manager) createTableIfNotExists(family nftables.TableFamily) (*nftables.Table, error) {
tables, err := m.conn.ListTablesOfFamily(family)
if err != nil {
return nil, fmt.Errorf("list of tables: %w", err)
}
for _, t := range tables {
if t.Name == FilterTableName {
return t, nil
}
}
return m.conn.AddTable(&nftables.Table{Name: FilterTableName, Family: nftables.TableFamilyIPv4}), nil
}
func (m *Manager) createChainIfNotExists(
family nftables.TableFamily,
name string,
hooknum nftables.ChainHook,
priority nftables.ChainPriority,
chainType nftables.ChainType,
) (*nftables.Chain, error) {
table, err := m.table(family)
if err != nil {
return nil, err
}
chains, err := m.conn.ListChainsOfTableFamily(family)
if err != nil {
return nil, fmt.Errorf("list of chains: %w", err)
}
for _, c := range chains {
if c.Name == name && c.Table.Name == table.Name {
return c, nil
}
}
polAccept := nftables.ChainPolicyAccept
chain := &nftables.Chain{
Name: name,
Table: table,
Hooknum: hooknum,
Priority: priority,
Type: chainType,
Policy: &polAccept,
}
chain = m.conn.AddChain(chain)
ifaceKey := expr.MetaKeyIIFNAME
shiftDSTAddr := 0
if name == FilterOutputChainName {
ifaceKey = expr.MetaKeyOIFNAME
shiftDSTAddr = 1
}
expressions := []expr.Any{
&expr.Meta{Key: ifaceKey, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: ifname(m.wgIface.Name()),
},
}
mask, _ := netip.AddrFromSlice(m.wgIface.Address().Network.Mask)
if m.wgIface.Address().IP.To4() == nil {
ip, _ := netip.AddrFromSlice(m.wgIface.Address().Network.IP.To16())
expressions = append(expressions,
&expr.Payload{
DestRegister: 2,
Base: expr.PayloadBaseNetworkHeader,
Offset: uint32(8 + (16 * shiftDSTAddr)),
Len: 16,
},
&expr.Bitwise{
SourceRegister: 2,
DestRegister: 2,
Len: 16,
Xor: []byte{0x0, 0x0, 0x0, 0x0},
Mask: mask.Unmap().AsSlice(),
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 2,
Data: ip.Unmap().AsSlice(),
},
&expr.Verdict{Kind: expr.VerdictAccept},
)
} else {
ip, _ := netip.AddrFromSlice(m.wgIface.Address().Network.IP.To4())
expressions = append(expressions,
&expr.Payload{
DestRegister: 2,
Base: expr.PayloadBaseNetworkHeader,
Offset: uint32(12 + (4 * shiftDSTAddr)),
Len: 4,
},
&expr.Bitwise{
SourceRegister: 2,
DestRegister: 2,
Len: 4,
Xor: []byte{0x0, 0x0, 0x0, 0x0},
Mask: m.wgIface.Address().Network.Mask,
},
&expr.Cmp{
Op: expr.CmpOpNeq,
Register: 2,
Data: ip.Unmap().AsSlice(),
},
&expr.Verdict{Kind: expr.VerdictAccept},
)
}
_ = m.conn.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: expressions,
})
expressions = []expr.Any{
&expr.Meta{Key: ifaceKey, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: ifname(m.wgIface.Name()),
},
&expr.Verdict{Kind: expr.VerdictDrop},
}
_ = m.conn.AddRule(&nftables.Rule{
Table: table,
Chain: chain,
Exprs: expressions,
})
if err := m.conn.Flush(); err != nil {
return nil, err
}
return chain, nil
}
// DeleteRule from the firewall by rule definition
func (m *Manager) DeleteRule(rule fw.Rule) error {
nativeRule, ok := rule.(*Rule)
if !ok {
return fmt.Errorf("invalid rule type")
}
if err := m.conn.DelRule(nativeRule.Rule); err != nil {
return err
}
return m.conn.Flush()
}
// Reset firewall to the default state
func (m *Manager) Reset() error {
m.mutex.Lock()
defer m.mutex.Unlock()
chains, err := m.conn.ListChains()
if err != nil {
return fmt.Errorf("list of chains: %w", err)
}
for _, c := range chains {
if c.Name == FilterInputChainName || c.Name == FilterOutputChainName {
m.conn.DelChain(c)
}
}
tables, err := m.conn.ListTables()
if err != nil {
return fmt.Errorf("list of tables: %w", err)
}
for _, t := range tables {
if t.Name == FilterTableName {
m.conn.DelTable(t)
}
}
return m.conn.Flush()
}
func encodePort(port fw.Port) []byte {
bs := make([]byte, 2)
binary.BigEndian.PutUint16(bs, uint16(port.Values[0]))
return bs
}
func ifname(n string) []byte {
b := make([]byte, 16)
copy(b, []byte(n+"\x00"))
return b
}

View File

@@ -1,194 +0,0 @@
package nftables
import (
"fmt"
"net"
"net/netip"
"testing"
"time"
"github.com/google/nftables"
"github.com/google/nftables/expr"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
fw "github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/iface"
)
// iFaceMapper defines subset methods of interface required for manager
type iFaceMock struct {
NameFunc func() string
AddressFunc func() iface.WGAddress
}
func (i *iFaceMock) Name() string {
if i.NameFunc != nil {
return i.NameFunc()
}
panic("NameFunc is not set")
}
func (i *iFaceMock) Address() iface.WGAddress {
if i.AddressFunc != nil {
return i.AddressFunc()
}
panic("AddressFunc is not set")
}
func TestNftablesManager(t *testing.T) {
mock := &iFaceMock{
NameFunc: func() string {
return "lo"
},
AddressFunc: func() iface.WGAddress {
return iface.WGAddress{
IP: net.ParseIP("100.96.0.1"),
Network: &net.IPNet{
IP: net.ParseIP("100.96.0.0"),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
}
},
}
// just check on the local interface
manager, err := Create(mock)
require.NoError(t, err)
time.Sleep(time.Second)
defer func() {
err = manager.Reset()
require.NoError(t, err, "failed to reset")
time.Sleep(time.Second)
}()
ip := net.ParseIP("100.96.0.1")
testClient := &nftables.Conn{}
rule, err := manager.AddFiltering(
ip,
fw.ProtocolTCP,
nil,
&fw.Port{Values: []int{53}},
fw.RuleDirectionIN,
fw.ActionDrop,
"",
)
require.NoError(t, err, "failed to add rule")
rules, err := testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4)
require.NoError(t, err, "failed to get rules")
// test expectations:
// 1) regular rule
// 2) "accept extra routed traffic rule" for the interface
// 3) "drop all rule" for the interface
require.Len(t, rules, 3, "expected 3 rules")
ipToAdd, _ := netip.AddrFromSlice(ip)
add := ipToAdd.Unmap()
expectedExprs := []expr.Any{
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: ifname("lo"),
},
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: uint32(9),
Len: uint32(1),
},
&expr.Cmp{
Register: 1,
Op: expr.CmpOpEq,
Data: []byte{unix.IPPROTO_TCP},
},
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseNetworkHeader,
Offset: 12,
Len: 4,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: add.AsSlice(),
},
&expr.Payload{
DestRegister: 1,
Base: expr.PayloadBaseTransportHeader,
Offset: 2,
Len: 2,
},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte{0, 53},
},
&expr.Verdict{Kind: expr.VerdictDrop},
}
require.ElementsMatch(t, rules[0].Exprs, expectedExprs, "expected the same expressions")
err = manager.DeleteRule(rule)
require.NoError(t, err, "failed to delete rule")
rules, err = testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4)
require.NoError(t, err, "failed to get rules")
// test expectations:
// 1) "accept extra routed traffic rule" for the interface
// 2) "drop all rule" for the interface
require.Len(t, rules, 2, "expected 2 rules after deleteion")
err = manager.Reset()
require.NoError(t, err, "failed to reset")
}
func TestNFtablesCreatePerformance(t *testing.T) {
mock := &iFaceMock{
NameFunc: func() string {
return "lo"
},
AddressFunc: func() iface.WGAddress {
return iface.WGAddress{
IP: net.ParseIP("100.96.0.1"),
Network: &net.IPNet{
IP: net.ParseIP("100.96.0.0"),
Mask: net.IPv4Mask(255, 255, 255, 0),
},
}
},
}
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
// just check on the local interface
manager, err := Create(mock)
require.NoError(t, err)
time.Sleep(time.Second)
defer func() {
if err := manager.Reset(); err != nil {
t.Errorf("clear the manager state: %v", err)
}
time.Sleep(time.Second)
}()
ip := net.ParseIP("10.20.0.100")
start := time.Now()
for i := 0; i < testMax; i++ {
port := &fw.Port{Values: []int{1000 + i}}
if i%2 == 0 {
_, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic")
} else {
_, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic")
}
require.NoError(t, err, "failed to add rule")
}
t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax))
})
}
}

View File

@@ -1,16 +0,0 @@
package nftables
import (
"github.com/google/nftables"
)
// Rule to handle management of rules
type Rule struct {
*nftables.Rule
id string
}
// GetRuleID returns the rule id
func (r *Rule) GetRuleID() string {
return r.id
}

View File

@@ -1,27 +1,14 @@
package firewall
import (
"strconv"
)
// Protocol is the protocol of the port
type Protocol string
// PortProtocol is the protocol of the port
type PortProtocol string
const (
// ProtocolTCP is the TCP protocol
ProtocolTCP Protocol = "tcp"
// PortProtocolTCP is the TCP protocol
PortProtocolTCP PortProtocol = "tcp"
// ProtocolUDP is the UDP protocol
ProtocolUDP Protocol = "udp"
// ProtocolICMP is the ICMP protocol
ProtocolICMP Protocol = "icmp"
// ProtocolALL cover all supported protocols
ProtocolALL Protocol = "all"
// ProtocolUnknown unknown protocol
ProtocolUnknown Protocol = "unknown"
// PortProtocolUDP is the UDP protocol
PortProtocolUDP PortProtocol = "udp"
)
// Port of the address for firewall rule
@@ -31,16 +18,7 @@ type Port struct {
// Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports
Values []int
}
// String interface implementation
func (p *Port) String() string {
var ports string
for _, port := range p.Values {
if ports != "" {
ports += ","
}
ports += strconv.Itoa(port)
}
return ports
// Proto is the protocol of the port
Proto PortProtocol
}

View File

@@ -1,30 +0,0 @@
package uspfilter
import (
"net"
"github.com/google/gopacket"
fw "github.com/netbirdio/netbird/client/firewall"
)
// Rule to handle management of rules
type Rule struct {
id string
ip net.IP
ipLayer gopacket.LayerType
matchByIP bool
protoLayer gopacket.LayerType
direction fw.RuleDirection
sPort uint16
dPort uint16
drop bool
comment string
udpHook func([]byte) bool
}
// GetRuleID returns the rule id
func (r *Rule) GetRuleID() string {
return r.id
}

View File

@@ -1,359 +0,0 @@
package uspfilter
import (
"fmt"
"net"
"sync"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
fw "github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/iface"
)
const layerTypeAll = 0
// IFaceMapper defines subset methods of interface required for manager
type IFaceMapper interface {
SetFilter(iface.PacketFilter) error
}
// Manager userspace firewall manager
type Manager struct {
outgoingRules []Rule
incomingRules []Rule
rulesIndex map[string]int
wgNetwork *net.IPNet
decoders sync.Pool
mutex sync.RWMutex
}
// decoder for packages
type decoder struct {
eth layers.Ethernet
ip4 layers.IPv4
ip6 layers.IPv6
tcp layers.TCP
udp layers.UDP
icmp4 layers.ICMPv4
icmp6 layers.ICMPv6
decoded []gopacket.LayerType
parser *gopacket.DecodingLayerParser
}
// Create userspace firewall manager constructor
func Create(iface IFaceMapper) (*Manager, error) {
m := &Manager{
rulesIndex: make(map[string]int),
decoders: sync.Pool{
New: func() any {
d := &decoder{
decoded: []gopacket.LayerType{},
}
d.parser = gopacket.NewDecodingLayerParser(
layers.LayerTypeIPv4,
&d.eth, &d.ip4, &d.ip6, &d.icmp4, &d.icmp6, &d.tcp, &d.udp,
)
d.parser.IgnoreUnsupported = true
return d
},
},
}
if err := iface.SetFilter(m); err != nil {
return nil, err
}
return m, nil
}
// AddFiltering rule to the firewall
//
// If comment argument is empty firewall manager should set
// rule ID as comment for the rule
func (m *Manager) AddFiltering(
ip net.IP,
proto fw.Protocol,
sPort *fw.Port,
dPort *fw.Port,
direction fw.RuleDirection,
action fw.Action,
comment string,
) (fw.Rule, error) {
r := Rule{
id: uuid.New().String(),
ip: ip,
ipLayer: layers.LayerTypeIPv6,
matchByIP: true,
direction: direction,
drop: action == fw.ActionDrop,
comment: comment,
}
if ipNormalized := ip.To4(); ipNormalized != nil {
r.ipLayer = layers.LayerTypeIPv4
r.ip = ipNormalized
}
if s := r.ip.String(); s == "0.0.0.0" || s == "::" {
r.matchByIP = false
}
if sPort != nil && len(sPort.Values) == 1 {
r.sPort = uint16(sPort.Values[0])
}
if dPort != nil && len(dPort.Values) == 1 {
r.dPort = uint16(dPort.Values[0])
}
switch proto {
case fw.ProtocolTCP:
r.protoLayer = layers.LayerTypeTCP
case fw.ProtocolUDP:
r.protoLayer = layers.LayerTypeUDP
case fw.ProtocolICMP:
r.protoLayer = layers.LayerTypeICMPv4
if r.ipLayer == layers.LayerTypeIPv6 {
r.protoLayer = layers.LayerTypeICMPv6
}
case fw.ProtocolALL:
r.protoLayer = layerTypeAll
}
m.mutex.Lock()
var p int
if direction == fw.RuleDirectionIN {
m.incomingRules = append(m.incomingRules, r)
p = len(m.incomingRules) - 1
} else {
m.outgoingRules = append(m.outgoingRules, r)
p = len(m.outgoingRules) - 1
}
m.rulesIndex[r.id] = p
m.mutex.Unlock()
return &r, nil
}
// DeleteRule from the firewall by rule definition
func (m *Manager) DeleteRule(rule fw.Rule) error {
m.mutex.Lock()
defer m.mutex.Unlock()
r, ok := rule.(*Rule)
if !ok {
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
}
p, ok := m.rulesIndex[r.id]
if !ok {
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
}
delete(m.rulesIndex, r.id)
var toUpdate []Rule
if r.direction == fw.RuleDirectionIN {
m.incomingRules = append(m.incomingRules[:p], m.incomingRules[p+1:]...)
toUpdate = m.incomingRules
} else {
m.outgoingRules = append(m.outgoingRules[:p], m.outgoingRules[p+1:]...)
toUpdate = m.outgoingRules
}
for i := 0; i < len(toUpdate); i++ {
m.rulesIndex[toUpdate[i].id] = i
}
return nil
}
// Reset firewall to the default state
func (m *Manager) Reset() error {
m.mutex.Lock()
defer m.mutex.Unlock()
m.outgoingRules = m.outgoingRules[:0]
m.incomingRules = m.incomingRules[:0]
m.rulesIndex = make(map[string]int)
return nil
}
// DropOutgoing filter outgoing packets
func (m *Manager) DropOutgoing(packetData []byte) bool {
return m.dropFilter(packetData, m.outgoingRules, false)
}
// DropIncoming filter incoming packets
func (m *Manager) DropIncoming(packetData []byte) bool {
return m.dropFilter(packetData, m.incomingRules, true)
}
// dropFilter imlements same logic for booth direction of the traffic
func (m *Manager) dropFilter(packetData []byte, rules []Rule, isIncomingPacket bool) bool {
m.mutex.RLock()
defer m.mutex.RUnlock()
d := m.decoders.Get().(*decoder)
defer m.decoders.Put(d)
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
log.Tracef("couldn't decode layer, err: %s", err)
return true
}
if len(d.decoded) < 2 {
log.Tracef("not enough levels in network packet")
return true
}
ipLayer := d.decoded[0]
switch ipLayer {
case layers.LayerTypeIPv4:
if !m.wgNetwork.Contains(d.ip4.SrcIP) || !m.wgNetwork.Contains(d.ip4.DstIP) {
return false
}
case layers.LayerTypeIPv6:
if !m.wgNetwork.Contains(d.ip6.SrcIP) || !m.wgNetwork.Contains(d.ip6.DstIP) {
return false
}
default:
log.Errorf("unknown layer: %v", d.decoded[0])
return true
}
payloadLayer := d.decoded[1]
// check if IP address match by IP
for _, rule := range rules {
if rule.matchByIP {
switch ipLayer {
case layers.LayerTypeIPv4:
if isIncomingPacket {
if !d.ip4.SrcIP.Equal(rule.ip) {
continue
}
} else {
if !d.ip4.DstIP.Equal(rule.ip) {
continue
}
}
case layers.LayerTypeIPv6:
if isIncomingPacket {
if !d.ip6.SrcIP.Equal(rule.ip) {
continue
}
} else {
if !d.ip6.DstIP.Equal(rule.ip) {
continue
}
}
}
}
if rule.protoLayer == layerTypeAll {
return rule.drop
}
if payloadLayer != rule.protoLayer {
continue
}
switch payloadLayer {
case layers.LayerTypeTCP:
if rule.sPort == 0 && rule.dPort == 0 {
return rule.drop
}
if rule.sPort != 0 && rule.sPort == uint16(d.tcp.SrcPort) {
return rule.drop
}
if rule.dPort != 0 && rule.dPort == uint16(d.tcp.DstPort) {
return rule.drop
}
case layers.LayerTypeUDP:
// if rule has UDP hook (and if we are here we match this rule)
// we ignore rule.drop and call this hook
if rule.udpHook != nil {
return rule.udpHook(packetData)
}
if rule.sPort == 0 && rule.dPort == 0 {
return rule.drop
}
if rule.sPort != 0 && rule.sPort == uint16(d.udp.SrcPort) {
return rule.drop
}
if rule.dPort != 0 && rule.dPort == uint16(d.udp.DstPort) {
return rule.drop
}
return rule.drop
case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6:
return rule.drop
}
}
// default policy is DROP ALL
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
//
// Hook function returns flag which indicates should be the matched package dropped or not
func (m *Manager) AddUDPPacketHook(
in bool, ip net.IP, dPort uint16, hook func([]byte) bool,
) string {
r := Rule{
id: uuid.New().String(),
ip: ip,
protoLayer: layers.LayerTypeUDP,
dPort: dPort,
ipLayer: layers.LayerTypeIPv6,
direction: fw.RuleDirectionOUT,
comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort),
udpHook: hook,
}
if ip.To4() != nil {
r.ipLayer = layers.LayerTypeIPv4
}
m.mutex.Lock()
var toUpdate []Rule
if in {
r.direction = fw.RuleDirectionIN
m.incomingRules = append([]Rule{r}, m.incomingRules...)
toUpdate = m.incomingRules
} else {
m.outgoingRules = append([]Rule{r}, m.outgoingRules...)
toUpdate = m.outgoingRules
}
for i := range toUpdate {
m.rulesIndex[toUpdate[i].id] = i
}
m.mutex.Unlock()
return r.id
}
// RemovePacketHook removes packet hook by given ID
func (m *Manager) RemovePacketHook(hookID string) error {
for _, r := range m.incomingRules {
if r.id == hookID {
return m.DeleteRule(&r)
}
}
for _, r := range m.outgoingRules {
if r.id == hookID {
return m.DeleteRule(&r)
}
}
return fmt.Errorf("hook with given id not found")
}

View File

@@ -1,407 +0,0 @@
package uspfilter
import (
"fmt"
"net"
"testing"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/stretchr/testify/require"
fw "github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/iface"
)
type IFaceMock struct {
SetFilterFunc func(iface.PacketFilter) error
}
func (i *IFaceMock) SetFilter(iface iface.PacketFilter) error {
if i.SetFilterFunc == nil {
return fmt.Errorf("not implemented")
}
return i.SetFilterFunc(iface)
}
func TestManagerCreate(t *testing.T) {
ifaceMock := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock)
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
if m == nil {
t.Error("Manager is nil")
}
}
func TestManagerAddFiltering(t *testing.T) {
isSetFilterCalled := false
ifaceMock := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error {
isSetFilterCalled = true
return nil
},
}
m, err := Create(ifaceMock)
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []int{80}}
direction := fw.RuleDirectionOUT
action := fw.ActionDrop
comment := "Test rule"
rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
}
if rule == nil {
t.Error("Rule is nil")
return
}
if !isSetFilterCalled {
t.Error("SetFilter was not called")
return
}
}
func TestManagerDeleteRule(t *testing.T) {
ifaceMock := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock)
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []int{80}}
direction := fw.RuleDirectionOUT
action := fw.ActionDrop
comment := "Test rule"
rule, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
}
ip = net.ParseIP("192.168.1.1")
proto = fw.ProtocolTCP
port = &fw.Port{Values: []int{80}}
direction = fw.RuleDirectionIN
action = fw.ActionDrop
comment = "Test rule 2"
rule2, err := m.AddFiltering(ip, proto, nil, port, direction, action, comment)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
}
err = m.DeleteRule(rule)
if err != nil {
t.Errorf("failed to delete rule: %v", err)
return
}
if idx, ok := m.rulesIndex[rule2.GetRuleID()]; !ok || len(m.incomingRules) != 1 || idx != 0 {
t.Errorf("rule2 is not in the rulesIndex")
}
err = m.DeleteRule(rule2)
if err != nil {
t.Errorf("failed to delete rule: %v", err)
return
}
if len(m.rulesIndex) != 0 || len(m.incomingRules) != 0 {
t.Errorf("rule1 still in the rulesIndex")
}
}
func TestAddUDPPacketHook(t *testing.T) {
tests := []struct {
name string
in bool
expDir fw.RuleDirection
ip net.IP
dPort uint16
hook func([]byte) bool
expectedID string
}{
{
name: "Test Outgoing UDP Packet Hook",
in: false,
expDir: fw.RuleDirectionOUT,
ip: net.IPv4(10, 168, 0, 1),
dPort: 8000,
hook: func([]byte) bool { return true },
},
{
name: "Test Incoming UDP Packet Hook",
in: true,
expDir: fw.RuleDirectionIN,
ip: net.IPv6loopback,
dPort: 9000,
hook: func([]byte) bool { return false },
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
manager := &Manager{
incomingRules: []Rule{},
outgoingRules: []Rule{},
rulesIndex: make(map[string]int),
}
manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook)
var addedRule Rule
if tt.in {
if len(manager.incomingRules) != 1 {
t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules))
return
}
addedRule = manager.incomingRules[0]
} else {
if len(manager.outgoingRules) != 1 {
t.Errorf("expected 1 outgoing rule, got %d", len(manager.outgoingRules))
return
}
addedRule = manager.outgoingRules[0]
}
if !tt.ip.Equal(addedRule.ip) {
t.Errorf("expected ip %s, got %s", tt.ip, addedRule.ip)
return
}
if tt.dPort != addedRule.dPort {
t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort)
return
}
if layers.LayerTypeUDP != addedRule.protoLayer {
t.Errorf("expected protoLayer %s, got %s", layers.LayerTypeUDP, addedRule.protoLayer)
return
}
if tt.expDir != addedRule.direction {
t.Errorf("expected direction %d, got %d", tt.expDir, addedRule.direction)
return
}
if addedRule.udpHook == nil {
t.Errorf("expected udpHook to be set")
return
}
// Ensure rulesIndex is correctly updated
index, ok := manager.rulesIndex[addedRule.id]
if !ok {
t.Errorf("expected rule to be in rulesIndex")
return
}
if index != 0 {
t.Errorf("expected rule index to be 0, got %d", index)
return
}
})
}
}
func TestManagerReset(t *testing.T) {
ifaceMock := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock)
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
ip := net.ParseIP("192.168.1.1")
proto := fw.ProtocolTCP
port := &fw.Port{Values: []int{80}}
direction := fw.RuleDirectionOUT
action := fw.ActionDrop
comment := "Test rule"
_, err = m.AddFiltering(ip, proto, nil, port, direction, action, comment)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
}
err = m.Reset()
if err != nil {
t.Errorf("failed to reset Manager: %v", err)
return
}
if len(m.rulesIndex) != 0 || len(m.outgoingRules) != 0 || len(m.incomingRules) != 0 {
t.Errorf("rules is not empty")
}
}
func TestNotMatchByIP(t *testing.T) {
ifaceMock := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error { return nil },
}
m, err := Create(ifaceMock)
if err != nil {
t.Errorf("failed to create Manager: %v", err)
return
}
m.wgNetwork = &net.IPNet{
IP: net.ParseIP("100.10.0.0"),
Mask: net.CIDRMask(16, 32),
}
ip := net.ParseIP("0.0.0.0")
proto := fw.ProtocolUDP
direction := fw.RuleDirectionOUT
action := fw.ActionAccept
comment := "Test rule"
_, err = m.AddFiltering(ip, proto, nil, nil, direction, action, comment)
if err != nil {
t.Errorf("failed to add filtering: %v", err)
return
}
ipv4 := &layers.IPv4{
TTL: 64,
Version: 4,
SrcIP: net.ParseIP("100.10.0.1"),
DstIP: net.ParseIP("100.10.0.100"),
Protocol: layers.IPProtocolUDP,
}
udp := &layers.UDP{
SrcPort: 51334,
DstPort: 53,
}
if err := udp.SetNetworkLayerForChecksum(ipv4); err != nil {
t.Errorf("failed to set network layer for checksum: %v", err)
return
}
payload := gopacket.Payload([]byte("test"))
buf := gopacket.NewSerializeBuffer()
opts := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
if err = gopacket.SerializeLayers(buf, opts, ipv4, udp, payload); err != nil {
t.Errorf("failed to serialize packet: %v", err)
return
}
if m.dropFilter(buf.Bytes(), m.outgoingRules, false) {
t.Errorf("expected packet to be accepted")
return
}
if err = m.Reset(); err != nil {
t.Errorf("failed to reset Manager: %v", err)
return
}
}
// TestRemovePacketHook tests the functionality of the RemovePacketHook method
func TestRemovePacketHook(t *testing.T) {
// creating mock iface
iface := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error { return nil },
}
// creating manager instance
manager, err := Create(iface)
if err != nil {
t.Fatalf("Failed to create Manager: %s", err)
}
// Add a UDP packet hook
hookFunc := func(data []byte) bool { return true }
hookID := manager.AddUDPPacketHook(false, net.IPv4(192, 168, 0, 1), 8080, hookFunc)
// Assert the hook is added by finding it in the manager's outgoing rules
found := false
for _, rule := range manager.outgoingRules {
if rule.id == hookID {
found = true
break
}
}
if !found {
t.Fatalf("The hook was not added properly.")
}
// Now remove the packet hook
err = manager.RemovePacketHook(hookID)
if err != nil {
t.Fatalf("Failed to remove hook: %s", err)
}
// Assert the hook is removed by checking it in the manager's outgoing rules
for _, rule := range manager.outgoingRules {
if rule.id == hookID {
t.Fatalf("The hook was not removed properly.")
}
}
}
func TestUSPFilterCreatePerformance(t *testing.T) {
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
// just check on the local interface
ifaceMock := &IFaceMock{
SetFilterFunc: func(iface.PacketFilter) error { return nil },
}
manager, err := Create(ifaceMock)
require.NoError(t, err)
time.Sleep(time.Second)
defer func() {
if err := manager.Reset(); err != nil {
t.Errorf("clear the manager state: %v", err)
}
time.Sleep(time.Second)
}()
ip := net.ParseIP("10.20.0.100")
start := time.Now()
for i := 0; i < testMax; i++ {
port := &fw.Port{Values: []int{1000 + i}}
if i%2 == 0 {
_, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "accept HTTP traffic")
} else {
_, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionIN, fw.ActionAccept, "accept HTTP traffic")
}
require.NoError(t, err, "failed to add rule")
}
t.Logf("execution avg per rule: %s", time.Since(start)/time.Duration(testMax))
})
}
}

View File

@@ -1,413 +0,0 @@
package acl
import (
"crypto/md5"
"encoding/hex"
"fmt"
"net"
"strconv"
"sync"
"time"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/iface"
mgmProto "github.com/netbirdio/netbird/management/proto"
)
// IFaceMapper defines subset methods of interface required for manager
type IFaceMapper interface {
Name() string
Address() iface.WGAddress
IsUserspaceBind() bool
SetFilter(iface.PacketFilter) error
}
// Manager is a ACL rules manager
type Manager interface {
ApplyFiltering(networkMap *mgmProto.NetworkMap)
Stop()
}
// DefaultManager uses firewall manager to handle
type DefaultManager struct {
manager firewall.Manager
rulesPairs map[string][]firewall.Rule
mutex sync.Mutex
}
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
//
// If allowByDefault is ture it appends allow ALL traffic rules to input and output chains.
func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
d.mutex.Lock()
defer d.mutex.Unlock()
start := time.Now()
defer func() {
total := 0
for _, pairs := range d.rulesPairs {
total += len(pairs)
}
log.Infof(
"ACL rules processed in: %v, total rules count: %d",
time.Since(start), total)
}()
if d.manager == nil {
log.Debug("firewall manager is not supported, skipping firewall rules")
return
}
rules, squashedProtocols := d.squashAcceptRules(networkMap)
enableSSH := (networkMap.PeerConfig != nil &&
networkMap.PeerConfig.SshConfig != nil &&
networkMap.PeerConfig.SshConfig.SshEnabled)
if _, ok := squashedProtocols[mgmProto.FirewallRule_ALL]; ok {
enableSSH = enableSSH && !ok
}
if _, ok := squashedProtocols[mgmProto.FirewallRule_TCP]; ok {
enableSSH = enableSSH && !ok
}
// if TCP protocol rules not squashed and SSH enabled
// we add default firewall rule which accepts connection to any peer
// in the network by SSH (TCP 22 port).
if enableSSH {
rules = append(rules, &mgmProto.FirewallRule{
PeerIP: "0.0.0.0",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_TCP,
Port: strconv.Itoa(ssh.DefaultSSHPort),
})
}
// if we got empty rules list but management not set networkMap.FirewallRulesIsEmpty flag
// we have old version of management without rules handling, we should allow all traffic
if len(networkMap.FirewallRules) == 0 && !networkMap.FirewallRulesIsEmpty {
log.Warn("this peer is connected to a NetBird Management service with an older version. Allowing all traffic from connected peers")
rules = append(rules,
&mgmProto.FirewallRule{
PeerIP: "0.0.0.0",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
&mgmProto.FirewallRule{
PeerIP: "0.0.0.0",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
)
}
applyFailed := false
newRulePairs := make(map[string][]firewall.Rule)
for _, r := range rules {
pairID, rulePair, err := d.protoRuleToFirewallRule(r)
if err != nil {
log.Errorf("failed to apply firewall rule: %+v, %v", r, err)
applyFailed = true
break
}
newRulePairs[pairID] = rulePair
}
if applyFailed {
log.Error("failed to apply firewall rules, rollback ACL to previous state")
for _, rules := range newRulePairs {
for _, rule := range rules {
if err := d.manager.DeleteRule(rule); err != nil {
log.Errorf("failed to delete new firewall rule (id: %v) during rollback: %v", rule.GetRuleID(), err)
continue
}
}
}
return
}
for pairID, rules := range d.rulesPairs {
if _, ok := newRulePairs[pairID]; !ok {
for _, rule := range rules {
if err := d.manager.DeleteRule(rule); err != nil {
log.Errorf("failed to delete firewall rule: %v", err)
continue
}
}
delete(d.rulesPairs, pairID)
}
}
d.rulesPairs = newRulePairs
}
// Stop ACL controller and clear firewall state
func (d *DefaultManager) Stop() {
d.mutex.Lock()
defer d.mutex.Unlock()
if err := d.manager.Reset(); err != nil {
log.WithError(err).Error("reset firewall state")
}
}
func (d *DefaultManager) protoRuleToFirewallRule(r *mgmProto.FirewallRule) (string, []firewall.Rule, error) {
ip := net.ParseIP(r.PeerIP)
if ip == nil {
return "", nil, fmt.Errorf("invalid IP address, skipping firewall rule")
}
protocol := convertToFirewallProtocol(r.Protocol)
if protocol == firewall.ProtocolUnknown {
return "", nil, fmt.Errorf("invalid protocol type: %d, skipping firewall rule", r.Protocol)
}
action := convertFirewallAction(r.Action)
if action == firewall.ActionUnknown {
return "", nil, fmt.Errorf("invalid action type: %d, skipping firewall rule", r.Action)
}
var port *firewall.Port
if r.Port != "" {
value, err := strconv.Atoi(r.Port)
if err != nil {
return "", nil, fmt.Errorf("invalid port, skipping firewall rule")
}
port = &firewall.Port{
Values: []int{value},
}
}
ruleID := d.getRuleID(ip, protocol, int(r.Direction), port, action, "")
if rulesPair, ok := d.rulesPairs[ruleID]; ok {
return ruleID, rulesPair, nil
}
var rules []firewall.Rule
var err error
switch r.Direction {
case mgmProto.FirewallRule_IN:
rules, err = d.addInRules(ip, protocol, port, action, "")
case mgmProto.FirewallRule_OUT:
rules, err = d.addOutRules(ip, protocol, port, action, "")
default:
return "", nil, fmt.Errorf("invalid direction, skipping firewall rule")
}
if err != nil {
return "", nil, err
}
d.rulesPairs[ruleID] = rules
return ruleID, rules, nil
}
func (d *DefaultManager) addInRules(ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, comment string) ([]firewall.Rule, error) {
var rules []firewall.Rule
rule, err := d.manager.AddFiltering(ip, protocol, nil, port, firewall.RuleDirectionIN, action, comment)
if err != nil {
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
}
rules = append(rules, rule)
if shouldSkipInvertedRule(protocol, port) {
return rules, nil
}
rule, err = d.manager.AddFiltering(ip, protocol, port, nil, firewall.RuleDirectionOUT, action, comment)
if err != nil {
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
}
return append(rules, rule), nil
}
func (d *DefaultManager) addOutRules(ip net.IP, protocol firewall.Protocol, port *firewall.Port, action firewall.Action, comment string) ([]firewall.Rule, error) {
var rules []firewall.Rule
rule, err := d.manager.AddFiltering(ip, protocol, nil, port, firewall.RuleDirectionOUT, action, comment)
if err != nil {
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
}
rules = append(rules, rule)
if shouldSkipInvertedRule(protocol, port) {
return rules, nil
}
rule, err = d.manager.AddFiltering(ip, protocol, port, nil, firewall.RuleDirectionIN, action, comment)
if err != nil {
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
}
return append(rules, rule), nil
}
// getRuleID() returns unique ID for the rule based on its parameters.
func (d *DefaultManager) getRuleID(
ip net.IP,
proto firewall.Protocol,
direction int,
port *firewall.Port,
action firewall.Action,
comment string,
) string {
idStr := ip.String() + string(proto) + strconv.Itoa(direction) + strconv.Itoa(int(action)) + comment
if port != nil {
idStr += port.String()
}
return hex.EncodeToString(md5.New().Sum([]byte(idStr)))
}
// squashAcceptRules does complex logic to convert many rules which allows connection by traffic type
// to all peers in the network map to one rule which just accepts that type of the traffic.
//
// NOTE: It will not squash two rules for same protocol if one covers all peers in the network,
// but other has port definitions or has drop policy.
func (d *DefaultManager) squashAcceptRules(
networkMap *mgmProto.NetworkMap,
) ([]*mgmProto.FirewallRule, map[mgmProto.FirewallRuleProtocol]struct{}) {
totalIPs := 0
for _, p := range append(networkMap.RemotePeers, networkMap.OfflinePeers...) {
for range p.AllowedIps {
totalIPs++
}
}
type protoMatch map[mgmProto.FirewallRuleProtocol]map[string]int
in := protoMatch{}
out := protoMatch{}
// this function we use to do calculation, can we squash the rules by protocol or not.
// We summ amount of Peers IP for given protocol we found in original rules list.
// But we zeroed the IP's for protocol if:
// 1. Any of the rule has DROP action type.
// 2. Any of rule contains Port.
//
// We zeroed this to notify squash function that this protocol can't be squashed.
addRuleToCalculationMap := func(i int, r *mgmProto.FirewallRule, protocols protoMatch) {
drop := r.Action == mgmProto.FirewallRule_DROP || r.Port != ""
if drop {
protocols[r.Protocol] = map[string]int{}
return
}
if _, ok := protocols[r.Protocol]; !ok {
protocols[r.Protocol] = map[string]int{}
}
match := protocols[r.Protocol]
if _, ok := match[r.PeerIP]; ok {
return
}
match[r.PeerIP] = i
}
for i, r := range networkMap.FirewallRules {
// calculate squash for different directions
if r.Direction == mgmProto.FirewallRule_IN {
addRuleToCalculationMap(i, r, in)
} else {
addRuleToCalculationMap(i, r, out)
}
}
// order of squashing by protocol is important
// only for ther first element ALL, it must be done first
protocolOrders := []mgmProto.FirewallRuleProtocol{
mgmProto.FirewallRule_ALL,
mgmProto.FirewallRule_ICMP,
mgmProto.FirewallRule_TCP,
mgmProto.FirewallRule_UDP,
}
// trace which type of protocols was squashed
squashedRules := []*mgmProto.FirewallRule{}
squashedProtocols := map[mgmProto.FirewallRuleProtocol]struct{}{}
squash := func(matches protoMatch, direction mgmProto.FirewallRuleDirection) {
for _, protocol := range protocolOrders {
if ipset, ok := matches[protocol]; !ok || len(ipset) != totalIPs || len(ipset) < 2 {
// don't squash if :
// 1. Rules not cover all peers in the network
// 2. Rules cover only one peer in the network.
continue
}
// add special rule 0.0.0.0 which allows all IP's in our firewall implementations
squashedRules = append(squashedRules, &mgmProto.FirewallRule{
PeerIP: "0.0.0.0",
Direction: direction,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: protocol,
})
squashedProtocols[protocol] = struct{}{}
if protocol == mgmProto.FirewallRule_ALL {
// if we have ALL traffic type squashed rule
// it allows all other type of traffic, so we can stop processing
break
}
}
}
squash(in, mgmProto.FirewallRule_IN)
squash(out, mgmProto.FirewallRule_OUT)
// if all protocol was squashed everything is allow and we can ignore all other rules
if _, ok := squashedProtocols[mgmProto.FirewallRule_ALL]; ok {
return squashedRules, squashedProtocols
}
if len(squashedRules) == 0 {
return networkMap.FirewallRules, squashedProtocols
}
var rules []*mgmProto.FirewallRule
// filter out rules which was squashed from final list
// if we also have other not squashed rules.
for i, r := range networkMap.FirewallRules {
if _, ok := squashedProtocols[r.Protocol]; ok {
if m, ok := in[r.Protocol]; ok && m[r.PeerIP] == i {
continue
} else if m, ok := out[r.Protocol]; ok && m[r.PeerIP] == i {
continue
}
}
rules = append(rules, r)
}
return append(rules, squashedRules...), squashedProtocols
}
func convertToFirewallProtocol(protocol mgmProto.FirewallRuleProtocol) firewall.Protocol {
switch protocol {
case mgmProto.FirewallRule_TCP:
return firewall.ProtocolTCP
case mgmProto.FirewallRule_UDP:
return firewall.ProtocolUDP
case mgmProto.FirewallRule_ICMP:
return firewall.ProtocolICMP
case mgmProto.FirewallRule_ALL:
return firewall.ProtocolALL
default:
return firewall.ProtocolUnknown
}
}
func shouldSkipInvertedRule(protocol firewall.Protocol, port *firewall.Port) bool {
return protocol == firewall.ProtocolALL || protocol == firewall.ProtocolICMP || port == nil
}
func convertFirewallAction(action mgmProto.FirewallRuleAction) firewall.Action {
switch action {
case mgmProto.FirewallRule_ACCEPT:
return firewall.ActionAccept
case mgmProto.FirewallRule_DROP:
return firewall.ActionDrop
default:
return firewall.ActionUnknown
}
}

View File

@@ -1,27 +0,0 @@
//go:build !linux
package acl
import (
"fmt"
"runtime"
"github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/client/firewall/uspfilter"
)
// Create creates a firewall manager instance
func Create(iface IFaceMapper) (manager *DefaultManager, err error) {
if iface.IsUserspaceBind() {
// use userspace packet filtering firewall
fm, err := uspfilter.Create(iface)
if err != nil {
return nil, err
}
return &DefaultManager{
manager: fm,
rulesPairs: make(map[string][]firewall.Rule),
}, nil
}
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
}

View File

@@ -1,36 +0,0 @@
package acl
import (
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/firewall"
"github.com/netbirdio/netbird/client/firewall/iptables"
"github.com/netbirdio/netbird/client/firewall/nftables"
"github.com/netbirdio/netbird/client/firewall/uspfilter"
)
// Create creates a firewall manager instance for the Linux
func Create(iface IFaceMapper) (manager *DefaultManager, err error) {
var fm firewall.Manager
if iface.IsUserspaceBind() {
// use userspace packet filtering firewall
if fm, err = uspfilter.Create(iface); err != nil {
log.Debugf("failed to create userspace filtering firewall: %s", err)
return nil, err
}
} else {
if fm, err = nftables.Create(iface); err != nil {
log.Debugf("failed to create nftables manager: %s", err)
// fallback to iptables
if fm, err = iptables.Create(iface); err != nil {
log.Errorf("failed to create iptables manager: %s", err)
return nil, err
}
}
}
return &DefaultManager{
manager: fm,
rulesPairs: make(map[string][]firewall.Rule),
}, nil
}

View File

@@ -1,333 +0,0 @@
package acl
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/netbirdio/netbird/client/internal/acl/mocks"
mgmProto "github.com/netbirdio/netbird/management/proto"
)
func TestDefaultManager(t *testing.T) {
networkMap := &mgmProto.NetworkMap{
FirewallRules: []*mgmProto.FirewallRule{
{
PeerIP: "10.93.0.1",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_TCP,
Port: "80",
},
{
PeerIP: "10.93.0.2",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_DROP,
Protocol: mgmProto.FirewallRule_UDP,
Port: "53",
},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
iface := mocks.NewMockIFaceMapper(ctrl)
iface.EXPECT().IsUserspaceBind().Return(true)
// iface.EXPECT().Name().Return("lo")
iface.EXPECT().SetFilter(gomock.Any())
// we receive one rule from the management so for testing purposes ignore it
acl, err := Create(iface)
if err != nil {
t.Errorf("create ACL manager: %v", err)
return
}
defer acl.Stop()
t.Run("apply firewall rules", func(t *testing.T) {
acl.ApplyFiltering(networkMap)
if len(acl.rulesPairs) != 2 {
t.Errorf("firewall rules not applied: %v", acl.rulesPairs)
return
}
})
t.Run("add extra rules", func(t *testing.T) {
existedPairs := map[string]struct{}{}
for id := range acl.rulesPairs {
existedPairs[id] = struct{}{}
}
// remove first rule
networkMap.FirewallRules = networkMap.FirewallRules[1:]
networkMap.FirewallRules = append(
networkMap.FirewallRules,
&mgmProto.FirewallRule{
PeerIP: "10.93.0.3",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_DROP,
Protocol: mgmProto.FirewallRule_ICMP,
},
)
acl.ApplyFiltering(networkMap)
// we should have one old and one new rule in the existed rules
if len(acl.rulesPairs) != 2 {
t.Errorf("firewall rules not applied")
return
}
// check that old rule was removed
previousCount := 0
for id := range acl.rulesPairs {
if _, ok := existedPairs[id]; ok {
previousCount++
}
}
if previousCount != 1 {
t.Errorf("old rule was not removed")
}
})
t.Run("handle default rules", func(t *testing.T) {
networkMap.FirewallRules = networkMap.FirewallRules[:0]
networkMap.FirewallRulesIsEmpty = true
if acl.ApplyFiltering(networkMap); len(acl.rulesPairs) != 0 {
t.Errorf("rules should be empty if FirewallRulesIsEmpty is set, got: %v", len(acl.rulesPairs))
return
}
networkMap.FirewallRulesIsEmpty = false
acl.ApplyFiltering(networkMap)
if len(acl.rulesPairs) != 2 {
t.Errorf("rules should contain 2 rules if FirewallRulesIsEmpty is not set, got: %v", len(acl.rulesPairs))
return
}
})
}
func TestDefaultManagerSquashRules(t *testing.T) {
networkMap := &mgmProto.NetworkMap{
RemotePeers: []*mgmProto.RemotePeerConfig{
{AllowedIps: []string{"10.93.0.1"}},
{AllowedIps: []string{"10.93.0.2"}},
{AllowedIps: []string{"10.93.0.3"}},
{AllowedIps: []string{"10.93.0.4"}},
},
FirewallRules: []*mgmProto.FirewallRule{
{
PeerIP: "10.93.0.1",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.2",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.3",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.4",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.1",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.2",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.3",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.4",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
},
}
manager := &DefaultManager{}
rules, _ := manager.squashAcceptRules(networkMap)
if len(rules) != 2 {
t.Errorf("rules should contain 2, got: %v", rules)
return
}
r := rules[0]
if r.PeerIP != "0.0.0.0" {
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
return
} else if r.Direction != mgmProto.FirewallRule_IN {
t.Errorf("direction should be IN, got: %v", r.Direction)
return
} else if r.Protocol != mgmProto.FirewallRule_ALL {
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
return
} else if r.Action != mgmProto.FirewallRule_ACCEPT {
t.Errorf("action should be ACCEPT, got: %v", r.Action)
return
}
r = rules[1]
if r.PeerIP != "0.0.0.0" {
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
return
} else if r.Direction != mgmProto.FirewallRule_OUT {
t.Errorf("direction should be OUT, got: %v", r.Direction)
return
} else if r.Protocol != mgmProto.FirewallRule_ALL {
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
return
} else if r.Action != mgmProto.FirewallRule_ACCEPT {
t.Errorf("action should be ACCEPT, got: %v", r.Action)
return
}
}
func TestDefaultManagerSquashRulesNoAffect(t *testing.T) {
networkMap := &mgmProto.NetworkMap{
RemotePeers: []*mgmProto.RemotePeerConfig{
{AllowedIps: []string{"10.93.0.1"}},
{AllowedIps: []string{"10.93.0.2"}},
{AllowedIps: []string{"10.93.0.3"}},
{AllowedIps: []string{"10.93.0.4"}},
},
FirewallRules: []*mgmProto.FirewallRule{
{
PeerIP: "10.93.0.1",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.2",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.3",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.4",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_TCP,
},
{
PeerIP: "10.93.0.1",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.2",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.3",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_ALL,
},
{
PeerIP: "10.93.0.4",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_UDP,
},
},
}
manager := &DefaultManager{}
if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) {
t.Errorf("we should got same amount of rules as intput, got %v", len(rules))
}
}
func TestDefaultManagerEnableSSHRules(t *testing.T) {
networkMap := &mgmProto.NetworkMap{
PeerConfig: &mgmProto.PeerConfig{
SshConfig: &mgmProto.SSHConfig{
SshEnabled: true,
},
},
RemotePeers: []*mgmProto.RemotePeerConfig{
{AllowedIps: []string{"10.93.0.1"}},
{AllowedIps: []string{"10.93.0.2"}},
{AllowedIps: []string{"10.93.0.3"}},
},
FirewallRules: []*mgmProto.FirewallRule{
{
PeerIP: "10.93.0.1",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_TCP,
},
{
PeerIP: "10.93.0.2",
Direction: mgmProto.FirewallRule_IN,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_TCP,
},
{
PeerIP: "10.93.0.3",
Direction: mgmProto.FirewallRule_OUT,
Action: mgmProto.FirewallRule_ACCEPT,
Protocol: mgmProto.FirewallRule_UDP,
},
},
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
iface := mocks.NewMockIFaceMapper(ctrl)
iface.EXPECT().IsUserspaceBind().Return(true)
// iface.EXPECT().Name().Return("lo")
iface.EXPECT().SetFilter(gomock.Any())
// we receive one rule from the management so for testing purposes ignore it
acl, err := Create(iface)
if err != nil {
t.Errorf("create ACL manager: %v", err)
return
}
defer acl.Stop()
acl.ApplyFiltering(networkMap)
if len(acl.rulesPairs) != 4 {
t.Errorf("expect 4 rules (last must be SSH), got: %d", len(acl.rulesPairs))
return
}
}

View File

@@ -1,7 +0,0 @@
## Mocks
To generate (or refresh) mocks from acl package please install [mockgen](https://github.com/golang/mock).
Run this command from the `./client/internal/acl` folder to update iface mapper interface mock:
```bash
mockgen -destination mocks/iface_mapper.go -package mocks . IFaceMapper
```

View File

@@ -1,91 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/netbirdio/netbird/client/internal/acl (interfaces: IFaceMapper)
// Package mocks is a generated GoMock package.
package mocks
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
iface "github.com/netbirdio/netbird/iface"
)
// MockIFaceMapper is a mock of IFaceMapper interface.
type MockIFaceMapper struct {
ctrl *gomock.Controller
recorder *MockIFaceMapperMockRecorder
}
// MockIFaceMapperMockRecorder is the mock recorder for MockIFaceMapper.
type MockIFaceMapperMockRecorder struct {
mock *MockIFaceMapper
}
// NewMockIFaceMapper creates a new mock instance.
func NewMockIFaceMapper(ctrl *gomock.Controller) *MockIFaceMapper {
mock := &MockIFaceMapper{ctrl: ctrl}
mock.recorder = &MockIFaceMapperMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockIFaceMapper) EXPECT() *MockIFaceMapperMockRecorder {
return m.recorder
}
// Address mocks base method.
func (m *MockIFaceMapper) Address() iface.WGAddress {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Address")
ret0, _ := ret[0].(iface.WGAddress)
return ret0
}
// Address indicates an expected call of Address.
func (mr *MockIFaceMapperMockRecorder) Address() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Address", reflect.TypeOf((*MockIFaceMapper)(nil).Address))
}
// IsUserspaceBind mocks base method.
func (m *MockIFaceMapper) IsUserspaceBind() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsUserspaceBind")
ret0, _ := ret[0].(bool)
return ret0
}
// IsUserspaceBind indicates an expected call of IsUserspaceBind.
func (mr *MockIFaceMapperMockRecorder) IsUserspaceBind() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsUserspaceBind", reflect.TypeOf((*MockIFaceMapper)(nil).IsUserspaceBind))
}
// Name mocks base method.
func (m *MockIFaceMapper) Name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Name")
ret0, _ := ret[0].(string)
return ret0
}
// Name indicates an expected call of Name.
func (mr *MockIFaceMapperMockRecorder) Name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockIFaceMapper)(nil).Name))
}
// SetFilter mocks base method.
func (m *MockIFaceMapper) SetFilter(arg0 iface.PacketFilter) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetFilter", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetFilter indicates an expected call of SetFilter.
func (mr *MockIFaceMapperMockRecorder) SetFilter(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFilter", reflect.TypeOf((*MockIFaceMapper)(nil).SetFilter), arg0)
}

View File

@@ -13,7 +13,6 @@ import (
gstatus "google.golang.org/grpc/status"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/client/system"
@@ -24,7 +23,7 @@ import (
)
// RunClient with main logic.
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, routeListener routemanager.RouteListener) error {
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) error {
backOff := &backoff.ExponentialBackOff{
InitialInterval: time.Second,
RandomizationFactor: 1,
@@ -145,20 +144,13 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
peerConfig := loginResp.GetPeerConfig()
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig)
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig, tunAdapter, iFaceDiscover)
if err != nil {
log.Error(err)
return wrapErr(err)
}
// in case of non Android os these variables will be nil
md := MobileDependency{
TunAdapter: tunAdapter,
IFaceDiscover: iFaceDiscover,
RouteListener: routeListener,
}
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, md, statusRecorder)
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, statusRecorder)
err = engine.Start()
if err != nil {
log.Errorf("error while starting Netbird Connection Engine: %s", err)
@@ -202,10 +194,13 @@ func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
}
// createEngineConfig converts configuration received from Management Service to EngineConfig
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*EngineConfig, error) {
engineConf := &EngineConfig{
WgIfaceName: config.WgIface,
WgAddr: peerConfig.Address,
TunAdapter: tunAdapter,
IFaceDiscover: iFaceDiscover,
IFaceBlackList: config.IFaceBlackList,
DisableIPv6Discovery: config.DisableIPv6Discovery,
WgPrivateKey: key,

View File

@@ -1,5 +1,3 @@
//go:build !android
package dns
import (

View File

@@ -1,5 +1,3 @@
//go:build !android
package dns
import (
@@ -34,10 +32,6 @@ func newFileConfigurator() (hostManager, error) {
return &fileConfigurator{}, nil
}
func (f *fileConfigurator) supportCustomPort() bool {
return false
}
func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
backupFileExist := false
_, err := os.Stat(fileDefaultResolvConfBackupLocation)

View File

@@ -10,7 +10,6 @@ import (
type hostManager interface {
applyDNSConfig(config hostDNSConfig) error
restoreHostDNS() error
supportCustomPort() bool
}
type hostDNSConfig struct {
@@ -27,9 +26,8 @@ type domainConfig struct {
}
type mockHostConfigurator struct {
applyDNSConfigFunc func(config hostDNSConfig) error
restoreHostDNSFunc func() error
supportCustomPortFunc func() bool
applyDNSConfigFunc func(config hostDNSConfig) error
restoreHostDNSFunc func() error
}
func (m *mockHostConfigurator) applyDNSConfig(config hostDNSConfig) error {
@@ -46,18 +44,10 @@ func (m *mockHostConfigurator) restoreHostDNS() error {
return fmt.Errorf("method restoreHostDNS is not implemented")
}
func (m *mockHostConfigurator) supportCustomPort() bool {
if m.supportCustomPortFunc != nil {
return m.supportCustomPortFunc()
}
return false
}
func newNoopHostMocker() hostManager {
return &mockHostConfigurator{
applyDNSConfigFunc: func(config hostDNSConfig) error { return nil },
restoreHostDNSFunc: func() error { return nil },
supportCustomPortFunc: func() bool { return true },
applyDNSConfigFunc: func(config hostDNSConfig) error { return nil },
restoreHostDNSFunc: func() error { return nil },
}
}

View File

@@ -1,24 +0,0 @@
package dns
import (
"github.com/netbirdio/netbird/iface"
)
type androidHostManager struct {
}
func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
return &androidHostManager{}, nil
}
func (a androidHostManager) applyDNSConfig(config hostDNSConfig) error {
return nil
}
func (a androidHostManager) restoreHostDNS() error {
return nil
}
func (a androidHostManager) supportCustomPort() bool {
return false
}

View File

@@ -8,9 +8,8 @@ import (
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
@@ -40,10 +39,6 @@ func newHostManager(_ *iface.WGIface) (hostManager, error) {
}, nil
}
func (s *systemConfigurator) supportCustomPort() bool {
return true
}
func (s *systemConfigurator) applyDNSConfig(config hostDNSConfig) error {
var err error

View File

@@ -1,5 +1,3 @@
//go:build !android
package dns
import (

View File

@@ -4,10 +4,9 @@ import (
"fmt"
"strings"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows/registry"
"github.com/netbirdio/netbird/iface"
)
const (
@@ -43,10 +42,6 @@ func newHostManager(wgInterface *iface.WGIface) (hostManager, error) {
}, nil
}
func (s *registryConfigurator) supportCustomPort() bool {
return false
}
func (r *registryConfigurator) applyDNSConfig(config hostDNSConfig) error {
var err error
if config.routeAll {

View File

@@ -2,12 +2,10 @@ package dns
import (
"fmt"
"sync"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns"
log "github.com/sirupsen/logrus"
"sync"
)
type registrationMap map[string]struct{}
@@ -17,12 +15,9 @@ type localResolver struct {
records sync.Map
}
func (d *localResolver) stop() {
}
// ServeDNS handles a DNS request
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
log.Tracef("received question: %#v", r.Question[0])
log.Tracef("received question: %#v\n", r.Question[0])
replyMessage := &dns.Msg{}
replyMessage.SetReply(r)
replyMessage.RecursionAvailable = true

View File

@@ -7,17 +7,16 @@ import (
// MockServer is the mock instance of a dns server
type MockServer struct {
InitializeFunc func() error
StartFunc func()
StopFunc func()
UpdateDNSServerFunc func(serial uint64, update nbdns.Config) error
}
// Initialize mock implementation of Initialize from Server interface
func (m *MockServer) Initialize() error {
if m.InitializeFunc != nil {
return m.InitializeFunc()
// Start mock implementation of Start from Server interface
func (m *MockServer) Start() {
if m.StartFunc != nil {
m.StartFunc()
}
return nil
}
// Stop mock implementation of Stop from Server interface
@@ -27,10 +26,6 @@ func (m *MockServer) Stop() {
}
}
func (m *MockServer) DnsIP() string {
return ""
}
// UpdateDNSServer mock implementation of UpdateDNSServer from Server interface
func (m *MockServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
if m.UpdateDNSServerFunc != nil {

View File

@@ -1,5 +1,3 @@
//go:build !android
package dns
import (
@@ -13,9 +11,8 @@ import (
"github.com/godbus/dbus/v5"
"github.com/hashicorp/go-version"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
@@ -91,10 +88,6 @@ func newNetworkManagerDbusConfigurator(wgInterface *iface.WGIface) (hostManager,
}, nil
}
func (n *networkManagerDbusConfigurator) supportCustomPort() bool {
return false
}
func (n *networkManagerDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
connSettings, configVersion, err := n.getAppliedConnectionSettings()
if err != nil {

View File

@@ -1,5 +1,3 @@
//go:build !android
package dns
import (
@@ -7,9 +5,8 @@ import (
"os/exec"
"strings"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const resolvconfCommand = "resolvconf"
@@ -24,10 +21,6 @@ func newResolvConfConfigurator(wgInterface *iface.WGIface) (hostManager, error)
}, nil
}
func (r *resolvconf) supportCustomPort() bool {
return false
}
func (r *resolvconf) applyDNSConfig(config hostDNSConfig) error {
var err error
if !config.routeAll {

View File

@@ -1,103 +0,0 @@
package dns
import (
"fmt"
"net"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/miekg/dns"
"golang.zx2c4.com/wireguard/tun"
)
type responseWriter struct {
local net.Addr
remote net.Addr
packet gopacket.Packet
device tun.Device
}
// LocalAddr returns the net.Addr of the server
func (r *responseWriter) LocalAddr() net.Addr {
return r.local
}
// RemoteAddr returns the net.Addr of the client that sent the current request.
func (r *responseWriter) RemoteAddr() net.Addr {
return r.remote
}
// WriteMsg writes a reply back to the client.
func (r *responseWriter) WriteMsg(msg *dns.Msg) error {
buff, err := msg.Pack()
if err != nil {
return err
}
_, err = r.Write(buff)
return err
}
// Write writes a raw buffer back to the client.
func (r *responseWriter) Write(data []byte) (int, error) {
var ip gopacket.SerializableLayer
// Get the UDP layer
udpLayer := r.packet.Layer(layers.LayerTypeUDP)
udp := udpLayer.(*layers.UDP)
// Swap the source and destination addresses for the response
udp.SrcPort, udp.DstPort = udp.DstPort, udp.SrcPort
// Check if it's an IPv4 packet
if ipv4Layer := r.packet.Layer(layers.LayerTypeIPv4); ipv4Layer != nil {
ipv4 := ipv4Layer.(*layers.IPv4)
ipv4.SrcIP, ipv4.DstIP = ipv4.DstIP, ipv4.SrcIP
ip = ipv4
} else if ipv6Layer := r.packet.Layer(layers.LayerTypeIPv6); ipv6Layer != nil {
ipv6 := ipv6Layer.(*layers.IPv6)
ipv6.SrcIP, ipv6.DstIP = ipv6.DstIP, ipv6.SrcIP
ip = ipv6
}
if err := udp.SetNetworkLayerForChecksum(ip.(gopacket.NetworkLayer)); err != nil {
return 0, fmt.Errorf("failed to set network layer for checksum: %v", err)
}
// Serialize the packet
buffer := gopacket.NewSerializeBuffer()
options := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
payload := gopacket.Payload(data)
err := gopacket.SerializeLayers(buffer, options, ip, udp, payload)
if err != nil {
return 0, fmt.Errorf("failed to serialize packet: %v", err)
}
send := buffer.Bytes()
sendBuffer := make([]byte, 40, len(send)+40)
sendBuffer = append(sendBuffer, send...)
return r.device.Write([][]byte{sendBuffer}, 40)
}
// Close closes the connection.
func (r *responseWriter) Close() error {
return nil
}
// TsigStatus returns the status of the Tsig.
func (r *responseWriter) TsigStatus() error {
return nil
}
// TsigTimersOnly sets the tsig timers only boolean.
func (r *responseWriter) TsigTimersOnly(bool) {
}
// Hijack lets the caller take over the connection.
// After a call to Hijack(), the DNS package will not do anything with the connection.
func (r *responseWriter) Hijack() {
}

View File

@@ -1,93 +0,0 @@
package dns
import (
"net"
"testing"
"github.com/golang/mock/gomock"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/iface/mocks"
)
func TestResponseWriterLocalAddr(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
device := mocks.NewMockDevice(ctrl)
device.EXPECT().Write(gomock.Any(), gomock.Any())
request := &dns.Msg{
Question: []dns.Question{{
Name: "google.com.",
Qtype: dns.TypeA,
Qclass: dns.TypeA,
}},
}
replyMessage := &dns.Msg{}
replyMessage.SetReply(request)
replyMessage.RecursionAvailable = true
replyMessage.Rcode = dns.RcodeSuccess
replyMessage.Answer = []dns.RR{
&dns.A{
A: net.IPv4(8, 8, 8, 8),
},
}
ipv4 := &layers.IPv4{
Protocol: layers.IPProtocolUDP,
SrcIP: net.IPv4(127, 0, 0, 1),
DstIP: net.IPv4(127, 0, 0, 2),
}
udp := &layers.UDP{
DstPort: 53,
SrcPort: 45223,
}
if err := udp.SetNetworkLayerForChecksum(ipv4); err != nil {
t.Error("failed to set network layer for checksum")
return
}
// Serialize the packet
buffer := gopacket.NewSerializeBuffer()
options := gopacket.SerializeOptions{
ComputeChecksums: true,
FixLengths: true,
}
requestData, err := request.Pack()
if err != nil {
t.Errorf("got an error while packing the request message, error: %v", err)
return
}
payload := gopacket.Payload(requestData)
if err := gopacket.SerializeLayers(buffer, options, ipv4, udp, payload); err != nil {
t.Errorf("failed to serialize packet: %v", err)
return
}
rw := &responseWriter{
local: &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 55223,
},
remote: &net.UDPAddr{
IP: net.IPv4(127, 0, 0, 1),
Port: 53,
},
packet: gopacket.NewPacket(
buffer.Bytes(),
layers.LayerTypeIPv4,
gopacket.Default,
),
device: device,
}
if err := rw.WriteMsg(replyMessage); err != nil {
t.Errorf("got an error while writing the local resolver response, error: %v", err)
return
}
}

View File

@@ -1,615 +1,10 @@
package dns
import (
"context"
"fmt"
"math/big"
"net"
"net/netip"
"runtime"
"sync"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/miekg/dns"
"github.com/mitchellh/hashstructure/v2"
log "github.com/sirupsen/logrus"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
)
const (
defaultPort = 53
customPort = 5053
defaultIP = "127.0.0.1"
customIP = "127.0.0.153"
)
import nbdns "github.com/netbirdio/netbird/dns"
// Server is a dns server interface
type Server interface {
Initialize() error
Start()
Stop()
DnsIP() string
UpdateDNSServer(serial uint64, update nbdns.Config) error
}
type registeredHandlerMap map[string]handlerWithStop
// DefaultServer dns server object
type DefaultServer struct {
ctx context.Context
ctxCancel context.CancelFunc
mux sync.Mutex
udpFilterHookID string
server *dns.Server
dnsMux *dns.ServeMux
dnsMuxMap registeredHandlerMap
localResolver *localResolver
wgInterface *iface.WGIface
hostManager hostManager
updateSerial uint64
listenerIsRunning bool
runtimePort int
runtimeIP string
previousConfigHash uint64
currentConfig hostDNSConfig
customAddress *netip.AddrPort
enabled bool
}
type handlerWithStop interface {
dns.Handler
stop()
}
type muxUpdate struct {
domain string
handler handlerWithStop
}
// NewDefaultServer returns a new dns server
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string, initialDnsCfg *nbdns.Config) (*DefaultServer, error) {
mux := dns.NewServeMux()
var addrPort *netip.AddrPort
if customAddress != "" {
parsedAddrPort, err := netip.ParseAddrPort(customAddress)
if err != nil {
return nil, fmt.Errorf("unable to parse the custom dns address, got error: %s", err)
}
addrPort = &parsedAddrPort
}
ctx, stop := context.WithCancel(ctx)
defaultServer := &DefaultServer{
ctx: ctx,
ctxCancel: stop,
server: &dns.Server{
Net: "udp",
Handler: mux,
UDPSize: 65535,
},
dnsMux: mux,
dnsMuxMap: make(registeredHandlerMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
wgInterface: wgInterface,
customAddress: addrPort,
}
if initialDnsCfg != nil {
defaultServer.enabled = hasValidDnsServer(initialDnsCfg)
}
if wgInterface.IsUserspaceBind() {
defaultServer.evelRuntimeAddressForUserspace()
}
return defaultServer, nil
}
// Initialize instantiate host manager. It required to be initialized wginterface
func (s *DefaultServer) Initialize() (err error) {
s.mux.Lock()
defer s.mux.Unlock()
if s.hostManager != nil {
return nil
}
if !s.wgInterface.IsUserspaceBind() {
s.evalRuntimeAddress()
}
s.hostManager, err = newHostManager(s.wgInterface)
return
}
// listen runs the listener in a go routine
func (s *DefaultServer) listen() {
// nil check required in unit tests
if s.wgInterface != nil && s.wgInterface.IsUserspaceBind() {
s.udpFilterHookID = s.filterDNSTraffic()
s.setListenerStatus(true)
return
}
log.Debugf("starting dns on %s", s.server.Addr)
go func() {
s.setListenerStatus(true)
defer s.setListenerStatus(false)
err := s.server.ListenAndServe()
if err != nil {
log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.runtimePort, err)
}
}()
}
// DnsIP returns the DNS resolver server IP address
//
// When kernel space interface used it return real DNS server listener IP address
// For bind interface, fake DNS resolver address returned (second last IP address from Nebird network)
func (s *DefaultServer) DnsIP() string {
if !s.enabled {
return ""
}
return s.runtimeIP
}
func (s *DefaultServer) getFirstListenerAvailable() (string, int, error) {
ips := []string{defaultIP, customIP}
if runtime.GOOS != "darwin" && s.wgInterface != nil {
ips = append([]string{s.wgInterface.Address().IP.String()}, ips...)
}
ports := []int{defaultPort, customPort}
for _, port := range ports {
for _, ip := range ips {
addrString := fmt.Sprintf("%s:%d", ip, port)
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString))
probeListener, err := net.ListenUDP("udp", udpAddr)
if err == nil {
err = probeListener.Close()
if err != nil {
log.Errorf("got an error closing the probe listener, error: %s", err)
}
return ip, port, nil
}
log.Warnf("binding dns on %s is not available, error: %s", addrString, err)
}
}
return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports)
}
func (s *DefaultServer) setListenerStatus(running bool) {
s.listenerIsRunning = running
}
// Stop stops the server
func (s *DefaultServer) Stop() {
s.mux.Lock()
defer s.mux.Unlock()
s.ctxCancel()
if s.hostManager != nil {
err := s.hostManager.restoreHostDNS()
if err != nil {
log.Error(err)
}
}
err := s.stopListener()
if err != nil {
log.Error(err)
}
}
func (s *DefaultServer) stopListener() error {
if s.wgInterface != nil && s.wgInterface.IsUserspaceBind() && s.listenerIsRunning {
// udpFilterHookID here empty only in the unit tests
if filter := s.wgInterface.GetFilter(); filter != nil && s.udpFilterHookID != "" {
if err := filter.RemovePacketHook(s.udpFilterHookID); err != nil {
log.Errorf("unable to remove DNS packet hook: %s", err)
}
}
s.udpFilterHookID = ""
s.listenerIsRunning = false
return nil
}
if !s.listenerIsRunning {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := s.server.ShutdownContext(ctx)
if err != nil {
return fmt.Errorf("stopping dns server listener returned an error: %v", err)
}
return nil
}
// UpdateDNSServer processes an update received from the management service
func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
select {
case <-s.ctx.Done():
log.Infof("not updating DNS server as context is closed")
return s.ctx.Err()
default:
if serial < s.updateSerial {
return fmt.Errorf("not applying dns update, error: "+
"network update is %d behind the last applied update", s.updateSerial-serial)
}
s.mux.Lock()
defer s.mux.Unlock()
if s.hostManager == nil {
return fmt.Errorf("dns service is not initialized yet")
}
hash, err := hashstructure.Hash(update, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
SlicesAsSets: true,
UseStringer: true,
})
if err != nil {
log.Errorf("unable to hash the dns configuration update, got error: %s", err)
}
if s.previousConfigHash == hash {
log.Debugf("not applying the dns configuration update as there is nothing new")
s.updateSerial = serial
return nil
}
if err := s.applyConfiguration(update); err != nil {
return err
}
s.updateSerial = serial
s.previousConfigHash = hash
return nil
}
}
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
// is the service should be disabled, we stop the listener or fake resolver
// and proceed with a regular update to clean up the handlers and records
if !update.ServiceEnable {
if err := s.stopListener(); err != nil {
log.Error(err)
}
} else if !s.listenerIsRunning {
s.listen()
}
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...)
s.updateMux(muxUpdates)
s.updateLocalResolver(localRecords)
s.currentConfig = dnsConfigToHostDNSConfig(update, s.runtimeIP, s.runtimePort)
hostUpdate := s.currentConfig
if s.runtimePort != defaultPort && !s.hostManager.supportCustomPort() {
log.Warnf("the DNS manager of this peer doesn't support custom port. Disabling primary DNS setup. " +
"Learn more at: https://netbird.io/docs/how-to-guides/nameservers#local-resolver")
hostUpdate.routeAll = false
}
if err = s.hostManager.applyDNSConfig(hostUpdate); err != nil {
log.Error(err)
}
return nil
}
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) {
var muxUpdates []muxUpdate
localRecords := make(map[string]nbdns.SimpleRecord, 0)
for _, customZone := range customZones {
if len(customZone.Records) == 0 {
return nil, nil, fmt.Errorf("received an empty list of records")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: customZone.Domain,
handler: s.localResolver,
})
for _, record := range customZone.Records {
var class uint16 = dns.ClassINET
if record.Class != nbdns.DefaultClass {
return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class)
}
key := buildRecordKey(record.Name, class, uint16(record.Type))
localRecords[key] = record
}
}
return muxUpdates, localRecords, nil
}
func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) {
var muxUpdates []muxUpdate
for _, nsGroup := range nameServerGroups {
if len(nsGroup.NameServers) == 0 {
log.Warn("received a nameserver group with empty nameserver list")
continue
}
handler := newUpstreamResolver(s.ctx)
for _, ns := range nsGroup.NameServers {
if ns.NSType != nbdns.UDPNameServerType {
log.Warnf("skiping nameserver %s with type %s, this peer supports only %s",
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
continue
}
handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns))
}
if len(handler.upstreamServers) == 0 {
handler.stop()
log.Errorf("received a nameserver group with an invalid nameserver list")
continue
}
// when upstream fails to resolve domain several times over all it servers
// it will calls this hook to exclude self from the configuration and
// reapply DNS settings, but it not touch the original configuration and serial number
// because it is temporal deactivation until next try
//
// after some period defined by upstream it trys to reactivate self by calling this hook
// everything we need here is just to re-apply current configuration because it already
// contains this upstream settings (temporal deactivation not removed it)
handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler)
if nsGroup.Primary {
muxUpdates = append(muxUpdates, muxUpdate{
domain: nbdns.RootZone,
handler: handler,
})
continue
}
if len(nsGroup.Domains) == 0 {
handler.stop()
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
}
for _, domain := range nsGroup.Domains {
if domain == "" {
handler.stop()
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: domain,
handler: handler,
})
}
}
return muxUpdates, nil
}
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
muxUpdateMap := make(registeredHandlerMap)
for _, update := range muxUpdates {
s.registerMux(update.domain, update.handler)
muxUpdateMap[update.domain] = update.handler
if existingHandler, ok := s.dnsMuxMap[update.domain]; ok {
existingHandler.stop()
}
}
for key, existingHandler := range s.dnsMuxMap {
_, found := muxUpdateMap[key]
if !found {
existingHandler.stop()
s.deregisterMux(key)
}
}
s.dnsMuxMap = muxUpdateMap
}
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
for key := range s.localResolver.registeredMap {
_, found := update[key]
if !found {
s.localResolver.deleteRecord(key)
}
}
updatedMap := make(registrationMap)
for key, record := range update {
err := s.localResolver.registerRecord(record)
if err != nil {
log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
}
updatedMap[key] = struct{}{}
}
s.localResolver.registeredMap = updatedMap
}
func getNSHostPort(ns nbdns.NameServer) string {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
}
func (s *DefaultServer) registerMux(pattern string, handler dns.Handler) {
s.dnsMux.Handle(pattern, handler)
}
func (s *DefaultServer) deregisterMux(pattern string) {
s.dnsMux.HandleRemove(pattern)
}
// upstreamCallbacks returns two functions, the first one is used to deactivate
// the upstream resolver from the configuration, the second one is used to
// reactivate it. Not allowed to call reactivate before deactivate.
func (s *DefaultServer) upstreamCallbacks(
nsGroup *nbdns.NameServerGroup,
handler dns.Handler,
) (deactivate func(), reactivate func()) {
var removeIndex map[string]int
deactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
l := log.WithField("nameservers", nsGroup.NameServers)
l.Info("temporary deactivate nameservers group due timeout")
removeIndex = make(map[string]int)
for _, domain := range nsGroup.Domains {
removeIndex[domain] = -1
}
if nsGroup.Primary {
removeIndex[nbdns.RootZone] = -1
s.currentConfig.routeAll = false
}
for i, item := range s.currentConfig.domains {
if _, found := removeIndex[item.domain]; found {
s.currentConfig.domains[i].disabled = true
s.deregisterMux(item.domain)
removeIndex[item.domain] = i
}
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("fail to apply nameserver deactivation on the host")
}
}
reactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
for domain, i := range removeIndex {
if i == -1 || i >= len(s.currentConfig.domains) || s.currentConfig.domains[i].domain != domain {
continue
}
s.currentConfig.domains[i].disabled = false
s.registerMux(domain, handler)
}
l := log.WithField("nameservers", nsGroup.NameServers)
l.Debug("reactivate temporary disabled nameserver group")
if nsGroup.Primary {
s.currentConfig.routeAll = true
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply")
}
}
return
}
func (s *DefaultServer) filterDNSTraffic() string {
filter := s.wgInterface.GetFilter()
if filter == nil {
log.Error("can't set DNS filter, filter not initialized")
return ""
}
firstLayerDecoder := layers.LayerTypeIPv4
if s.wgInterface.Address().Network.IP.To4() == nil {
firstLayerDecoder = layers.LayerTypeIPv6
}
hook := func(packetData []byte) bool {
// Decode the packet
packet := gopacket.NewPacket(packetData, firstLayerDecoder, gopacket.Default)
// Get the UDP layer
udpLayer := packet.Layer(layers.LayerTypeUDP)
udp := udpLayer.(*layers.UDP)
msg := new(dns.Msg)
if err := msg.Unpack(udp.Payload); err != nil {
log.Tracef("parse DNS request: %v", err)
return true
}
writer := responseWriter{
packet: packet,
device: s.wgInterface.GetDevice().Device,
}
go s.dnsMux.ServeDNS(&writer, msg)
return true
}
return filter.AddUDPPacketHook(false, net.ParseIP(s.runtimeIP), uint16(s.runtimePort), hook)
}
func (s *DefaultServer) evelRuntimeAddressForUserspace() {
s.runtimeIP = getLastIPFromNetwork(s.wgInterface.Address().Network, 1)
s.runtimePort = defaultPort
s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort)
}
func (s *DefaultServer) evalRuntimeAddress() {
defer func() {
s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort)
}()
if s.customAddress != nil {
s.runtimeIP = s.customAddress.Addr().String()
s.runtimePort = int(s.customAddress.Port())
return
}
ip, port, err := s.getFirstListenerAvailable()
if err != nil {
log.Error(err)
return
}
s.runtimeIP = ip
s.runtimePort = port
}
func getLastIPFromNetwork(network *net.IPNet, fromEnd int) string {
// Calculate the last IP in the CIDR range
var endIP net.IP
for i := 0; i < len(network.IP); i++ {
endIP = append(endIP, network.IP[i]|^network.Mask[i])
}
// convert to big.Int
endInt := big.NewInt(0)
endInt.SetBytes(endIP)
// subtract fromEnd from the last ip
fromEndBig := big.NewInt(int64(fromEnd))
resultInt := big.NewInt(0)
resultInt.Sub(endInt, fromEndBig)
return net.IP(resultInt.Bytes()).String()
}
func hasValidDnsServer(cfg *nbdns.Config) bool {
for _, c := range cfg.NameServerGroups {
if c.Primary {
return true
}
}
return false
}

View File

@@ -0,0 +1,32 @@
package dns
import (
"context"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
)
// DefaultServer dummy dns server
type DefaultServer struct {
}
// NewDefaultServer On Android the DNS feature is not supported yet
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string) (*DefaultServer, error) {
return &DefaultServer{}, nil
}
// Start dummy implementation
func (s DefaultServer) Start() {
}
// Stop dummy implementation
func (s DefaultServer) Stop() {
}
// UpdateDNSServer dummy implementation
func (s DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
return nil
}

View File

@@ -0,0 +1,465 @@
//go:build !android
package dns
import (
"context"
"fmt"
"net"
"net/netip"
"runtime"
"sync"
"time"
"github.com/miekg/dns"
"github.com/mitchellh/hashstructure/v2"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
)
const (
defaultPort = 53
customPort = 5053
defaultIP = "127.0.0.1"
customIP = "127.0.0.153"
)
// DefaultServer dns server object
type DefaultServer struct {
ctx context.Context
ctxCancel context.CancelFunc
upstreamCtxCancel context.CancelFunc
mux sync.Mutex
server *dns.Server
dnsMux *dns.ServeMux
dnsMuxMap registrationMap
localResolver *localResolver
wgInterface *iface.WGIface
hostManager hostManager
updateSerial uint64
listenerIsRunning bool
runtimePort int
runtimeIP string
previousConfigHash uint64
currentConfig hostDNSConfig
customAddress *netip.AddrPort
}
type muxUpdate struct {
domain string
handler dns.Handler
}
// NewDefaultServer returns a new dns server
func NewDefaultServer(ctx context.Context, wgInterface *iface.WGIface, customAddress string) (*DefaultServer, error) {
mux := dns.NewServeMux()
dnsServer := &dns.Server{
Net: "udp",
Handler: mux,
UDPSize: 65535,
}
ctx, stop := context.WithCancel(ctx)
var addrPort *netip.AddrPort
if customAddress != "" {
parsedAddrPort, err := netip.ParseAddrPort(customAddress)
if err != nil {
stop()
return nil, fmt.Errorf("unable to parse the custom dns address, got error: %s", err)
}
addrPort = &parsedAddrPort
}
defaultServer := &DefaultServer{
ctx: ctx,
ctxCancel: stop,
server: dnsServer,
dnsMux: mux,
dnsMuxMap: make(registrationMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
wgInterface: wgInterface,
runtimePort: defaultPort,
customAddress: addrPort,
}
hostmanager, err := newHostManager(wgInterface)
if err != nil {
stop()
return nil, err
}
defaultServer.hostManager = hostmanager
return defaultServer, err
}
// Start runs the listener in a go routine
func (s *DefaultServer) Start() {
if s.customAddress != nil {
s.runtimeIP = s.customAddress.Addr().String()
s.runtimePort = int(s.customAddress.Port())
} else {
ip, port, err := s.getFirstListenerAvailable()
if err != nil {
log.Error(err)
return
}
s.runtimeIP = ip
s.runtimePort = port
}
s.server.Addr = fmt.Sprintf("%s:%d", s.runtimeIP, s.runtimePort)
log.Debugf("starting dns on %s", s.server.Addr)
go func() {
s.setListenerStatus(true)
defer s.setListenerStatus(false)
err := s.server.ListenAndServe()
if err != nil {
log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.runtimePort, err)
}
}()
}
func (s *DefaultServer) getFirstListenerAvailable() (string, int, error) {
ips := []string{defaultIP, customIP}
if runtime.GOOS != "darwin" && s.wgInterface != nil {
ips = append([]string{s.wgInterface.Address().IP.String()}, ips...)
}
ports := []int{defaultPort, customPort}
for _, port := range ports {
for _, ip := range ips {
addrString := fmt.Sprintf("%s:%d", ip, port)
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString))
probeListener, err := net.ListenUDP("udp", udpAddr)
if err == nil {
err = probeListener.Close()
if err != nil {
log.Errorf("got an error closing the probe listener, error: %s", err)
}
return ip, port, nil
}
log.Warnf("binding dns on %s is not available, error: %s", addrString, err)
}
}
return "", 0, fmt.Errorf("unable to find an unused ip and port combination. IPs tested: %v and ports %v", ips, ports)
}
func (s *DefaultServer) setListenerStatus(running bool) {
s.listenerIsRunning = running
}
// Stop stops the server
func (s *DefaultServer) Stop() {
s.mux.Lock()
defer s.mux.Unlock()
s.ctxCancel()
err := s.hostManager.restoreHostDNS()
if err != nil {
log.Error(err)
}
err = s.stopListener()
if err != nil {
log.Error(err)
}
}
func (s *DefaultServer) stopListener() error {
if !s.listenerIsRunning {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := s.server.ShutdownContext(ctx)
if err != nil {
return fmt.Errorf("stopping dns server listener returned an error: %v", err)
}
return nil
}
// UpdateDNSServer processes an update received from the management service
func (s *DefaultServer) UpdateDNSServer(serial uint64, update nbdns.Config) error {
select {
case <-s.ctx.Done():
log.Infof("not updating DNS server as context is closed")
return s.ctx.Err()
default:
if serial < s.updateSerial {
return fmt.Errorf("not applying dns update, error: "+
"network update is %d behind the last applied update", s.updateSerial-serial)
}
s.mux.Lock()
defer s.mux.Unlock()
hash, err := hashstructure.Hash(update, hashstructure.FormatV2, &hashstructure.HashOptions{
ZeroNil: true,
IgnoreZeroValue: true,
SlicesAsSets: true,
UseStringer: true,
})
if err != nil {
log.Errorf("unable to hash the dns configuration update, got error: %s", err)
}
if s.previousConfigHash == hash {
log.Debugf("not applying the dns configuration update as there is nothing new")
s.updateSerial = serial
return nil
}
if err := s.applyConfiguration(update); err != nil {
return err
}
s.updateSerial = serial
s.previousConfigHash = hash
return nil
}
}
func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
// is the service should be disabled, we stop the listener
// and proceed with a regular update to clean up the handlers and records
if !update.ServiceEnable {
err := s.stopListener()
if err != nil {
log.Error(err)
}
} else if !s.listenerIsRunning {
s.Start()
}
localMuxUpdates, localRecords, err := s.buildLocalHandlerUpdate(update.CustomZones)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
upstreamMuxUpdates, err := s.buildUpstreamHandlerUpdate(update.NameServerGroups)
if err != nil {
return fmt.Errorf("not applying dns update, error: %v", err)
}
muxUpdates := append(localMuxUpdates, upstreamMuxUpdates...)
s.updateMux(muxUpdates)
s.updateLocalResolver(localRecords)
s.currentConfig = dnsConfigToHostDNSConfig(update, s.runtimeIP, s.runtimePort)
if err = s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
log.Error(err)
}
return nil
}
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) {
var muxUpdates []muxUpdate
localRecords := make(map[string]nbdns.SimpleRecord, 0)
for _, customZone := range customZones {
if len(customZone.Records) == 0 {
return nil, nil, fmt.Errorf("received an empty list of records")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: customZone.Domain,
handler: s.localResolver,
})
for _, record := range customZone.Records {
var class uint16 = dns.ClassINET
if record.Class != nbdns.DefaultClass {
return nil, nil, fmt.Errorf("received an invalid class type: %s", record.Class)
}
key := buildRecordKey(record.Name, class, uint16(record.Type))
localRecords[key] = record
}
}
return muxUpdates, localRecords, nil
}
func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) {
// clean up the previous upstream resolver
if s.upstreamCtxCancel != nil {
s.upstreamCtxCancel()
}
var muxUpdates []muxUpdate
for _, nsGroup := range nameServerGroups {
if len(nsGroup.NameServers) == 0 {
log.Warn("received a nameserver group with empty nameserver list")
continue
}
var ctx context.Context
ctx, s.upstreamCtxCancel = context.WithCancel(s.ctx)
handler := newUpstreamResolver(ctx)
for _, ns := range nsGroup.NameServers {
if ns.NSType != nbdns.UDPNameServerType {
log.Warnf("skiping nameserver %s with type %s, this peer supports only %s",
ns.IP.String(), ns.NSType.String(), nbdns.UDPNameServerType.String())
continue
}
handler.upstreamServers = append(handler.upstreamServers, getNSHostPort(ns))
}
if len(handler.upstreamServers) == 0 {
log.Errorf("received a nameserver group with an invalid nameserver list")
continue
}
// when upstream fails to resolve domain several times over all it servers
// it will calls this hook to exclude self from the configuration and
// reapply DNS settings, but it not touch the original configuration and serial number
// because it is temporal deactivation until next try
//
// after some period defined by upstream it trys to reactivate self by calling this hook
// everything we need here is just to re-apply current configuration because it already
// contains this upstream settings (temporal deactivation not removed it)
handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler)
if nsGroup.Primary {
muxUpdates = append(muxUpdates, muxUpdate{
domain: nbdns.RootZone,
handler: handler,
})
continue
}
if len(nsGroup.Domains) == 0 {
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
}
for _, domain := range nsGroup.Domains {
if domain == "" {
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
}
muxUpdates = append(muxUpdates, muxUpdate{
domain: domain,
handler: handler,
})
}
}
return muxUpdates, nil
}
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
muxUpdateMap := make(registrationMap)
for _, update := range muxUpdates {
s.registerMux(update.domain, update.handler)
muxUpdateMap[update.domain] = struct{}{}
}
for key := range s.dnsMuxMap {
_, found := muxUpdateMap[key]
if !found {
s.deregisterMux(key)
}
}
s.dnsMuxMap = muxUpdateMap
}
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
for key := range s.localResolver.registeredMap {
_, found := update[key]
if !found {
s.localResolver.deleteRecord(key)
}
}
updatedMap := make(registrationMap)
for key, record := range update {
err := s.localResolver.registerRecord(record)
if err != nil {
log.Warnf("got an error while registering the record (%s), error: %v", record.String(), err)
}
updatedMap[key] = struct{}{}
}
s.localResolver.registeredMap = updatedMap
}
func getNSHostPort(ns nbdns.NameServer) string {
return fmt.Sprintf("%s:%d", ns.IP.String(), ns.Port)
}
func (s *DefaultServer) registerMux(pattern string, handler dns.Handler) {
s.dnsMux.Handle(pattern, handler)
}
func (s *DefaultServer) deregisterMux(pattern string) {
s.dnsMux.HandleRemove(pattern)
}
// upstreamCallbacks returns two functions, the first one is used to deactivate
// the upstream resolver from the configuration, the second one is used to
// reactivate it. Not allowed to call reactivate before deactivate.
func (s *DefaultServer) upstreamCallbacks(
nsGroup *nbdns.NameServerGroup,
handler dns.Handler,
) (deactivate func(), reactivate func()) {
var removeIndex map[string]int
deactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
l := log.WithField("nameservers", nsGroup.NameServers)
l.Info("temporary deactivate nameservers group due timeout")
removeIndex = make(map[string]int)
for _, domain := range nsGroup.Domains {
removeIndex[domain] = -1
}
if nsGroup.Primary {
removeIndex[nbdns.RootZone] = -1
s.currentConfig.routeAll = false
}
for i, item := range s.currentConfig.domains {
if _, found := removeIndex[item.domain]; found {
s.currentConfig.domains[i].disabled = true
s.deregisterMux(item.domain)
removeIndex[item.domain] = i
}
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("fail to apply nameserver deactivation on the host")
}
}
reactivate = func() {
s.mux.Lock()
defer s.mux.Unlock()
for domain, i := range removeIndex {
if i == -1 || i >= len(s.currentConfig.domains) || s.currentConfig.domains[i].domain != domain {
continue
}
s.currentConfig.domains[i].disabled = false
s.registerMux(domain, handler)
}
l := log.WithField("nameservers", nsGroup.NameServers)
l.Debug("reactivate temporary disabled nameserver group")
if nsGroup.Primary {
s.currentConfig.routeAll = true
}
if err := s.hostManager.applyDNSConfig(s.currentConfig); err != nil {
l.WithError(err).Error("reactivate temporary disabled nameserver group, DNS update apply")
}
}
return
}

View File

@@ -5,18 +5,16 @@ import (
"fmt"
"net"
"net/netip"
"os"
"strings"
"testing"
"time"
"github.com/golang/mock/gomock"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/miekg/dns"
"github.com/netbirdio/netbird/client/internal/stdnet"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
pfmock "github.com/netbirdio/netbird/iface/mocks"
)
var zoneRecords = []nbdns.SimpleRecord{
@@ -43,23 +41,21 @@ func TestUpdateDNSServer(t *testing.T) {
},
}
dummyHandler := &localResolver{}
testCases := []struct {
name string
initUpstreamMap registeredHandlerMap
initUpstreamMap registrationMap
initLocalMap registrationMap
initSerial uint64
inputSerial uint64
inputUpdate nbdns.Config
shouldFail bool
expectedUpstreamMap registeredHandlerMap
expectedUpstreamMap registrationMap
expectedLocalMap registrationMap
}{
{
name: "Initial Config Should Succeed",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registeredHandlerMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
@@ -81,13 +77,13 @@ func TestUpdateDNSServer(t *testing.T) {
},
},
},
expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler, nbdns.RootZone: dummyHandler},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}, nbdns.RootZone: struct{}{}},
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
},
{
name: "New Config Should Succeed",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registeredHandlerMap{buildRecordKey(zoneRecords[0].Name, 1, 1): dummyHandler},
initUpstreamMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
@@ -105,13 +101,13 @@ func TestUpdateDNSServer(t *testing.T) {
},
},
},
expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler},
expectedUpstreamMap: registrationMap{"netbird.io": struct{}{}, "netbird.cloud": struct{}{}},
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
},
{
name: "Smaller Config Serial Should Be Skipped",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registeredHandlerMap),
initUpstreamMap: make(registrationMap),
initSerial: 2,
inputSerial: 1,
shouldFail: true,
@@ -119,7 +115,7 @@ func TestUpdateDNSServer(t *testing.T) {
{
name: "Empty NS Group Domain Or Not Primary Element Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registeredHandlerMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
@@ -141,7 +137,7 @@ func TestUpdateDNSServer(t *testing.T) {
{
name: "Invalid NS Group Nameservers list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registeredHandlerMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
@@ -163,7 +159,7 @@ func TestUpdateDNSServer(t *testing.T) {
{
name: "Invalid Custom Zone Records list Should Fail",
initLocalMap: make(registrationMap),
initUpstreamMap: make(registeredHandlerMap),
initUpstreamMap: make(registrationMap),
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{
@@ -185,21 +181,21 @@ func TestUpdateDNSServer(t *testing.T) {
{
name: "Empty Config Should Succeed and Clean Maps",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{ServiceEnable: true},
expectedUpstreamMap: make(registeredHandlerMap),
expectedUpstreamMap: make(registrationMap),
expectedLocalMap: make(registrationMap),
},
{
name: "Disabled Service Should clean map",
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler},
initUpstreamMap: registrationMap{zoneRecords[0].Name: struct{}{}},
initSerial: 0,
inputSerial: 1,
inputUpdate: nbdns.Config{ServiceEnable: false},
expectedUpstreamMap: make(registeredHandlerMap),
expectedUpstreamMap: make(registrationMap),
expectedLocalMap: make(registrationMap),
},
}
@@ -224,11 +220,7 @@ func TestUpdateDNSServer(t *testing.T) {
t.Log(err)
}
}()
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", nil)
if err != nil {
t.Fatal(err)
}
err = dnsServer.Initialize()
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "")
if err != nil {
t.Fatal(err)
}
@@ -278,133 +270,6 @@ func TestUpdateDNSServer(t *testing.T) {
}
}
func TestDNSFakeResolverHandleUpdates(t *testing.T) {
ov := os.Getenv("NB_WG_KERNEL_DISABLED")
defer os.Setenv("NB_WG_KERNEL_DISABLED", ov)
os.Setenv("NB_WG_KERNEL_DISABLED", "true")
newNet, err := stdnet.NewNet(nil)
if err != nil {
t.Errorf("create stdnet: %v", err)
return
}
wgIface, err := iface.NewWGIFace("utun2301", "100.66.100.1/32", iface.DefaultMTU, nil, newNet)
if err != nil {
t.Errorf("build interface wireguard: %v", err)
return
}
err = wgIface.Create()
if err != nil {
t.Errorf("crate and init wireguard interface: %v", err)
return
}
defer func() {
if err = wgIface.Close(); err != nil {
t.Logf("close wireguard interface: %v", err)
}
}()
ctrl := gomock.NewController(t)
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.EXPECT().SetNetwork(ipNet)
packetfilter.EXPECT().DropOutgoing(gomock.Any()).AnyTimes()
packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
packetfilter.EXPECT().RemovePacketHook(gomock.Any()).AnyTimes()
if err := wgIface.SetFilter(packetfilter); err != nil {
t.Errorf("set packet filter: %v", err)
return
}
dnsServer, err := NewDefaultServer(context.Background(), wgIface, "", nil)
if err != nil {
t.Errorf("create DNS server: %v", err)
return
}
err = dnsServer.Initialize()
if err != nil {
t.Errorf("run DNS server: %v", err)
return
}
defer func() {
if err = dnsServer.hostManager.restoreHostDNS(); err != nil {
t.Logf("restore DNS settings on the host: %v", err)
return
}
}()
dnsServer.dnsMuxMap = registeredHandlerMap{zoneRecords[0].Name: &localResolver{}}
dnsServer.localResolver.registeredMap = registrationMap{"netbird.cloud": struct{}{}}
dnsServer.updateSerial = 0
nameServers := []nbdns.NameServer{
{
IP: netip.MustParseAddr("8.8.8.8"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
{
IP: netip.MustParseAddr("8.8.4.4"),
NSType: nbdns.UDPNameServerType,
Port: 53,
},
}
update := nbdns.Config{
ServiceEnable: true,
CustomZones: []nbdns.CustomZone{
{
Domain: "netbird.cloud",
Records: zoneRecords,
},
},
NameServerGroups: []*nbdns.NameServerGroup{
{
Domains: []string{"netbird.io"},
NameServers: nameServers,
},
{
NameServers: nameServers,
Primary: true,
},
},
}
// Start the server with regular configuration
if err := dnsServer.UpdateDNSServer(1, update); err != nil {
t.Fatalf("update dns server should not fail, got error: %v", err)
return
}
update2 := update
update2.ServiceEnable = false
// Disable the server, stop the listener
if err := dnsServer.UpdateDNSServer(2, update2); err != nil {
t.Fatalf("update dns server should not fail, got error: %v", err)
return
}
update3 := update2
update3.NameServerGroups = update3.NameServerGroups[:1]
// But service still get updates and we checking that we handle
// internal state in the right way
if err := dnsServer.UpdateDNSServer(3, update3); err != nil {
t.Fatalf("update dns server should not fail, got error: %v", err)
return
}
}
func TestDNSServerStartStop(t *testing.T) {
testCases := []struct {
name string
@@ -424,7 +289,7 @@ func TestDNSServerStartStop(t *testing.T) {
dnsServer := getDefaultServerWithNoHostManager(t, testCase.addrPort)
dnsServer.hostManager = newNoopHostMocker()
dnsServer.listen()
dnsServer.Start()
time.Sleep(100 * time.Millisecond)
if !dnsServer.listenerIsRunning {
t.Fatal("dns server listener is not running")
@@ -561,42 +426,15 @@ func getDefaultServerWithNoHostManager(t *testing.T, addrPort string) *DefaultSe
ctx, cancel := context.WithCancel(context.TODO())
ds := &DefaultServer{
return &DefaultServer{
ctx: ctx,
ctxCancel: cancel,
server: dnsServer,
dnsMux: mux,
dnsMuxMap: make(registeredHandlerMap),
dnsMuxMap: make(registrationMap),
localResolver: &localResolver{
registeredMap: make(registrationMap),
},
customAddress: parsedAddrPort,
}
ds.evalRuntimeAddress()
return ds
}
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 := getLastIPFromNetwork(ipnet, 1)
if lastIP != tt.ip {
t.Errorf("wrong IP address, expected %s: got %s", tt.ip, lastIP)
}
}
}

View File

@@ -1,5 +1,3 @@
//go:build !android
package dns
import (
@@ -11,11 +9,10 @@ import (
"github.com/godbus/dbus/v5"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
const (
@@ -78,10 +75,6 @@ func newSystemdDbusConfigurator(wgInterface *iface.WGIface) (hostManager, error)
}, nil
}
func (s *systemdDbusConfigurator) supportCustomPort() bool {
return true
}
func (s *systemdDbusConfigurator) applyDNSConfig(config hostDNSConfig) error {
parsedIP, err := netip.ParseAddr(config.serverIP)
if err != nil {

View File

@@ -3,31 +3,24 @@ package dns
import (
"context"
"errors"
"fmt"
"net"
"sync"
"sync/atomic"
"time"
"github.com/cenkalti/backoff/v4"
"github.com/miekg/dns"
log "github.com/sirupsen/logrus"
)
const (
failsTillDeact = int32(5)
reactivatePeriod = 30 * time.Second
failsTillDeact = int32(3)
reactivatePeriod = time.Minute
upstreamTimeout = 15 * time.Second
)
type upstreamClient interface {
ExchangeContext(ctx context.Context, m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error)
}
type upstreamResolver struct {
ctx context.Context
cancel context.CancelFunc
upstreamClient upstreamClient
upstreamClient *dns.Client
upstreamServers []string
disabled bool
failsCount atomic.Int32
@@ -40,11 +33,9 @@ type upstreamResolver struct {
reactivate func()
}
func newUpstreamResolver(parentCTX context.Context) *upstreamResolver {
ctx, cancel := context.WithCancel(parentCTX)
func newUpstreamResolver(ctx context.Context) *upstreamResolver {
return &upstreamResolver{
ctx: ctx,
cancel: cancel,
upstreamClient: &dns.Client{},
upstreamTimeout: upstreamTimeout,
reactivatePeriod: reactivatePeriod,
@@ -52,11 +43,6 @@ func newUpstreamResolver(parentCTX context.Context) *upstreamResolver {
}
}
func (u *upstreamResolver) stop() {
log.Debugf("stoping serving DNS for upstreams %s", u.upstreamServers)
u.cancel()
}
// ServeDNS handles a DNS request
func (u *upstreamResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
defer u.checkUpstreamFails()
@@ -121,57 +107,28 @@ func (u *upstreamResolver) checkUpstreamFails() {
log.Warnf("upstream resolving is disabled for %v", reactivatePeriod)
u.deactivate()
u.disabled = true
go u.waitUntilResponse()
go u.waitUntilReactivation()
}
}
// waitUntilResponse retries, in an exponential interval, querying the upstream servers until it gets a positive response
func (u *upstreamResolver) waitUntilResponse() {
exponentialBackOff := &backoff.ExponentialBackOff{
InitialInterval: 500 * time.Millisecond,
RandomizationFactor: 0.5,
Multiplier: 1.1,
MaxInterval: u.reactivatePeriod,
MaxElapsedTime: 0,
Stop: backoff.Stop,
Clock: backoff.SystemClock,
}
r := new(dns.Msg).SetQuestion("netbird.io.", dns.TypeA)
operation := func() error {
select {
case <-u.ctx.Done():
return backoff.Permanent(fmt.Errorf("exiting upstream retry loop for upstreams %s: parent context has been canceled", u.upstreamServers))
default:
// waitUntilReactivation reset fails counter and activates upstream resolving
func (u *upstreamResolver) waitUntilReactivation() {
timer := time.NewTimer(u.reactivatePeriod)
defer func() {
if !timer.Stop() {
<-timer.C
}
}()
var err error
for _, upstream := range u.upstreamServers {
ctx, cancel := context.WithTimeout(u.ctx, u.upstreamTimeout)
_, _, err = u.upstreamClient.ExchangeContext(ctx, r, upstream)
cancel()
if err == nil {
return nil
}
}
log.Tracef("checking connectivity with upstreams %s failed with error: %s. Retrying in %s", err, u.upstreamServers, exponentialBackOff.NextBackOff())
return fmt.Errorf("got an error from upstream check call")
}
err := backoff.Retry(operation, exponentialBackOff)
if err != nil {
log.Warn(err)
select {
case <-u.ctx.Done():
return
case <-timer.C:
log.Info("upstream resolving is reactivated")
u.failsCount.Store(0)
u.reactivate()
u.disabled = false
}
log.Infof("upstreams %s are responsive again. Adding them back to system", u.upstreamServers)
u.failsCount.Store(0)
u.reactivate()
u.disabled = false
}
// isTimeout returns true if the given error is a network timeout error.

View File

@@ -2,11 +2,10 @@ package dns
import (
"context"
"github.com/miekg/dns"
"strings"
"testing"
"time"
"github.com/miekg/dns"
)
func TestUpstreamResolver_ServeDNS(t *testing.T) {
@@ -107,29 +106,8 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
}
}
type mockUpstreamResolver struct {
r *dns.Msg
rtt time.Duration
err error
}
// ExchangeContext mock implementation of ExchangeContext from upstreamResolver
func (c mockUpstreamResolver) ExchangeContext(_ context.Context, _ *dns.Msg, _ string) (r *dns.Msg, rtt time.Duration, err error) {
return c.r, c.rtt, c.err
}
func TestUpstreamResolver_DeactivationReactivation(t *testing.T) {
resolver := &upstreamResolver{
ctx: context.TODO(),
upstreamClient: &mockUpstreamResolver{
err: nil,
r: new(dns.Msg),
rtt: time.Millisecond,
},
upstreamTimeout: upstreamTimeout,
reactivatePeriod: reactivatePeriod,
failsTillDeact: failsTillDeact,
}
resolver := newUpstreamResolver(context.TODO())
resolver.upstreamServers = []string{"0.0.0.0:-1"}
resolver.failsTillDeact = 0
resolver.reactivatePeriod = time.Microsecond * 100

View File

@@ -3,7 +3,6 @@ package internal
import (
"context"
"fmt"
"io"
"math/rand"
"net"
"net/netip"
@@ -17,19 +16,16 @@ import (
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/internal/acl"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
nbssh "github.com/netbirdio/netbird/client/ssh"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/iface/bind"
mgm "github.com/netbirdio/netbird/management/client"
mgmProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/sharedsock"
signal "github.com/netbirdio/netbird/signal/client"
sProto "github.com/netbirdio/netbird/signal/proto"
"github.com/netbirdio/netbird/util"
@@ -50,6 +46,10 @@ var ErrResetConnection = fmt.Errorf("reset connection")
type EngineConfig struct {
WgPort int
WgIfaceName string
// TunAdapter is option. It is necessary for mobile version.
TunAdapter iface.TunAdapter
IFaceDiscover stdnet.ExternalIFaceDiscover
// WgAddr is a Wireguard local address (Netbird Network IP)
WgAddr string
@@ -89,9 +89,7 @@ type Engine struct {
// syncMsgMux is used to guarantee sequential Management Service message processing
syncMsgMux *sync.Mutex
config *EngineConfig
mobileDep MobileDependency
config *EngineConfig
// STUNs is a list of STUN servers used by ICE
STUNs []*ice.URL
// TURNs is a list of STUN servers used by ICE
@@ -103,8 +101,10 @@ type Engine struct {
wgInterface *iface.WGIface
udpMux *bind.UniversalUDPMuxDefault
udpMuxConn io.Closer
udpMux ice.UDPMux
udpMuxSrflx ice.UniversalUDPMux
udpMuxConn *net.UDPConn
udpMuxConnSrflx *net.UDPConn
// networkSerial is the latest CurrentSerial (state ID) of the network sent by the Management service
networkSerial uint64
@@ -115,7 +115,6 @@ type Engine struct {
statusRecorder *peer.Status
routeManager routemanager.Manager
acl acl.Manager
dnsServer dns.Server
}
@@ -130,7 +129,7 @@ type Peer struct {
func NewEngine(
ctx context.Context, cancel context.CancelFunc,
signalClient signal.Client, mgmClient mgm.Client,
config *EngineConfig, mobileDep MobileDependency, statusRecorder *peer.Status,
config *EngineConfig, statusRecorder *peer.Status,
) *Engine {
return &Engine{
ctx: ctx,
@@ -140,7 +139,6 @@ func NewEngine(
peerConns: make(map[string]*peer.Conn),
syncMsgMux: &sync.Mutex{},
config: config,
mobileDep: mobileDep,
STUNs: []*ice.URL{},
TURNs: []*ice.URL{},
networkSerial: 0,
@@ -180,46 +178,15 @@ func (e *Engine) Start() error {
var err error
transportNet, err := e.newStdNet()
if err != nil {
log.Errorf("failed to create pion's stdnet: %s", err)
log.Warnf("failed to create pion's stdnet: %s", err)
}
e.wgInterface, err = iface.NewWGIFace(wgIFaceName, wgAddr, iface.DefaultMTU, e.mobileDep.TunAdapter, transportNet)
e.wgInterface, err = iface.NewWGIFace(wgIFaceName, wgAddr, iface.DefaultMTU, e.config.TunAdapter, transportNet)
if err != nil {
log.Errorf("failed creating wireguard interface instance %s: [%s]", wgIFaceName, err.Error())
return err
}
var routes []*route.Route
var dnsCfg *nbdns.Config
if runtime.GOOS == "android" {
routes, dnsCfg, err = e.readInitialSettings()
if err != nil {
return err
}
}
if e.dnsServer == nil {
// todo fix custom address
dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress, dnsCfg)
if err != nil {
e.close()
return err
}
e.dnsServer = dnsServer
}
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, routes)
e.routeManager.SetRouteChangeListener(e.mobileDep.RouteListener)
if runtime.GOOS != "android" {
err = e.wgInterface.Create()
} else {
err = e.wgInterface.CreateOnMobile(iface.MobileIFaceArguments{
Routes: e.routeManager.InitialRouteRange(),
Dns: e.dnsServer.DnsIP(),
})
}
err = e.wgInterface.Create()
if err != nil {
log.Errorf("failed creating tunnel interface %s: [%s]", wgIFaceName, err.Error())
e.close()
@@ -240,29 +207,45 @@ func (e *Engine) Start() error {
e.close()
return err
}
e.udpMux = udpMux
e.udpMux = udpMux.UDPMuxDefault
e.udpMuxSrflx = udpMux
log.Infof("using userspace bind mode %s", udpMux.LocalAddr().String())
} else {
rawSock, err := sharedsock.Listen(e.config.WgPort, sharedsock.NewIncomingSTUNFilter())
networkName := "udp"
if e.config.DisableIPv6Discovery {
networkName = "udp4"
}
e.udpMuxConn, err = net.ListenUDP(networkName, &net.UDPAddr{Port: e.config.UDPMuxPort})
if err != nil {
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxPort, err.Error())
e.close()
return err
}
mux := bind.NewUniversalUDPMuxDefault(bind.UniversalUDPMuxParams{UDPConn: rawSock, Net: transportNet})
go mux.ReadFromConn(e.ctx)
e.udpMuxConn = rawSock
e.udpMux = mux
udpMuxParams := ice.UDPMuxParams{
UDPConn: e.udpMuxConn,
Net: transportNet,
}
e.udpMux = ice.NewUDPMuxDefault(udpMuxParams)
e.udpMuxConnSrflx, err = net.ListenUDP(networkName, &net.UDPAddr{Port: e.config.UDPMuxSrflxPort})
if err != nil {
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxSrflxPort, err.Error())
e.close()
return err
}
e.udpMuxSrflx = ice.NewUniversalUDPMuxDefault(ice.UniversalUDPMuxParams{UDPConn: e.udpMuxConnSrflx, Net: transportNet})
}
if acl, err := acl.Create(e.wgInterface); err != nil {
log.Errorf("failed to create ACL manager, policy will not work: %s", err.Error())
} else {
e.acl = acl
}
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder)
err = e.dnsServer.Initialize()
if err != nil {
e.close()
return err
if e.dnsServer == nil {
// todo fix custom address
dnsServer, err := dns.NewDefaultServer(e.ctx, e.wgInterface, e.config.CustomDNSAddress)
if err != nil {
e.close()
return err
}
e.dnsServer = dnsServer
}
e.receiveSignalEvents()
@@ -412,6 +395,9 @@ func SignalOfferAnswer(offerAnswer peer.OfferAnswer, myKey wgtypes.Key, remoteKe
return err
}
// indicates message support in gRPC
msg.Body.FeaturesSupported = []uint32{signal.DirectCheck}
err = s.Send(msg)
if err != nil {
return err
@@ -605,7 +591,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
// cleanup request, most likely our peer has been deleted
if networkMap.GetRemotePeersIsEmpty() {
err := e.removeAllPeers()
e.statusRecorder.FinishPeerListModifications()
if err != nil {
return err
}
@@ -625,8 +610,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
return err
}
e.statusRecorder.FinishPeerListModifications()
// update SSHServer by adding remote peer SSH keys
if !isNil(e.sshServer) {
for _, config := range networkMap.GetRemotePeers() {
@@ -658,9 +641,6 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
log.Errorf("failed to update dns server, err: %v", err)
}
if e.acl != nil {
e.acl.ApplyFiltering(networkMap)
}
e.networkSerial = serial
return nil
}
@@ -762,13 +742,17 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
}
e.peerConns[peerKey] = conn
err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn)
err = e.statusRecorder.AddPeer(peerKey)
if err != nil {
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
}
go e.connWorker(conn, peerKey)
}
err := e.statusRecorder.UpdatePeerFQDN(peerKey, peerConfig.Fqdn)
if err != nil {
log.Warnf("error updating peer's %s fqdn in the status recorder, got error: %v", peerKey, err)
}
return nil
}
@@ -811,27 +795,19 @@ func (e *Engine) connWorker(conn *peer.Conn, peerKey string) {
}
}
func (e *Engine) peerExists(peerKey string) bool {
func (e Engine) peerExists(peerKey string) bool {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
_, ok := e.peerConns[peerKey]
return ok
}
func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, error) {
func (e Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, error) {
log.Debugf("creating peer connection %s", pubKey)
var stunTurn []*ice.URL
stunTurn = append(stunTurn, e.STUNs...)
stunTurn = append(stunTurn, e.TURNs...)
proxyConfig := proxy.Config{
RemoteKey: pubKey,
WgListenAddr: fmt.Sprintf("127.0.0.1:%d", e.config.WgPort),
WgInterface: e.wgInterface,
AllowedIps: allowedIPs,
PreSharedKey: e.config.PreSharedKey,
}
// randomize connection timeout
timeout := time.Duration(rand.Intn(PeerConnectionTimeoutMax-PeerConnectionTimeoutMin)+PeerConnectionTimeoutMin) * time.Millisecond
config := peer.ConnConfig{
@@ -841,15 +817,14 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs string) (*peer.Conn, e
InterfaceBlackList: e.config.IFaceBlackList,
DisableIPv6Discovery: e.config.DisableIPv6Discovery,
Timeout: timeout,
UDPMux: e.udpMux.UDPMuxDefault,
UDPMuxSrflx: e.udpMux,
ProxyConfig: proxyConfig,
UDPMux: e.udpMux,
UDPMuxSrflx: e.udpMuxSrflx,
LocalWgPort: e.config.WgPort,
NATExternalIPs: e.parseNATExternalIPMappings(),
UserspaceBind: e.wgInterface.IsUserspaceBind(),
AllowedIPs: allowedIPs,
}
peerConn, err := peer.NewConn(config, e.statusRecorder, e.mobileDep.TunAdapter, e.mobileDep.IFaceDiscover)
peerConn, err := peer.NewConn(config, e.wgInterface, e.statusRecorder, e.config.TunAdapter, e.config.IFaceDiscover)
if err != nil {
return nil, err
}
@@ -888,6 +863,7 @@ func (e *Engine) receiveSignalEvents() {
err := e.signal.Receive(func(msg *sProto.Message) error {
e.syncMsgMux.Lock()
defer e.syncMsgMux.Unlock()
conn := e.peerConns[msg.Key]
if conn == nil {
return fmt.Errorf("wrongly addressed message %s", msg.Key)
@@ -934,6 +910,18 @@ func (e *Engine) receiveSignalEvents() {
}
conn.OnRemoteCandidate(candidate)
case sProto.Body_MODE:
protoMode := msg.GetBody().GetMode()
if protoMode == nil {
return fmt.Errorf("received an empty mode message")
}
err := conn.OnModeMessage(peer.ModeMessage{
Direct: protoMode.GetDirect(),
})
if err != nil {
log.Errorf("failed processing a mode message -> %s", err)
return err
}
}
return nil
@@ -1024,6 +1012,12 @@ func (e *Engine) close() {
}
}
if e.udpMuxConnSrflx != nil {
if err := e.udpMuxConnSrflx.Close(); err != nil {
log.Debugf("close server reflexive udp mux connection: %v", err)
}
}
if !isNil(e.sshServer) {
err := e.sshServer.Stop()
if err != nil {
@@ -1039,19 +1033,6 @@ func (e *Engine) close() {
e.dnsServer.Stop()
}
if e.acl != nil {
e.acl.Stop()
}
}
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, error) {
netMap, err := e.mgmClient.GetNetworkMap()
if err != nil {
return nil, nil, err
}
routes := toRoutes(netMap.GetRoutes())
dnsCfg := toDNSConfig(netMap.GetDNSConfig())
return routes, &dnsCfg, nil
}
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {

View File

@@ -3,5 +3,5 @@ package internal
import "github.com/netbirdio/netbird/client/internal/stdnet"
func (e *Engine) newStdNet() (*stdnet.Net, error) {
return stdnet.NewNetWithDiscover(e.mobileDep.IFaceDiscover, e.config.IFaceBlackList)
return stdnet.NewNetWithDiscover(e.config.IFaceDiscover, e.config.IFaceBlackList)
}

View File

@@ -3,6 +3,8 @@ package internal
import (
"context"
"fmt"
"github.com/netbirdio/netbird/iface/bind"
"github.com/pion/transport/v2/stdnet"
"net"
"net/netip"
"os"
@@ -13,7 +15,6 @@ import (
"testing"
"time"
"github.com/pion/transport/v2/stdnet"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -28,7 +29,6 @@ import (
"github.com/netbirdio/netbird/client/system"
nbdns "github.com/netbirdio/netbird/dns"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/iface/bind"
mgmt "github.com/netbirdio/netbird/management/client"
mgmtProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/management/server"
@@ -74,7 +74,7 @@ func TestEngine_SSH(t *testing.T) {
WgAddr: "100.64.0.1/24",
WgPrivateKey: key,
WgPort: 33100,
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
}, peer.NewRecorder("https://mgm"))
engine.dnsServer = &dns.MockServer{
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
@@ -208,7 +208,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
WgAddr: "100.64.0.1/24",
WgPrivateKey: key,
WgPort: 33100,
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
}, peer.NewRecorder("https://mgm"))
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
@@ -217,7 +217,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
if err != nil {
t.Fatal(err)
}
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), engine.wgInterface, engine.statusRecorder, nil)
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), engine.wgInterface, engine.statusRecorder)
engine.dnsServer = &dns.MockServer{
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
}
@@ -404,7 +404,7 @@ func TestEngine_Sync(t *testing.T) {
WgAddr: "100.64.0.1/24",
WgPrivateKey: key,
WgPort: 33100,
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
}, peer.NewRecorder("https://mgm"))
engine.dnsServer = &dns.MockServer{
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
@@ -562,7 +562,7 @@ func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
WgAddr: wgAddr,
WgPrivateKey: key,
WgPort: 33100,
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
}, peer.NewRecorder("https://mgm"))
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
@@ -731,7 +731,7 @@ func TestEngine_UpdateNetworkMapWithDNSUpdate(t *testing.T) {
WgAddr: wgAddr,
WgPrivateKey: key,
WgPort: 33100,
}, MobileDependency{}, peer.NewRecorder("https://mgm"))
}, peer.NewRecorder("https://mgm"))
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
@@ -1000,7 +1000,7 @@ func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey strin
WgPort: wgPort,
}
return NewEngine(ctx, cancel, signalClient, mgmtClient, conf, MobileDependency{}, peer.NewRecorder("https://mgm")), nil
return NewEngine(ctx, cancel, signalClient, mgmtClient, conf, peer.NewRecorder("https://mgm")), nil
}
func startSignal() (*grpc.Server, string, error) {
@@ -1039,7 +1039,7 @@ func startManagement(dataDir string) (*grpc.Server, string, error) {
return nil, "", err
}
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
store, err := server.NewFileStore(config.Datadir, nil)
store, err := server.NewFileStore(config.Datadir)
if err != nil {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}

View File

@@ -1,14 +0,0 @@
package internal
import (
"github.com/netbirdio/netbird/client/internal/routemanager"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/iface"
)
// MobileDependency collect all dependencies for mobile platform
type MobileDependency struct {
TunAdapter iface.TunAdapter
IFaceDiscover stdnet.ExternalIFaceDiscover
RouteListener routemanager.RouteListener
}

View File

@@ -148,11 +148,6 @@ func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
return DeviceAuthInfo{}, fmt.Errorf("unmarshaling response failed with error: %v", err)
}
// Fallback to the verification_uri if the IdP doesn't support verification_uri_complete
if deviceCode.VerificationURIComplete == "" {
deviceCode.VerificationURIComplete = deviceCode.VerificationURI
}
return deviceCode, err
}

View File

@@ -14,17 +14,11 @@ import (
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/iface/bind"
signal "github.com/netbirdio/netbird/signal/client"
sProto "github.com/netbirdio/netbird/signal/proto"
"github.com/netbirdio/netbird/version"
)
const (
iceKeepAliveDefault = 4 * time.Second
iceDisconnectedTimeoutDefault = 6 * time.Second
)
// ConnConfig is a peer Connection configuration
type ConnConfig struct {
@@ -43,8 +37,6 @@ type ConnConfig struct {
Timeout time.Duration
ProxyConfig proxy.Config
UDPMux ice.UDPMux
UDPMuxSrflx ice.UniversalUDPMux
@@ -52,8 +44,7 @@ type ConnConfig struct {
NATExternalIPs []string
// UsesBind indicates whether the WireGuard interface is userspace and uses bind.ICEBind
UserspaceBind bool
AllowedIPs string
}
// OfferAnswer represents a session establishment offer or answer
@@ -102,6 +93,7 @@ type Conn struct {
remoteModeCh chan ModeMessage
meta meta
wgIface *iface.WGIface
adapter iface.TunAdapter
iFaceDiscover stdnet.ExternalIFaceDiscover
}
@@ -129,7 +121,7 @@ func (conn *Conn) UpdateConf(conf ConnConfig) {
// NewConn creates a new not opened Conn to the remote peer.
// To establish a connection run Conn.Open
func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*Conn, error) {
func NewConn(config ConnConfig, wgIface *iface.WGIface, statusRecorder *Status, adapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover) (*Conn, error) {
return &Conn{
config: config,
mu: sync.Mutex{},
@@ -141,6 +133,7 @@ func NewConn(config ConnConfig, statusRecorder *Status, adapter iface.TunAdapter
remoteModeCh: make(chan ModeMessage, 1),
adapter: adapter,
iFaceDiscover: iFaceDiscover,
wgIface: wgIface,
}, nil
}
@@ -153,25 +146,19 @@ func (conn *Conn) reCreateAgent() error {
var err error
transportNet, err := conn.newStdNet()
if err != nil {
log.Errorf("failed to create pion's stdnet: %s", err)
log.Warnf("failed to create pion's stdnet: %s", err)
}
iceKeepAlive := iceKeepAlive()
iceDisconnectedTimeout := iceDisconnectedTimeout()
agentConfig := &ice.AgentConfig{
MulticastDNSMode: ice.MulticastDNSModeDisabled,
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
Urls: conn.config.StunTurn,
CandidateTypes: conn.candidateTypes(),
FailedTimeout: &failedTimeout,
InterfaceFilter: stdnet.InterfaceFilter(conn.config.InterfaceBlackList),
UDPMux: conn.config.UDPMux,
UDPMuxSrflx: conn.config.UDPMuxSrflx,
NAT1To1IPs: conn.config.NATExternalIPs,
Net: transportNet,
DisconnectedTimeout: &iceDisconnectedTimeout,
KeepaliveInterval: &iceKeepAlive,
MulticastDNSMode: ice.MulticastDNSModeDisabled,
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
Urls: conn.config.StunTurn,
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
FailedTimeout: &failedTimeout,
InterfaceFilter: stdnet.InterfaceFilter(conn.config.InterfaceBlackList),
UDPMux: conn.config.UDPMux,
UDPMuxSrflx: conn.config.UDPMuxSrflx,
NAT1To1IPs: conn.config.NATExternalIPs,
Net: transportNet,
}
if conn.config.DisableIPv6Discovery {
@@ -202,13 +189,6 @@ func (conn *Conn) reCreateAgent() error {
return nil
}
func (conn *Conn) candidateTypes() []ice.CandidateType {
if hasICEForceRelayConn() {
return []ice.CandidateType{ice.CandidateTypeRelay}
}
return []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay}
}
// Open opens connection to the remote peer starting ICE candidate gathering process.
// Blocks until connection has been closed or connection timeout.
// ConnStatus will be set accordingly
@@ -217,7 +197,7 @@ func (conn *Conn) Open() error {
peerState := State{PubKey: conn.config.Key}
peerState.IP = strings.Split(conn.config.ProxyConfig.AllowedIps, "/")[0]
peerState.IP = strings.Split(conn.config.AllowedIPs, "/")[0]
peerState.ConnStatusUpdate = time.Now()
peerState.ConnStatus = conn.status
@@ -334,10 +314,82 @@ func (conn *Conn) Open() error {
}
}
// useProxy determines whether a direct connection (without a go proxy) is possible
//
// There are 3 cases:
//
// * When neither candidate is from hard nat and one of the peers has a public IP
//
// * both peers are in the same private network
//
// * Local peer uses userspace interface with bind.ICEBind and is not relayed
//
// Please note, that this check happens when peers were already able to ping each other using ICE layer.
func shouldUseProxy(pair *ice.CandidatePair, userspaceBind bool) bool {
if !isRelayCandidate(pair.Local) && userspaceBind {
return false
}
if !isHardNATCandidate(pair.Local) && isHostCandidateWithPublicIP(pair.Remote) {
return false
}
if !isHardNATCandidate(pair.Remote) && isHostCandidateWithPublicIP(pair.Local) {
return false
}
if isHostCandidateWithPrivateIP(pair.Local) && isHostCandidateWithPrivateIP(pair.Remote) && isSameNetworkPrefix(pair) {
return false
}
return true
}
func isSameNetworkPrefix(pair *ice.CandidatePair) bool {
localIPStr, _, err := net.SplitHostPort(pair.Local.Address())
if err != nil {
return false
}
remoteIPStr, _, err := net.SplitHostPort(pair.Remote.Address())
if err != nil {
return false
}
localIP := net.ParseIP(localIPStr)
remoteIP := net.ParseIP(remoteIPStr)
if localIP == nil || remoteIP == nil {
return false
}
// only consider /16 networks
mask := net.IPMask{255, 255, 0, 0}
return localIP.Mask(mask).Equal(remoteIP.Mask(mask))
}
func isRelayCandidate(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeRelay
}
func isHardNATCandidate(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeRelay || candidate.Type() == ice.CandidateTypePeerReflexive
}
func isHostCandidateWithPublicIP(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeHost && isPublicIP(candidate.Address())
}
func isHostCandidateWithPrivateIP(candidate ice.Candidate) bool {
return candidate.Type() == ice.CandidateTypeHost && !isPublicIP(candidate.Address())
}
func isPublicIP(address string) bool {
ip := net.ParseIP(address)
if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() {
return false
}
return true
}
// startProxy starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
conn.mu.Lock()
@@ -350,7 +402,7 @@ func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
}
peerState := State{PubKey: conn.config.Key}
p := conn.getProxy(pair, remoteWgPort)
p := conn.getProxyWithMessageExchange(pair, remoteWgPort)
conn.proxy = p
err = p.Start(remoteConn)
if err != nil {
@@ -366,7 +418,7 @@ func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
peerState.Relayed = true
}
peerState.Direct = p.Type() == proxy.TypeDirectNoProxy || p.Type() == proxy.TypeNoProxy
peerState.Direct = p.Type() == proxy.TypeDirectNoProxy
err = conn.statusRecorder.UpdatePeerState(peerState)
if err != nil {
@@ -376,35 +428,63 @@ func (conn *Conn) startProxy(remoteConn net.Conn, remoteWgPort int) error {
return nil
}
// todo rename this method and the proxy package to something more appropriate
func (conn *Conn) getProxy(pair *ice.CandidatePair, remoteWgPort int) proxy.Proxy {
if isRelayCandidate(pair.Local) {
return proxy.NewWireGuardProxy(conn.config.ProxyConfig)
func (conn *Conn) getProxyWithMessageExchange(pair *ice.CandidatePair, remoteWgPort int) proxy.Proxy {
useProxy := shouldUseProxy(pair, conn.wgIface.IsUserspaceBind())
localDirectMode := !useProxy
remoteDirectMode := localDirectMode
if conn.meta.protoSupport.DirectCheck {
go conn.sendLocalDirectMode(localDirectMode)
// will block until message received or timeout
remoteDirectMode = conn.receiveRemoteDirectMode()
}
// To support old version's with direct mode we attempt to punch an additional role with the remote wireguard port
go conn.punchRemoteWGPort(pair, remoteWgPort)
if conn.wgIface.IsUserspaceBind() && localDirectMode {
return proxy.NewNoProxy(conn.config.ProxyConfig)
}
return proxy.NewNoProxy(conn.config.ProxyConfig)
if localDirectMode && remoteDirectMode {
//wgInterface *iface.WGIface, remoteKey string, allowedIps string, preSharedKey *wgtypes.Key, remoteWgPort int)
return proxy.NewDirectNoProxy(conn.wgIface, conn.config.Key, conn.config.AllowedIPs, remoteWgPort)
}
log.Debugf("falling back to local proxy mode with peer %s", conn.config.Key)
return proxy.NewWireGuardProxy(conn.config.ProxyConfig)
}
func (conn *Conn) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
// wait local endpoint configuration
time.Sleep(time.Second)
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", pair.Remote.Address(), remoteWgPort))
if err != nil {
log.Warnf("got an error while resolving the udp address, err: %s", err)
return
}
func (conn *Conn) sendLocalDirectMode(localMode bool) {
// todo what happens when we couldn't deliver this message?
// we could retry, etc but there is no guarantee
mux, ok := conn.config.UDPMuxSrflx.(*bind.UniversalUDPMuxDefault)
if !ok {
log.Warn("invalid udp mux conversion")
return
}
_, err = mux.GetSharedConn().WriteTo([]byte{0x6e, 0x62}, addr)
err := conn.sendSignalMessage(&sProto.Message{
Key: conn.config.LocalKey,
RemoteKey: conn.config.Key,
Body: &sProto.Body{
Type: sProto.Body_MODE,
Mode: &sProto.Mode{
Direct: &localMode,
},
NetBirdVersion: version.NetbirdVersion(),
},
})
if err != nil {
log.Warnf("got an error while sending the punch packet, err: %s", err)
log.Errorf("failed to send local proxy mode to remote peer %s, error: %s", conn.config.Key, err)
}
}
func (conn *Conn) receiveRemoteDirectMode() bool {
timeout := time.Second
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case receivedMSG := <-conn.remoteModeCh:
return receivedMSG.Direct
case <-timer.C:
// we didn't receive a message from remote so we assume that it supports the direct mode to keep the old behaviour
log.Debugf("timeout after %s while waiting for remote direct mode message from remote peer %s",
timeout, conn.config.Key)
return true
}
}
@@ -628,6 +708,16 @@ func (conn *Conn) GetKey() string {
return conn.config.Key
}
// OnModeMessage unmarshall the payload message and send it to the mode message channel
func (conn *Conn) OnModeMessage(message ModeMessage) error {
select {
case conn.remoteModeCh <- message:
return nil
default:
return fmt.Errorf("unable to process mode message: channel busy")
}
}
// RegisterProtoSupportMeta register supported proto message in the connection metadata
func (conn *Conn) RegisterProtoSupportMeta(support []uint32) {
protoSupport := signal.ParseFeaturesSupported(support)

View File

@@ -1,17 +1,18 @@
package peer
import (
"github.com/netbirdio/netbird/client/internal/stdnet"
"sync"
"testing"
"time"
"github.com/netbirdio/netbird/client/internal/stdnet"
"github.com/magiconair/properties/assert"
"github.com/pion/ice/v2"
"golang.org/x/sync/errgroup"
"github.com/netbirdio/netbird/client/internal/proxy"
"github.com/netbirdio/netbird/iface"
sproto "github.com/netbirdio/netbird/signal/proto"
)
var connConf = ConnConfig{
@@ -168,3 +169,274 @@ func TestConn_Close(t *testing.T) {
wg.Wait()
}
type mockICECandidate struct {
ice.Candidate
AddressFunc func() string
TypeFunc func() ice.CandidateType
}
// Address mocks and overwrite ice.Candidate Address method
func (m *mockICECandidate) Address() string {
if m.AddressFunc != nil {
return m.AddressFunc()
}
return ""
}
// Type mocks and overwrite ice.Candidate Type method
func (m *mockICECandidate) Type() ice.CandidateType {
if m.TypeFunc != nil {
return m.TypeFunc()
}
return ice.CandidateTypeUnspecified
}
func TestConn_ShouldUseProxy(t *testing.T) {
publicHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "8.8.8.8"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
privateHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "10.0.0.1:44576"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
srflxCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeServerReflexive
},
}
prflxCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypePeerReflexive
},
}
relayCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeRelay
},
}
testCases := []struct {
name string
candatePair *ice.CandidatePair
expected bool
}{
{
name: "Use Proxy When Local Candidate Is Relay",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: privateHostCandidate,
},
expected: true,
},
{
name: "Use Proxy When Remote Candidate Is Relay",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: relayCandidate,
},
expected: true,
},
{
name: "Use Proxy When Local Candidate Is Peer Reflexive",
candatePair: &ice.CandidatePair{
Local: prflxCandidate,
Remote: privateHostCandidate,
},
expected: true,
},
{
name: "Use Proxy When Remote Candidate Is Peer Reflexive",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: prflxCandidate,
},
expected: true,
},
{
name: "Don't Use Proxy When Local Candidate Is Public And Remote Is Private",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: privateHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Remote Candidate Is Public And Local Is Private",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Local Candidate is Public And Remote Is Server Reflexive",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: srflxCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Remote Candidate is Public And Local Is Server Reflexive",
candatePair: &ice.CandidatePair{
Local: srflxCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Both Candidates Are Public",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
expected: false,
},
{
name: "Don't Use Proxy When Both Candidates Are Private",
candatePair: &ice.CandidatePair{
Local: privateHostCandidate,
Remote: privateHostCandidate,
},
expected: false,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := shouldUseProxy(testCase.candatePair, false)
if result != testCase.expected {
t.Errorf("got a different result. Expected %t Got %t", testCase.expected, result)
}
})
}
}
func TestGetProxyWithMessageExchange(t *testing.T) {
publicHostCandidate := &mockICECandidate{
AddressFunc: func() string {
return "8.8.8.8"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeHost
},
}
relayCandidate := &mockICECandidate{
AddressFunc: func() string {
return "1.1.1.1"
},
TypeFunc: func() ice.CandidateType {
return ice.CandidateTypeRelay
},
}
testCases := []struct {
name string
candatePair *ice.CandidatePair
inputDirectModeSupport bool
inputRemoteModeMessage bool
expected proxy.Type
}{
{
name: "Should Result In Using Wireguard Proxy When Local Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: true,
expected: proxy.TypeWireGuard,
},
{
name: "Should Result In Using Wireguard Proxy When Remote Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: false,
expected: proxy.TypeWireGuard,
},
{
name: "Should Result In Using Wireguard Proxy When Remote Direct Mode Support Is False And Local Eval Is Use Proxy",
candatePair: &ice.CandidatePair{
Local: relayCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: false,
inputRemoteModeMessage: false,
expected: proxy.TypeWireGuard,
},
{
name: "Should Result In Using Direct When Remote Direct Mode Support Is False And Local Eval Is No Use Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: false,
inputRemoteModeMessage: false,
expected: proxy.TypeDirectNoProxy,
},
{
name: "Should Result In Using Direct When Local And Remote Eval Is No Proxy",
candatePair: &ice.CandidatePair{
Local: publicHostCandidate,
Remote: publicHostCandidate,
},
inputDirectModeSupport: true,
inputRemoteModeMessage: true,
expected: proxy.TypeDirectNoProxy,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
g := errgroup.Group{}
conn, err := NewConn(connConf, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
conn.meta.protoSupport.DirectCheck = testCase.inputDirectModeSupport
conn.SetSendSignalMessage(func(message *sproto.Message) error {
return nil
})
g.Go(func() error {
return conn.OnModeMessage(ModeMessage{
Direct: testCase.inputRemoteModeMessage,
})
})
resultProxy := conn.getProxyWithMessageExchange(testCase.candatePair, 1000)
err = g.Wait()
if err != nil {
t.Error(err)
}
if resultProxy.Type() != testCase.expected {
t.Errorf("result didn't match expected value: Expected: %s, Got: %s", testCase.expected, resultProxy.Type())
}
})
}
}

View File

@@ -1,53 +0,0 @@
package peer
import (
"os"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
const (
envICEKeepAliveIntervalSec = "NB_ICE_KEEP_ALIVE_INTERVAL_SEC"
envICEDisconnectedTimeoutSec = "NB_ICE_DISCONNECTED_TIMEOUT_SEC"
envICEForceRelayConn = "NB_ICE_FORCE_RELAY_CONN"
)
func iceKeepAlive() time.Duration {
keepAliveEnv := os.Getenv(envICEKeepAliveIntervalSec)
if keepAliveEnv == "" {
return iceKeepAliveDefault
}
log.Debugf("setting ICE keep alive interval to %s seconds", keepAliveEnv)
keepAliveEnvSec, err := strconv.Atoi(keepAliveEnv)
if err != nil {
log.Warnf("invalid value %s set for %s, using default %v", keepAliveEnv, envICEKeepAliveIntervalSec, iceKeepAliveDefault)
return iceKeepAliveDefault
}
return time.Duration(keepAliveEnvSec) * time.Second
}
func iceDisconnectedTimeout() time.Duration {
disconnectedTimeoutEnv := os.Getenv(envICEDisconnectedTimeoutSec)
if disconnectedTimeoutEnv == "" {
return iceDisconnectedTimeoutDefault
}
log.Debugf("setting ICE disconnected timeout to %s seconds", disconnectedTimeoutEnv)
disconnectedTimeoutSec, err := strconv.Atoi(disconnectedTimeoutEnv)
if err != nil {
log.Warnf("invalid value %s set for %s, using default %v", disconnectedTimeoutEnv, envICEDisconnectedTimeoutSec, iceDisconnectedTimeoutDefault)
return iceDisconnectedTimeoutDefault
}
return time.Duration(disconnectedTimeoutSec) * time.Second
}
func hasICEForceRelayConn() bool {
disconnectedTimeoutEnv := os.Getenv(envICEForceRelayConn)
return strings.ToLower(disconnectedTimeoutEnv) == "true"
}

View File

@@ -59,11 +59,6 @@ type Status struct {
mgmAddress string
signalAddress string
notifier *notifier
// To reduce the number of notification invocation this bool will be true when need to call the notification
// Some Peer actions mostly used by in a batch when the network map has been synchronized. In these type of events
// set to true this variable and at the end of the processing we will reset it by the FinishPeerListModifications()
peerListChangedForNotification bool
}
// NewRecorder returns a new Status instance
@@ -83,13 +78,10 @@ func (d *Status) ReplaceOfflinePeers(replacement []State) {
defer d.mux.Unlock()
d.offlinePeers = make([]State, len(replacement))
copy(d.offlinePeers, replacement)
// todo we should set to true in case if the list changed only
d.peerListChangedForNotification = true
}
// AddPeer adds peer to Daemon status map
func (d *Status) AddPeer(peerPubKey string, fqdn string) error {
func (d *Status) AddPeer(peerPubKey string) error {
d.mux.Lock()
defer d.mux.Unlock()
@@ -97,12 +89,7 @@ func (d *Status) AddPeer(peerPubKey string, fqdn string) error {
if ok {
return errors.New("peer already exist")
}
d.peers[peerPubKey] = State{
PubKey: peerPubKey,
ConnStatus: StatusDisconnected,
FQDN: fqdn,
}
d.peerListChangedForNotification = true
d.peers[peerPubKey] = State{PubKey: peerPubKey, ConnStatus: StatusDisconnected}
return nil
}
@@ -124,13 +111,13 @@ func (d *Status) RemovePeer(peerPubKey string) error {
defer d.mux.Unlock()
_, ok := d.peers[peerPubKey]
if !ok {
return errors.New("no peer with to remove")
if ok {
delete(d.peers, peerPubKey)
return nil
}
delete(d.peers, peerPubKey)
d.peerListChangedForNotification = true
return nil
d.notifyPeerListChanged()
return errors.New("no peer with to remove")
}
// UpdatePeerState updates peer status
@@ -147,8 +134,6 @@ func (d *Status) UpdatePeerState(receivedState State) error {
peerState.IP = receivedState.IP
}
skipNotification := shouldSkipNotify(receivedState, peerState)
if receivedState.ConnStatus != peerState.ConnStatus {
peerState.ConnStatus = receivedState.ConnStatus
peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate
@@ -160,10 +145,6 @@ func (d *Status) UpdatePeerState(receivedState State) error {
d.peers[receivedState.PubKey] = peerState
if skipNotification {
return nil
}
ch, found := d.changeNotify[receivedState.PubKey]
if found && ch != nil {
close(ch)
@@ -174,19 +155,6 @@ func (d *Status) UpdatePeerState(receivedState State) error {
return nil
}
func shouldSkipNotify(new, curr State) bool {
switch {
case new.ConnStatus == StatusConnecting:
return true
case new.ConnStatus == StatusDisconnected && curr.ConnStatus == StatusConnecting:
return true
case new.ConnStatus == StatusDisconnected && curr.ConnStatus == StatusDisconnected:
return curr.IP != ""
default:
return false
}
}
// UpdatePeerFQDN update peer's state fqdn only
func (d *Status) UpdatePeerFQDN(peerPubKey, fqdn string) error {
d.mux.Lock()
@@ -200,21 +168,8 @@ func (d *Status) UpdatePeerFQDN(peerPubKey, fqdn string) error {
peerState.FQDN = fqdn
d.peers[peerPubKey] = peerState
return nil
}
// FinishPeerListModifications this event invoke the notification
func (d *Status) FinishPeerListModifications() {
d.mux.Lock()
if !d.peerListChangedForNotification {
d.mux.Unlock()
return
}
d.peerListChangedForNotification = false
d.mux.Unlock()
d.notifyPeerListChanged()
return nil
}
// GetPeerStateChangeNotifier returns a change notifier channel for a peer
@@ -353,7 +308,7 @@ func (d *Status) onConnectionChanged() {
}
func (d *Status) notifyPeerListChanged() {
d.notifier.peerListChanged(len(d.peers) + len(d.offlinePeers))
d.notifier.peerListChanged(len(d.peers))
}
func (d *Status) notifyAddressChanged() {

View File

@@ -9,13 +9,13 @@ import (
func TestAddPeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
err := status.AddPeer(key, "abc.netbird")
err := status.AddPeer(key)
assert.NoError(t, err, "shouldn't return error")
_, exists := status.peers[key]
assert.True(t, exists, "value was found")
err = status.AddPeer(key, "abc.netbird")
err = status.AddPeer(key)
assert.Error(t, err, "should return error on duplicate")
}
@@ -23,7 +23,7 @@ func TestAddPeer(t *testing.T) {
func TestGetPeer(t *testing.T) {
key := "abc"
status := NewRecorder("https://mgm")
err := status.AddPeer(key, "abc.netbird")
err := status.AddPeer(key)
assert.NoError(t, err, "shouldn't return error")
peerStatus, err := status.GetPeer(key)

View File

@@ -0,0 +1,67 @@
package proxy
import (
"net"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/iface"
)
// DirectNoProxy is used when there is no need for a proxy between ICE and WireGuard.
// This is possible in either of these cases:
// - peers are in the same local network
// - one of the peers has a public static IP (host)
// DirectNoProxy will just update remote peer with a remote host and fixed WireGuard port (r.g. 51820).
// In order DirectNoProxy to work, WireGuard port has to be fixed for the time being.
type DirectNoProxy struct {
wgInterface *iface.WGIface
remoteKey string
allowedIps string
// RemoteWgListenPort is a WireGuard port of a remote peer.
// It is used instead of the hardcoded 51820 port.
remoteWgListenPort int
}
// NewDirectNoProxy creates a new DirectNoProxy with a provided config and remote peer's WireGuard listen port
func NewDirectNoProxy(wgInterface *iface.WGIface, remoteKey string, allowedIps string, remoteWgPort int) *DirectNoProxy {
return &DirectNoProxy{
wgInterface: wgInterface,
remoteKey: remoteKey,
allowedIps: allowedIps,
remoteWgListenPort: remoteWgPort}
}
// Close removes peer from the WireGuard interface
func (p *DirectNoProxy) Close() error {
err := p.wgInterface.RemovePeer(p.remoteKey)
if err != nil {
return err
}
return nil
}
// Start just updates WireGuard peer with the remote IP and default WireGuard port
func (p *DirectNoProxy) Start(remoteConn net.Conn) error {
log.Debugf("using DirectNoProxy while connecting to peer %s", p.remoteKey)
addr, err := net.ResolveUDPAddr("udp", remoteConn.RemoteAddr().String())
if err != nil {
return err
}
addr.Port = p.remoteWgListenPort
err = p.wgInterface.UpdatePeer(p.remoteKey, p.allowedIps, addr)
if err != nil {
return err
}
return nil
}
// Type returns the type of this proxy
func (p *DirectNoProxy) Type() Type {
return TypeDirectNoProxy
}

View File

@@ -1,15 +1,10 @@
package proxy
import (
"github.com/netbirdio/netbird/iface"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"io"
"net"
"time"
)
const DefaultWgKeepAlive = 25 * time.Second
type Type string
const (
@@ -19,14 +14,6 @@ const (
TypeNoProxy Type = "NoProxy"
)
type Config struct {
WgListenAddr string
RemoteKey string
WgInterface *iface.WGIface
AllowedIps string
PreSharedKey *wgtypes.Key
}
type Proxy interface {
io.Closer
// Start creates a local remoteConn and starts proxying data from/to remoteConn

View File

@@ -71,7 +71,7 @@ func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
}
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]routerPeerStatus) string {
chosen := ""
var chosen string
chosenScore := 0
currID := ""
@@ -85,26 +85,17 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]ro
if !found || !peerStatus.connected {
continue
}
if r.Metric < route.MaxMetric {
metricDiff := route.MaxMetric - r.Metric
tempScore = metricDiff * 10
}
if !peerStatus.relayed {
tempScore++
}
if peerStatus.direct {
if !peerStatus.direct {
tempScore++
}
if tempScore > chosenScore || (tempScore == chosenScore && r.ID == currID) {
chosen = r.ID
chosenScore = tempScore
}
if chosen == "" && currID == "" {
if tempScore > chosenScore || (tempScore == chosenScore && currID == r.ID) {
chosen = r.ID
chosenScore = tempScore
}
@@ -115,9 +106,7 @@ func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]ro
for _, r := range c.routes {
peers = append(peers, r.Peer)
}
log.Warnf("the network %s has not been assigned a routing peer as no peers from the list %s are currently connected", c.network, peers)
log.Warnf("no route was chosen for network %s because no peers from list %s were connected", c.network, peers)
} else if chosen != currID {
log.Infof("new chosen route is %s with peer %s with score %d", chosen, c.routes[chosen].Peer, chosenScore)
}

View File

@@ -1,199 +0,0 @@
package routemanager
import (
"net/netip"
"testing"
"github.com/netbirdio/netbird/route"
)
func TestGetBestrouteFromStatuses(t *testing.T) {
testCases := []struct {
name string
statuses map[string]routerPeerStatus
expectedRouteID string
currentRoute *route.Route
existingRoutes map[string]*route.Route
}{
{
name: "one route",
statuses: map[string]routerPeerStatus{
"route1": {
connected: true,
relayed: false,
direct: true,
},
},
existingRoutes: map[string]*route.Route{
"route1": {
ID: "route1",
Metric: route.MaxMetric,
Peer: "peer1",
},
},
currentRoute: nil,
expectedRouteID: "route1",
},
{
name: "one connected routes with relayed and direct",
statuses: map[string]routerPeerStatus{
"route1": {
connected: true,
relayed: true,
direct: true,
},
},
existingRoutes: map[string]*route.Route{
"route1": {
ID: "route1",
Metric: route.MaxMetric,
Peer: "peer1",
},
},
currentRoute: nil,
expectedRouteID: "route1",
},
{
name: "one connected routes with relayed and no direct",
statuses: map[string]routerPeerStatus{
"route1": {
connected: true,
relayed: true,
direct: false,
},
},
existingRoutes: map[string]*route.Route{
"route1": {
ID: "route1",
Metric: route.MaxMetric,
Peer: "peer1",
},
},
currentRoute: nil,
expectedRouteID: "route1",
},
{
name: "no connected peers",
statuses: map[string]routerPeerStatus{
"route1": {
connected: false,
relayed: false,
direct: false,
},
},
existingRoutes: map[string]*route.Route{
"route1": {
ID: "route1",
Metric: route.MaxMetric,
Peer: "peer1",
},
},
currentRoute: nil,
expectedRouteID: "",
},
{
name: "multiple connected peers with different metrics",
statuses: map[string]routerPeerStatus{
"route1": {
connected: true,
relayed: false,
direct: true,
},
"route2": {
connected: true,
relayed: false,
direct: true,
},
},
existingRoutes: map[string]*route.Route{
"route1": {
ID: "route1",
Metric: 9000,
Peer: "peer1",
},
"route2": {
ID: "route2",
Metric: route.MaxMetric,
Peer: "peer2",
},
},
currentRoute: nil,
expectedRouteID: "route1",
},
{
name: "multiple connected peers with one relayed",
statuses: map[string]routerPeerStatus{
"route1": {
connected: true,
relayed: false,
direct: true,
},
"route2": {
connected: true,
relayed: true,
direct: true,
},
},
existingRoutes: map[string]*route.Route{
"route1": {
ID: "route1",
Metric: route.MaxMetric,
Peer: "peer1",
},
"route2": {
ID: "route2",
Metric: route.MaxMetric,
Peer: "peer2",
},
},
currentRoute: nil,
expectedRouteID: "route1",
},
{
name: "multiple connected peers with one direct",
statuses: map[string]routerPeerStatus{
"route1": {
connected: true,
relayed: false,
direct: true,
},
"route2": {
connected: true,
relayed: false,
direct: false,
},
},
existingRoutes: map[string]*route.Route{
"route1": {
ID: "route1",
Metric: route.MaxMetric,
Peer: "peer1",
},
"route2": {
ID: "route2",
Metric: route.MaxMetric,
Peer: "peer2",
},
},
currentRoute: nil,
expectedRouteID: "route1",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// create new clientNetwork
client := &clientNetwork{
network: netip.MustParsePrefix("192.168.0.0/24"),
routes: tc.existingRoutes,
chosenRoute: tc.currentRoute,
}
chosenRoute := client.getBestRouteFromStatuses(tc.statuses)
if chosenRoute != tc.expectedRouteID {
t.Errorf("expected routeID %s, got %s", tc.expectedRouteID, chosenRoute)
}
})
}
}

View File

@@ -1,15 +1,12 @@
//go:build !android
package routemanager
import (
"context"
"fmt"
"github.com/coreos/go-iptables/iptables"
"github.com/google/nftables"
log "github.com/sirupsen/logrus"
)
import "github.com/google/nftables"
const (
ipv6Forwarding = "netbird-rt-ipv6-forwarding"

View File

@@ -1,17 +1,14 @@
//go:build !android
package routemanager
import (
"context"
"fmt"
"github.com/coreos/go-iptables/iptables"
log "github.com/sirupsen/logrus"
"net/netip"
"os/exec"
"strings"
"sync"
"github.com/coreos/go-iptables/iptables"
log "github.com/sirupsen/logrus"
)
func isIptablesSupported() bool {

View File

@@ -1,13 +1,10 @@
//go:build !android
package routemanager
import (
"context"
"testing"
"github.com/coreos/go-iptables/iptables"
"github.com/stretchr/testify/require"
"testing"
)
func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {

View File

@@ -1,167 +1,9 @@
package routemanager
import (
"context"
"runtime"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/version"
)
import "github.com/netbirdio/netbird/route"
// Manager is a route manager interface
type Manager interface {
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error
SetRouteChangeListener(listener RouteListener)
InitialRouteRange() []string
Stop()
}
// DefaultManager is the default instance of a route manager
type DefaultManager struct {
ctx context.Context
stop context.CancelFunc
mux sync.Mutex
clientNetworks map[string]*clientNetwork
serverRouter *serverRouter
statusRecorder *peer.Status
wgInterface *iface.WGIface
pubKey string
notifier *notifier
}
// NewManager returns a new route manager
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status, initialRoutes []*route.Route) *DefaultManager {
mCTX, cancel := context.WithCancel(ctx)
dm := &DefaultManager{
ctx: mCTX,
stop: cancel,
clientNetworks: make(map[string]*clientNetwork),
serverRouter: newServerRouter(ctx, wgInterface),
statusRecorder: statusRecorder,
wgInterface: wgInterface,
pubKey: pubKey,
notifier: newNotifier(),
}
if runtime.GOOS == "android" {
cr := dm.clientRoutes(initialRoutes)
dm.notifier.setInitialClientRoutes(cr)
}
return dm
}
// Stop stops the manager watchers and clean firewall rules
func (m *DefaultManager) Stop() {
m.stop()
m.serverRouter.cleanUp()
m.ctx = nil
}
// UpdateRoutes compares received routes with existing routes and remove, update or add them to the client and server maps
func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error {
select {
case <-m.ctx.Done():
log.Infof("not updating routes as context is closed")
return m.ctx.Err()
default:
m.mux.Lock()
defer m.mux.Unlock()
newServerRoutesMap, newClientRoutesIDMap := m.classifiesRoutes(newRoutes)
m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
m.notifier.onNewRoutes(newClientRoutesIDMap)
err := m.serverRouter.updateRoutes(newServerRoutesMap)
if err != nil {
return err
}
return nil
}
}
// SetRouteChangeListener set RouteListener for route change notifier
func (m *DefaultManager) SetRouteChangeListener(listener RouteListener) {
m.notifier.setListener(listener)
}
// InitialRouteRange return the list of initial routes. It used by mobile systems
func (m *DefaultManager) InitialRouteRange() []string {
return m.notifier.initialRouteRanges()
}
func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[string][]*route.Route) {
// removing routes that do not exist as per the update from the Management service.
for id, client := range m.clientNetworks {
_, found := networks[id]
if !found {
log.Debugf("stopping client network watcher, %s", id)
client.stop()
delete(m.clientNetworks, id)
}
}
for id, routes := range networks {
clientNetworkWatcher, found := m.clientNetworks[id]
if !found {
clientNetworkWatcher = newClientNetworkWatcher(m.ctx, m.wgInterface, m.statusRecorder, routes[0].Network)
m.clientNetworks[id] = clientNetworkWatcher
go clientNetworkWatcher.peersStateAndUpdateWatcher()
}
update := routesUpdate{
updateSerial: updateSerial,
routes: routes,
}
clientNetworkWatcher.sendUpdateToClientNetworkWatcher(update)
}
}
func (m *DefaultManager) classifiesRoutes(newRoutes []*route.Route) (map[string]*route.Route, map[string][]*route.Route) {
newClientRoutesIDMap := make(map[string][]*route.Route)
newServerRoutesMap := make(map[string]*route.Route)
ownNetworkIDs := make(map[string]bool)
for _, newRoute := range newRoutes {
networkID := route.GetHAUniqueID(newRoute)
if newRoute.Peer == m.pubKey {
ownNetworkIDs[networkID] = true
// only linux is supported for now
if runtime.GOOS != "linux" {
log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS)
continue
}
newServerRoutesMap[newRoute.ID] = newRoute
}
}
for _, newRoute := range newRoutes {
networkID := route.GetHAUniqueID(newRoute)
if !ownNetworkIDs[networkID] {
// if prefix is too small, lets assume is a possible default route which is not yet supported
// we skip this route management
if newRoute.Network.Bits() < 7 {
log.Errorf("this agent version: %s, doesn't support default routes, received %s, skiping this route",
version.NetbirdVersion(), newRoute.Network)
continue
}
newClientRoutesIDMap[networkID] = append(newClientRoutesIDMap[networkID], newRoute)
}
}
return newServerRoutesMap, newClientRoutesIDMap
}
func (m *DefaultManager) clientRoutes(initialRoutes []*route.Route) []*route.Route {
_, crMap := m.classifiesRoutes(initialRoutes)
rs := make([]*route.Route, 0)
for _, routes := range crMap {
rs = append(rs, routes...)
}
return rs
}

View File

@@ -0,0 +1,31 @@
package routemanager
import (
"context"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
)
// DefaultManager dummy router manager for Android
type DefaultManager struct {
ctx context.Context
serverRouter *serverRouter
wgInterface *iface.WGIface
}
// NewManager returns a new dummy route manager what doing nothing
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status) *DefaultManager {
return &DefaultManager{}
}
// UpdateRoutes ...
func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error {
return nil
}
// Stop ...
func (m *DefaultManager) Stop() {
}

View File

@@ -0,0 +1,186 @@
//go:build !android
package routemanager
import (
"context"
"fmt"
"runtime"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
"github.com/netbirdio/netbird/version"
)
// DefaultManager is the default instance of a route manager
type DefaultManager struct {
ctx context.Context
stop context.CancelFunc
mux sync.Mutex
clientNetworks map[string]*clientNetwork
serverRoutes map[string]*route.Route
serverRouter *serverRouter
statusRecorder *peer.Status
wgInterface *iface.WGIface
pubKey string
}
// NewManager returns a new route manager
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *peer.Status) *DefaultManager {
mCTX, cancel := context.WithCancel(ctx)
return &DefaultManager{
ctx: mCTX,
stop: cancel,
clientNetworks: make(map[string]*clientNetwork),
serverRoutes: make(map[string]*route.Route),
serverRouter: &serverRouter{
routes: make(map[string]*route.Route),
netForwardHistoryEnabled: isNetForwardHistoryEnabled(),
firewall: NewFirewall(ctx),
},
statusRecorder: statusRecorder,
wgInterface: wgInterface,
pubKey: pubKey,
}
}
// Stop stops the manager watchers and clean firewall rules
func (m *DefaultManager) Stop() {
m.stop()
m.serverRouter.firewall.CleanRoutingRules()
}
func (m *DefaultManager) updateClientNetworks(updateSerial uint64, networks map[string][]*route.Route) {
// removing routes that do not exist as per the update from the Management service.
for id, client := range m.clientNetworks {
_, found := networks[id]
if !found {
log.Debugf("stopping client network watcher, %s", id)
client.stop()
delete(m.clientNetworks, id)
}
}
for id, routes := range networks {
clientNetworkWatcher, found := m.clientNetworks[id]
if !found {
clientNetworkWatcher = newClientNetworkWatcher(m.ctx, m.wgInterface, m.statusRecorder, routes[0].Network)
m.clientNetworks[id] = clientNetworkWatcher
go clientNetworkWatcher.peersStateAndUpdateWatcher()
}
update := routesUpdate{
updateSerial: updateSerial,
routes: routes,
}
clientNetworkWatcher.sendUpdateToClientNetworkWatcher(update)
}
}
func (m *DefaultManager) updateServerRoutes(routesMap map[string]*route.Route) error {
serverRoutesToRemove := make([]string, 0)
if len(routesMap) > 0 {
err := m.serverRouter.firewall.RestoreOrCreateContainers()
if err != nil {
return fmt.Errorf("couldn't initialize firewall containers, got err: %v", err)
}
}
for routeID := range m.serverRoutes {
update, found := routesMap[routeID]
if !found || !update.IsEqual(m.serverRoutes[routeID]) {
serverRoutesToRemove = append(serverRoutesToRemove, routeID)
continue
}
}
for _, routeID := range serverRoutesToRemove {
oldRoute := m.serverRoutes[routeID]
err := m.removeFromServerNetwork(oldRoute)
if err != nil {
log.Errorf("unable to remove route id: %s, network %s, from server, got: %v",
oldRoute.ID, oldRoute.Network, err)
}
delete(m.serverRoutes, routeID)
}
for id, newRoute := range routesMap {
_, found := m.serverRoutes[id]
if found {
continue
}
err := m.addToServerNetwork(newRoute)
if err != nil {
log.Errorf("unable to add route %s from server, got: %v", newRoute.ID, err)
continue
}
m.serverRoutes[id] = newRoute
}
if len(m.serverRoutes) > 0 {
err := enableIPForwarding()
if err != nil {
return err
}
}
return nil
}
// UpdateRoutes compares received routes with existing routes and remove, update or add them to the client and server maps
func (m *DefaultManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error {
select {
case <-m.ctx.Done():
log.Infof("not updating routes as context is closed")
return m.ctx.Err()
default:
m.mux.Lock()
defer m.mux.Unlock()
newClientRoutesIDMap := make(map[string][]*route.Route)
newServerRoutesMap := make(map[string]*route.Route)
ownNetworkIDs := make(map[string]bool)
for _, newRoute := range newRoutes {
networkID := route.GetHAUniqueID(newRoute)
if newRoute.Peer == m.pubKey {
ownNetworkIDs[networkID] = true
// only linux is supported for now
if runtime.GOOS != "linux" {
log.Warnf("received a route to manage, but agent doesn't support router mode on %s OS", runtime.GOOS)
continue
}
newServerRoutesMap[newRoute.ID] = newRoute
}
}
for _, newRoute := range newRoutes {
networkID := route.GetHAUniqueID(newRoute)
if !ownNetworkIDs[networkID] {
// if prefix is too small, lets assume is a possible default route which is not yet supported
// we skip this route management
if newRoute.Network.Bits() < 7 {
log.Errorf("this agent version: %s, doesn't support default routes, received %s, skiping this route",
version.NetbirdVersion(), newRoute.Network)
continue
}
newClientRoutesIDMap[networkID] = append(newClientRoutesIDMap[networkID], newRoute)
}
}
m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
err := m.updateServerRoutes(newServerRoutesMap)
if err != nil {
return err
}
return nil
}
}

View File

@@ -392,7 +392,6 @@ func TestManagerUpdateRoutes(t *testing.T) {
for n, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
@@ -406,7 +405,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
statusRecorder := peer.NewRecorder("https://mgm")
ctx := context.TODO()
routeManager := NewManager(ctx, localPeerKey, wgInterface, statusRecorder, nil)
routeManager := NewManager(ctx, localPeerKey, wgInterface, statusRecorder)
defer routeManager.Stop()
if len(testCase.inputInitRoutes) > 0 {
@@ -420,7 +419,7 @@ func TestManagerUpdateRoutes(t *testing.T) {
require.Len(t, routeManager.clientNetworks, testCase.clientNetworkWatchersExpected, "client networks size should match")
if testCase.shouldCheckServerRoutes {
require.Len(t, routeManager.serverRouter.routes, testCase.serverRoutesExpected, "server networks size should match")
require.Len(t, routeManager.serverRoutes, testCase.serverRoutesExpected, "server networks size should match")
}
})
}

View File

@@ -1,10 +1,7 @@
package routemanager
import (
"context"
"fmt"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
)
@@ -14,11 +11,6 @@ type MockManager struct {
StopFunc func()
}
// InitialRouteRange mock implementation of InitialRouteRange from Manager interface
func (m *MockManager) InitialRouteRange() []string {
return nil
}
// UpdateRoutes mock implementation of UpdateRoutes from Manager interface
func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error {
if m.UpdateRoutesFunc != nil {
@@ -27,15 +19,6 @@ func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route
return fmt.Errorf("method UpdateRoutes is not implemented")
}
// Start mock implementation of Start from Manager interface
func (m *MockManager) Start(ctx context.Context, iface *iface.WGIface) {
}
// SetRouteChangeListener mock implementation of SetRouteChangeListener from Manager interface
func (m *MockManager) SetRouteChangeListener(listener RouteListener) {
}
// Stop mock implementation of Stop from Manager interface
func (m *MockManager) Stop() {
if m.StopFunc != nil {

View File

@@ -1,19 +1,16 @@
//go:build !android
package routemanager
import (
"context"
"fmt"
"net"
"net/netip"
"sync"
"github.com/google/nftables"
"github.com/google/nftables/binaryutil"
"github.com/google/nftables/expr"
log "github.com/sirupsen/logrus"
"net"
"net/netip"
"sync"
)
import "github.com/google/nftables"
const (
nftablesTable = "netbird-rt"

View File

@@ -1,15 +1,12 @@
//go:build !android
package routemanager
import (
"context"
"testing"
"github.com/google/nftables"
"github.com/google/nftables/expr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
func TestNftablesManager_RestoreOrCreateContainers(t *testing.T) {

View File

@@ -1,90 +0,0 @@
package routemanager
import (
"sort"
"sync"
"github.com/netbirdio/netbird/route"
)
// RouteListener is a callback interface for mobile system
type RouteListener interface {
// OnNewRouteSetting invoke when new route setting has been arrived
OnNewRouteSetting()
}
type notifier struct {
initialRouteRangers []string
routeRangers []string
routeListener RouteListener
routeListenerMux sync.Mutex
}
func newNotifier() *notifier {
return &notifier{}
}
func (n *notifier) setListener(listener RouteListener) {
n.routeListenerMux.Lock()
defer n.routeListenerMux.Unlock()
n.routeListener = listener
}
func (n *notifier) setInitialClientRoutes(clientRoutes []*route.Route) {
nets := make([]string, 0)
for _, r := range clientRoutes {
nets = append(nets, r.Network.String())
}
sort.Strings(nets)
n.initialRouteRangers = nets
}
func (n *notifier) onNewRoutes(idMap map[string][]*route.Route) {
newNets := make([]string, 0)
for _, routes := range idMap {
for _, r := range routes {
newNets = append(newNets, r.Network.String())
}
}
sort.Strings(newNets)
if !n.hasDiff(n.routeRangers, newNets) {
return
}
n.routeRangers = newNets
if !n.hasDiff(n.initialRouteRangers, newNets) {
return
}
n.notify()
}
func (n *notifier) notify() {
n.routeListenerMux.Lock()
defer n.routeListenerMux.Unlock()
if n.routeListener == nil {
return
}
go func(l RouteListener) {
l.OnNewRouteSetting()
}(n.routeListener)
}
func (n *notifier) hasDiff(a []string, b []string) bool {
if len(a) != len(b) {
return true
}
for i, v := range a {
if v != b[i] {
return true
}
}
return false
}
func (n *notifier) initialRouteRanges() []string {
return n.initialRouteRangers
}

View File

@@ -1,24 +0,0 @@
package routemanager
import (
"net/netip"
"github.com/netbirdio/netbird/route"
)
type routerPair struct {
ID string
source string
destination string
masquerade bool
}
func routeToRouterPair(source string, route *route.Route) routerPair {
parsed := netip.MustParsePrefix(source).Masked()
return routerPair{
ID: route.ID,
source: parsed.String(),
destination: route.Network.Masked().String(),
masquerade: route.Masquerade,
}
}

View File

@@ -0,0 +1,67 @@
package routemanager
import (
"github.com/netbirdio/netbird/route"
log "github.com/sirupsen/logrus"
"net/netip"
"sync"
)
type serverRouter struct {
routes map[string]*route.Route
// best effort to keep net forward configuration as it was
netForwardHistoryEnabled bool
mux sync.Mutex
firewall firewallManager
}
type routerPair struct {
ID string
source string
destination string
masquerade bool
}
func routeToRouterPair(source string, route *route.Route) routerPair {
parsed := netip.MustParsePrefix(source).Masked()
return routerPair{
ID: route.ID,
source: parsed.String(),
destination: route.Network.Masked().String(),
masquerade: route.Masquerade,
}
}
func (m *DefaultManager) removeFromServerNetwork(route *route.Route) error {
select {
case <-m.ctx.Done():
log.Infof("not removing from server network because context is done")
return m.ctx.Err()
default:
m.serverRouter.mux.Lock()
defer m.serverRouter.mux.Unlock()
err := m.serverRouter.firewall.RemoveRoutingRules(routeToRouterPair(m.wgInterface.Address().String(), route))
if err != nil {
return err
}
delete(m.serverRouter.routes, route.ID)
return nil
}
}
func (m *DefaultManager) addToServerNetwork(route *route.Route) error {
select {
case <-m.ctx.Done():
log.Infof("not adding to server network because context is done")
return m.ctx.Err()
default:
m.serverRouter.mux.Lock()
defer m.serverRouter.mux.Unlock()
err := m.serverRouter.firewall.InsertRoutingRules(routeToRouterPair(m.wgInterface.Address().String(), route))
if err != nil {
return err
}
m.serverRouter.routes[route.ID] = route
return nil
}
}

View File

@@ -1,21 +0,0 @@
package routemanager
import (
"context"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
)
type serverRouter struct {
}
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) *serverRouter {
return &serverRouter{}
}
func (r *serverRouter) updateRoutes(routesMap map[string]*route.Route) error {
return nil
}
func (r *serverRouter) cleanUp() {}

View File

@@ -1,120 +0,0 @@
//go:build !android
package routemanager
import (
"context"
"fmt"
"sync"
log "github.com/sirupsen/logrus"
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/route"
)
type serverRouter struct {
mux sync.Mutex
ctx context.Context
routes map[string]*route.Route
firewall firewallManager
wgInterface *iface.WGIface
}
func newServerRouter(ctx context.Context, wgInterface *iface.WGIface) *serverRouter {
return &serverRouter{
ctx: ctx,
routes: make(map[string]*route.Route),
firewall: NewFirewall(ctx),
wgInterface: wgInterface,
}
}
func (m *serverRouter) updateRoutes(routesMap map[string]*route.Route) error {
serverRoutesToRemove := make([]string, 0)
if len(routesMap) > 0 {
err := m.firewall.RestoreOrCreateContainers()
if err != nil {
return fmt.Errorf("couldn't initialize firewall containers, got err: %v", err)
}
}
for routeID := range m.routes {
update, found := routesMap[routeID]
if !found || !update.IsEqual(m.routes[routeID]) {
serverRoutesToRemove = append(serverRoutesToRemove, routeID)
}
}
for _, routeID := range serverRoutesToRemove {
oldRoute := m.routes[routeID]
err := m.removeFromServerNetwork(oldRoute)
if err != nil {
log.Errorf("unable to remove route id: %s, network %s, from server, got: %v",
oldRoute.ID, oldRoute.Network, err)
}
delete(m.routes, routeID)
}
for id, newRoute := range routesMap {
_, found := m.routes[id]
if found {
continue
}
err := m.addToServerNetwork(newRoute)
if err != nil {
log.Errorf("unable to add route %s from server, got: %v", newRoute.ID, err)
continue
}
m.routes[id] = newRoute
}
if len(m.routes) > 0 {
err := enableIPForwarding()
if err != nil {
return err
}
}
return nil
}
func (m *serverRouter) removeFromServerNetwork(route *route.Route) error {
select {
case <-m.ctx.Done():
log.Infof("not removing from server network because context is done")
return m.ctx.Err()
default:
m.mux.Lock()
defer m.mux.Unlock()
err := m.firewall.RemoveRoutingRules(routeToRouterPair(m.wgInterface.Address().String(), route))
if err != nil {
return err
}
delete(m.routes, route.ID)
return nil
}
}
func (m *serverRouter) addToServerNetwork(route *route.Route) error {
select {
case <-m.ctx.Done():
log.Infof("not adding to server network because context is done")
return m.ctx.Err()
default:
m.mux.Lock()
defer m.mux.Unlock()
err := m.firewall.InsertRoutingRules(routeToRouterPair(m.wgInterface.Address().String(), route))
if err != nil {
return err
}
m.routes[route.ID] = route
return nil
}
}
func (m *serverRouter) cleanUp() {
m.firewall.CleanRoutingRules()
}

View File

@@ -1,39 +1,29 @@
//go:build !android
package routemanager
import (
"fmt"
"net"
"net/netip"
"github.com/libp2p/go-netroute"
log "github.com/sirupsen/logrus"
"net"
"net/netip"
)
var errRouteNotFound = fmt.Errorf("route not found")
func addToRouteTableIfNoExists(prefix netip.Prefix, addr string) error {
defaultGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
gateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
if err != nil && err != errRouteNotFound {
return err
}
prefixGateway, err := getExistingRIBRouteGateway(prefix)
if err != nil && err != errRouteNotFound {
return err
}
gatewayIP := netip.MustParseAddr(defaultGateway.String())
if prefix.Contains(gatewayIP) {
log.Warnf("skipping adding a new route for network %s because it overlaps with the default gateway: %s", prefix, gatewayIP)
if prefixGateway != nil && !prefixGateway.Equal(gateway) {
log.Warnf("skipping adding a new route for network %s because it already exists and is pointing to the non default gateway: %s", prefix, prefixGateway)
return nil
}
ok, err := existsInRouteTable(prefix)
if err != nil {
return err
}
if ok {
log.Warnf("skipping adding a new route for network %s because it already exists", prefix)
return nil
}
return addToRouteTable(prefix, addr)
}
@@ -60,7 +50,6 @@ func getExistingRIBRouteGateway(prefix netip.Prefix) (net.IP, error) {
log.Errorf("getting routes returned an error: %v", err)
return nil, errRouteNotFound
}
if gateway == nil {
return preferredSrc, nil
}

View File

@@ -1,13 +0,0 @@
package routemanager
import (
"net/netip"
)
func addToRouteTableIfNoExists(prefix netip.Prefix, addr string) error {
return nil
}
func removeFromRouteTableIfNonSystem(prefix netip.Prefix, addr string) error {
return nil
}

View File

@@ -1,82 +0,0 @@
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
// +build darwin dragonfly freebsd netbsd openbsd
package routemanager
import (
"fmt"
"net"
"net/netip"
"syscall"
"golang.org/x/net/route"
)
// selected BSD Route flags.
const (
RTF_UP = 0x1
RTF_GATEWAY = 0x2
RTF_HOST = 0x4
RTF_REJECT = 0x8
RTF_DYNAMIC = 0x10
RTF_MODIFIED = 0x20
RTF_STATIC = 0x800
RTF_BLACKHOLE = 0x1000
RTF_LOCAL = 0x200000
RTF_BROADCAST = 0x400000
RTF_MULTICAST = 0x800000
)
func existsInRouteTable(prefix netip.Prefix) (bool, error) {
tab, err := route.FetchRIB(syscall.AF_UNSPEC, route.RIBTypeRoute, 0)
if err != nil {
return false, err
}
msgs, err := route.ParseRIB(route.RIBTypeRoute, tab)
if err != nil {
return false, err
}
for _, msg := range msgs {
m := msg.(*route.RouteMessage)
if m.Version < 3 || m.Version > 5 {
return false, fmt.Errorf("unexpected RIB message version: %d", m.Version)
}
if m.Type != 4 /* RTM_GET */ {
return true, fmt.Errorf("unexpected RIB message type: %d", m.Type)
}
if m.Flags&RTF_UP == 0 ||
m.Flags&(RTF_REJECT|RTF_BLACKHOLE) != 0 {
continue
}
dst, err := toIPAddr(m.Addrs[0])
if err != nil {
return true, fmt.Errorf("unexpected RIB destination: %v", err)
}
mask, _ := toIPAddr(m.Addrs[2])
cidr, _ := net.IPMask(mask.To4()).Size()
if dst.String() == prefix.Addr().String() && cidr == prefix.Bits() {
return true, nil
}
}
return false, nil
}
func toIPAddr(a route.Addr) (net.IP, error) {
switch t := a.(type) {
case *route.Inet4Addr:
ip := net.IPv4(t.IP[0], t.IP[1], t.IP[2], t.IP[3])
return ip, nil
case *route.Inet6Addr:
ip := make(net.IP, net.IPv6len)
copy(ip, t.IP[:])
return ip, nil
default:
return net.IP{}, fmt.Errorf("unknown family: %v", t)
}
}

View File

@@ -1,33 +1,12 @@
//go:build !android
package routemanager
import (
"github.com/vishvananda/netlink"
"net"
"net/netip"
"os"
"syscall"
"unsafe"
"github.com/vishvananda/netlink"
)
// Pulled from http://man7.org/linux/man-pages/man7/rtnetlink.7.html
// See the section on RTM_NEWROUTE, specifically 'struct rtmsg'.
type routeInfoInMemory struct {
Family byte
DstLen byte
SrcLen byte
TOS byte
Table byte
Protocol byte
Scope byte
Type byte
Flags uint32
}
const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
func addToRouteTable(prefix netip.Prefix, addr string) error {
@@ -79,56 +58,16 @@ func removeFromRouteTable(prefix netip.Prefix) error {
return nil
}
func existsInRouteTable(prefix netip.Prefix) (bool, error) {
tab, err := syscall.NetlinkRIB(syscall.RTM_GETROUTE, syscall.AF_UNSPEC)
if err != nil {
return true, err
}
msgs, err := syscall.ParseNetlinkMessage(tab)
if err != nil {
return true, err
}
loop:
for _, m := range msgs {
switch m.Header.Type {
case syscall.NLMSG_DONE:
break loop
case syscall.RTM_NEWROUTE:
rt := (*routeInfoInMemory)(unsafe.Pointer(&m.Data[0]))
attrs, err := syscall.ParseNetlinkRouteAttr(&m)
if err != nil {
return true, err
}
if rt.Family != syscall.AF_INET {
continue loop
}
for _, attr := range attrs {
if attr.Attr.Type == syscall.RTA_DST {
ip := net.IP(attr.Value)
mask := net.CIDRMask(int(rt.DstLen), len(attr.Value)*8)
cidr, _ := mask.Size()
if ip.String() == prefix.Addr().String() && cidr == prefix.Bits() {
return true, nil
}
}
}
}
}
return false, nil
}
func enableIPForwarding() error {
bytes, err := os.ReadFile(ipv4ForwardingPath)
if err != nil {
return err
}
// check if it is already enabled
// see more: https://github.com/netbirdio/netbird/issues/872
if len(bytes) > 0 && bytes[0] == 49 {
return nil
}
return os.WriteFile(ipv4ForwardingPath, []byte("1"), 0644)
err := os.WriteFile(ipv4ForwardingPath, []byte("1"), 0644)
return err
}
func isNetForwardHistoryEnabled() bool {
out, err := os.ReadFile(ipv4ForwardingPath)
if err != nil {
// todo
panic(err)
}
return string(out) == "1"
}

View File

@@ -4,11 +4,10 @@
package routemanager
import (
log "github.com/sirupsen/logrus"
"net/netip"
"os/exec"
"runtime"
log "github.com/sirupsen/logrus"
)
func addToRouteTable(prefix netip.Prefix, addr string) error {
@@ -35,3 +34,8 @@ func enableIPForwarding() error {
log.Infof("enable IP forwarding is not implemented on %s", runtime.GOOS)
return nil
}
func isNetForwardHistoryEnabled() bool {
log.Infof("check netforward history is not implemented on %s", runtime.GOOS)
return false
}

View File

@@ -1,19 +1,13 @@
package routemanager
import (
"bytes"
"fmt"
"github.com/netbirdio/netbird/iface"
"github.com/pion/transport/v2/stdnet"
"github.com/stretchr/testify/require"
"net"
"net/netip"
"os"
"strings"
"testing"
"github.com/pion/transport/v2/stdnet"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/iface"
)
func TestAddRemoveRoutes(t *testing.T) {
@@ -120,98 +114,3 @@ func TestGetExistingRIBRouteGateway(t *testing.T) {
t.Fatalf("local ip should match with testing IP: want %s got %s", testingIP, localIP.String())
}
}
func TestAddExistAndRemoveRouteNonAndroid(t *testing.T) {
defaultGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
fmt.Println("defaultGateway: ", defaultGateway)
if err != nil {
t.Fatal("shouldn't return error when fetching the gateway: ", err)
}
testCases := []struct {
name string
prefix netip.Prefix
preExistingPrefix netip.Prefix
shouldAddRoute bool
}{
{
name: "Should Add And Remove random Route",
prefix: netip.MustParsePrefix("99.99.99.99/32"),
shouldAddRoute: true,
},
{
name: "Should Not Add Route if overlaps with default gateway",
prefix: netip.MustParsePrefix(defaultGateway.String() + "/31"),
shouldAddRoute: false,
},
{
name: "Should Add Route if bigger network exists",
prefix: netip.MustParsePrefix("100.100.100.0/24"),
preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"),
shouldAddRoute: true,
},
{
name: "Should Add Route if smaller network exists",
prefix: netip.MustParsePrefix("100.100.0.0/16"),
preExistingPrefix: netip.MustParsePrefix("100.100.100.0/24"),
shouldAddRoute: true,
},
{
name: "Should Not Add Route if same network exists",
prefix: netip.MustParsePrefix("100.100.0.0/16"),
preExistingPrefix: netip.MustParsePrefix("100.100.0.0/16"),
shouldAddRoute: false,
},
}
for n, testCase := range testCases {
var buf bytes.Buffer
log.SetOutput(&buf)
defer func() {
log.SetOutput(os.Stderr)
}()
t.Run(testCase.name, func(t *testing.T) {
newNet, err := stdnet.NewNet()
if err != nil {
t.Fatal(err)
}
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun53%d", n), "100.65.75.2/24", iface.DefaultMTU, nil, newNet)
require.NoError(t, err, "should create testing WGIface interface")
defer wgInterface.Close()
err = wgInterface.Create()
require.NoError(t, err, "should create testing wireguard interface")
MockAddr := wgInterface.Address().IP.String()
// Prepare the environment
if testCase.preExistingPrefix.IsValid() {
err := addToRouteTableIfNoExists(testCase.preExistingPrefix, MockAddr)
require.NoError(t, err, "should not return err when adding pre-existing route")
}
// Add the route
err = addToRouteTableIfNoExists(testCase.prefix, MockAddr)
require.NoError(t, err, "should not return err when adding route")
if testCase.shouldAddRoute {
// test if route exists after adding
ok, err := existsInRouteTable(testCase.prefix)
require.NoError(t, err, "should not return err")
require.True(t, ok, "route should exist")
// remove route again if added
err = removeFromRouteTableIfNonSystem(testCase.prefix, MockAddr)
require.NoError(t, err, "should not return err")
}
// route should either not have been added or should have been removed
// In case of already existing route, it should not have been added (but still exist)
ok, err := existsInRouteTable(testCase.prefix)
fmt.Println("Buffer string: ", buf.String())
require.NoError(t, err, "should not return err")
if !strings.Contains(buf.String(), "because it already exists") {
require.False(t, ok, "route should not exist")
}
})
}
}

View File

@@ -1,37 +0,0 @@
//go:build windows
// +build windows
package routemanager
import (
"net"
"net/netip"
"github.com/yusufpapurcu/wmi"
)
type Win32_IP4RouteTable struct {
Destination string
Mask string
}
func existsInRouteTable(prefix netip.Prefix) (bool, error) {
var routes []Win32_IP4RouteTable
query := "SELECT Destination, Mask FROM Win32_IP4RouteTable"
err := wmi.Query(query, &routes)
if err != nil {
return true, err
}
for _, route := range routes {
ip := net.ParseIP(route.Mask)
ip = ip.To4()
mask := net.IPv4Mask(ip[0], ip[1], ip[2], ip[3])
cidr, _ := mask.Size()
if route.Destination == prefix.Addr().String() && cidr == prefix.Bits() {
return true, nil
}
}
return false, nil
}

View File

@@ -78,9 +78,6 @@ func (m *mobileIFaceDiscover) parseInterfacesString(interfaces string) []*transp
addrs := strings.Trim(fields[1], " \n")
foundAddress := false
for _, addr := range strings.Split(addrs, " ") {
if strings.Contains(addr, "%") {
continue
}
ip, ipNet, err := net.ParseCIDR(addr)
if err != nil {
log.Warnf("%s", err)

View File

@@ -3,8 +3,6 @@ package stdnet
import (
"fmt"
"testing"
log "github.com/sirupsen/logrus"
)
func Test_parseInterfacesString(t *testing.T) {
@@ -22,7 +20,6 @@ func Test_parseInterfacesString(t *testing.T) {
{"wlan0", 30, 1500, true, true, false, false, true, "10.1.10.131/24"},
{"rmnet0", 30, 1500, true, true, false, false, true, "192.168.0.56/24"},
{"rmnet_data1", 30, 1500, true, true, false, false, true, "fec0::118c:faf7:8d97:3cb2/64"},
{"rmnet_data2", 30, 1500, true, true, false, false, true, "fec0::118c:faf7:8d97:3cb2%rmnet2/64"},
}
var exampleString string
@@ -38,13 +35,13 @@ func Test_parseInterfacesString(t *testing.T) {
d.multicast,
d.addr)
}
d := mobileIFaceDiscover{}
nets := d.parseInterfacesString(exampleString)
if len(nets) == 0 {
t.Fatalf("failed to parse interfaces")
}
log.Printf("%d", len(nets))
for i, net := range nets {
if net.MTU != testData[i].mtu {
t.Errorf("invalid mtu: %d, expected: %d", net.MTU, testData[0].mtu)
@@ -63,7 +60,7 @@ func Test_parseInterfacesString(t *testing.T) {
if len(addr) == 0 {
t.Errorf("invalid address parsing")
}
log.Printf("%v", addr)
if addr[0].String() != testData[i].addr {
t.Errorf("invalid address: %s, expected: %s", addr[0].String(), testData[i].addr)
}

View File

@@ -1,9 +1,8 @@
package main
import (
"os"
"github.com/netbirdio/netbird/client/cmd"
"os"
)
func main() {

View File

@@ -102,7 +102,7 @@ func (s *Server) Start() error {
}
go func() {
if err := internal.RunClient(ctx, config, s.statusRecorder, nil, nil, nil); err != nil {
if err := internal.RunClient(ctx, config, s.statusRecorder, nil, nil); err != nil {
log.Errorf("init connections: %v", err)
}
}()
@@ -236,9 +236,7 @@ func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*pro
}, nil
} else {
log.Warnf("canceling previous waiting execution")
if s.oauthAuthFlow.waitCancel != nil {
s.oauthAuthFlow.waitCancel()
}
s.oauthAuthFlow.waitCancel()
}
}
@@ -391,7 +389,7 @@ func (s *Server) Up(callerCtx context.Context, _ *proto.UpRequest) (*proto.UpRes
}
go func() {
if err := internal.RunClient(ctx, s.config, s.statusRecorder, nil, nil, nil); err != nil {
if err := internal.RunClient(ctx, s.config, s.statusRecorder, nil, nil); err != nil {
log.Errorf("run client connection: %v", err)
return
}

View File

@@ -43,15 +43,6 @@ func extractUserAgent(ctx context.Context) string {
return ""
}
// extractDeviceName extracts device name from context or returns the default system name
func extractDeviceName(ctx context.Context, defaultName string) string {
v, ok := ctx.Value(DeviceNameCtxKey).(string)
if !ok {
return defaultName
}
return v
}
// GetDesktopUIUserAgent returns the Desktop ui user agent
func GetDesktopUIUserAgent() string {
return "netbird-desktop-ui/" + version.NetbirdVersion()

View File

@@ -24,13 +24,21 @@ func GetInfo(ctx context.Context) *Info {
}
gio := &Info{Kernel: kernel, Core: osVersion(), Platform: "unknown", OS: "android", OSVersion: osVersion(), GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
gio.Hostname = extractDeviceName(ctx, "android")
gio.Hostname = extractDeviceName(ctx)
gio.WiretrusteeVersion = version.NetbirdVersion()
gio.UIVersion = extractUserAgent(ctx)
return gio
}
func extractDeviceName(ctx context.Context) string {
v, ok := ctx.Value(DeviceNameCtxKey).(string)
if !ok {
return "android"
}
return v
}
func uname() []string {
res := run("/system/bin/uname", "-a")
return strings.Split(res, " ")

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