mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
Compare commits
481 Commits
v0.0.7
...
feature/up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45de3c1b06 | ||
|
|
04e4407ea7 | ||
|
|
06055af361 | ||
|
|
abd1230a69 | ||
|
|
f7de12daf8 | ||
|
|
c49fb0c40c | ||
|
|
6e9a162877 | ||
|
|
b4e03f4616 | ||
|
|
369a7ef345 | ||
|
|
c88e6a7342 | ||
|
|
2cd9b11e7d | ||
|
|
93d20e370b | ||
|
|
878ca6db22 | ||
|
|
2033650908 | ||
|
|
34c1c7d901 | ||
|
|
051fd3a4d7 | ||
|
|
af69a48745 | ||
|
|
68ff97ba84 | ||
|
|
c5705803a5 | ||
|
|
7e1ae448e0 | ||
|
|
518a2561a2 | ||
|
|
c75ffd0f4b | ||
|
|
e4ad6174ca | ||
|
|
6de313070a | ||
|
|
cd7d1a80c9 | ||
|
|
be7d829858 | ||
|
|
ed1872560f | ||
|
|
de898899a4 | ||
|
|
b63ec71aed | ||
|
|
1012172f04 | ||
|
|
788bb00ef1 | ||
|
|
4e5ee70b3d | ||
|
|
f1c00ae543 | ||
|
|
553a13588b | ||
|
|
586c0f5c3d | ||
|
|
c13f0b9f07 | ||
|
|
dd4ff61b51 | ||
|
|
e3657610bc | ||
|
|
e8733a37af | ||
|
|
3def84b111 | ||
|
|
47add9a9c3 | ||
|
|
09312b3e6d | ||
|
|
762a26dcea | ||
|
|
000ea72aec | ||
|
|
4b34a6d6df | ||
|
|
c39cd2f7b0 | ||
|
|
6dc3e8ca90 | ||
|
|
245863cd51 | ||
|
|
14e322d3f7 | ||
|
|
1be8c16e34 | ||
|
|
851de3fd4e | ||
|
|
c13288781f | ||
|
|
e34e0ccd12 | ||
|
|
95dc9cc16c | ||
|
|
d1c2b3d703 | ||
|
|
966661fe91 | ||
|
|
67ddaade58 | ||
|
|
138cf35e00 | ||
|
|
2555a6c3e8 | ||
|
|
86a66c6202 | ||
|
|
275d364df6 | ||
|
|
a3c5fa1307 | ||
|
|
75a69ca26b | ||
|
|
ae8e3ad6fe | ||
|
|
ff729f6755 | ||
|
|
7e1b20da5d | ||
|
|
d4a3ee9d87 | ||
|
|
49e9113e0f | ||
|
|
3bdfa3cc8e | ||
|
|
8c953c5a2c | ||
|
|
e95f0f7acb | ||
|
|
fa7b413fe7 | ||
|
|
295f0c755a | ||
|
|
a98f6f840a | ||
|
|
faad5a1e98 | ||
|
|
e8caa562b0 | ||
|
|
1aafc15607 | ||
|
|
06860c4c10 | ||
|
|
f883a10535 | ||
|
|
8ec7f1cd96 | ||
|
|
aae84e40e2 | ||
|
|
5623735234 | ||
|
|
f9f2d7c7ef | ||
|
|
35c7cae267 | ||
|
|
503a116f7c | ||
|
|
a454a1aa28 | ||
|
|
a88ac40b05 | ||
|
|
bfff6110aa | ||
|
|
f810feafdf | ||
|
|
57536da245 | ||
|
|
c9b5328f19 | ||
|
|
dab146ed87 | ||
|
|
b96e616844 | ||
|
|
0cba0f81e0 | ||
|
|
ebd70a569c | ||
|
|
e7b43253b0 | ||
|
|
d005cd32b0 | ||
|
|
fa0399d975 | ||
|
|
e6e9f0322f | ||
|
|
60ac8c3268 | ||
|
|
2e5d4ba6fa | ||
|
|
0fbe78375e | ||
|
|
87631cbc8b | ||
|
|
ec39202590 | ||
|
|
b227a7c34e | ||
|
|
c86bacb5c3 | ||
|
|
59a964eed8 | ||
|
|
feff6dc966 | ||
|
|
258cb3d43b | ||
|
|
4088aaf6fe | ||
|
|
1bb504ea78 | ||
|
|
594da0a6b8 | ||
|
|
889fa646fc | ||
|
|
59ae10a66d | ||
|
|
3e4b779d7b | ||
|
|
98c764c095 | ||
|
|
e5c429af1a | ||
|
|
4b5e6b93a6 | ||
|
|
2c087cd254 | ||
|
|
94fbfcdb85 | ||
|
|
5e3eceb0d6 | ||
|
|
65069c1787 | ||
|
|
abe78666d4 | ||
|
|
5cbfa4bb9e | ||
|
|
32611e1131 | ||
|
|
e334e8db53 | ||
|
|
3eb230e1a0 | ||
|
|
3ce3ccc39a | ||
|
|
11a3863c28 | ||
|
|
3992fe4743 | ||
|
|
6ce8a13ffa | ||
|
|
001cf98dce | ||
|
|
77e58295e7 | ||
|
|
7d893c0238 | ||
|
|
b623c255b6 | ||
|
|
e5c52efb4c | ||
|
|
49cca57565 | ||
|
|
7e5449fb55 | ||
|
|
fec3132585 | ||
|
|
fbf778a221 | ||
|
|
c7e5e5c7c9 | ||
|
|
219888254e | ||
|
|
70ffc9d625 | ||
|
|
17fbbbea2a | ||
|
|
f5933660ba | ||
|
|
951e011a9c | ||
|
|
196207402d | ||
|
|
83e743d704 | ||
|
|
c3bc85e22d | ||
|
|
ede2795529 | ||
|
|
a0d5a8fb9c | ||
|
|
2aaeeac7f6 | ||
|
|
a15d52b263 | ||
|
|
97ab8f4c34 | ||
|
|
cf336bd49d | ||
|
|
a2fc4ec221 | ||
|
|
76db9afa11 | ||
|
|
4ef3c7a637 | ||
|
|
bd61be24be | ||
|
|
1cd1e84290 | ||
|
|
957474817f | ||
|
|
3a69f334e8 | ||
|
|
1660a915e2 | ||
|
|
e3b809a1d4 | ||
|
|
b2f4322a31 | ||
|
|
d7b69b91b9 | ||
|
|
a3a6283ac6 | ||
|
|
be0c5c887c | ||
|
|
8cc93e0dbe | ||
|
|
24d5f9efac | ||
|
|
c1b162c974 | ||
|
|
612ef98f03 | ||
|
|
605ca03519 | ||
|
|
ff62fec956 | ||
|
|
347a668bd5 | ||
|
|
ef47385e38 | ||
|
|
3e46f38166 | ||
|
|
64e2e34dae | ||
|
|
8dd92f14bf | ||
|
|
071b03e790 | ||
|
|
3385ea6379 | ||
|
|
430e0415df | ||
|
|
b72ed91cb4 | ||
|
|
0b8387bd2c | ||
|
|
5d4c2643a3 | ||
|
|
69cda73bbb | ||
|
|
b29948b910 | ||
|
|
5f5cbf7e20 | ||
|
|
41c6af6b6f | ||
|
|
23fad49756 | ||
|
|
5546eba36a | ||
|
|
60a9da734f | ||
|
|
852c7c50c0 | ||
|
|
1c2c1a876b | ||
|
|
e5dcd4753e | ||
|
|
765d3a0ad0 | ||
|
|
97e4f9a801 | ||
|
|
d468718d00 | ||
|
|
15e371b592 | ||
|
|
cd9a418df2 | ||
|
|
919f0aa3da | ||
|
|
b59fd50226 | ||
|
|
3c959bb178 | ||
|
|
efbb5acf63 | ||
|
|
b339a9321a | ||
|
|
b045865d6e | ||
|
|
8680f16abd | ||
|
|
98dc5824ce | ||
|
|
0739038d51 | ||
|
|
8ab6eb1cf4 | ||
|
|
30625c68a9 | ||
|
|
fd7282d3cf | ||
|
|
2ad899b066 | ||
|
|
dfa67410b5 | ||
|
|
23f028e65d | ||
|
|
5db130a12e | ||
|
|
9a3fba3fa3 | ||
|
|
0f7ab4354b | ||
|
|
64f2d295a8 | ||
|
|
afb302d5e7 | ||
|
|
9d1ecbbfb2 | ||
|
|
bafa71fc2e | ||
|
|
319632ffe8 | ||
|
|
828410b34c | ||
|
|
4d2b194570 | ||
|
|
a67b9a16af | ||
|
|
6ae27c9a9b | ||
|
|
ff6e369a21 | ||
|
|
5c3b5e7f40 | ||
|
|
8c75ef8bef | ||
|
|
fdc11fff47 | ||
|
|
3dca2d6953 | ||
|
|
6b7d4cf644 | ||
|
|
edd4125742 | ||
|
|
7bf9793f85 | ||
|
|
fcbf980588 | ||
|
|
d08e5efbce | ||
|
|
95ef8547f3 | ||
|
|
ed1e4dfc51 | ||
|
|
4d34fb4e64 | ||
|
|
1fb8b74cd2 | ||
|
|
d040cfed7e | ||
|
|
2c729fe5cc | ||
|
|
e9066b4651 | ||
|
|
673e807528 | ||
|
|
892080bc38 | ||
|
|
2d39f6ccae | ||
|
|
0b2c26847b | ||
|
|
595ea0d4f8 | ||
|
|
f714868fdd | ||
|
|
81821a1f39 | ||
|
|
842b143a48 | ||
|
|
1323a74db0 | ||
|
|
74485d3b13 | ||
|
|
bef3b3392b | ||
|
|
fcea3c99d4 | ||
|
|
96799a25b5 | ||
|
|
07291cdb93 | ||
|
|
21139938c1 | ||
|
|
5cf2d0a6a9 | ||
|
|
8551afe04e | ||
|
|
1685817171 | ||
|
|
e17f662683 | ||
|
|
a764fb870c | ||
|
|
cabff941ac | ||
|
|
b5f35dfb5e | ||
|
|
1d426b7f81 | ||
|
|
e4f9406d44 | ||
|
|
7c79ff62ee | ||
|
|
32c369257b | ||
|
|
08dd719aa1 | ||
|
|
84c714dd93 | ||
|
|
996c8d7c62 | ||
|
|
25e68ce493 | ||
|
|
4881dcbd51 | ||
|
|
d505f70972 | ||
|
|
6a80684378 | ||
|
|
2624a7c4e6 | ||
|
|
9a412e7bf1 | ||
|
|
b5d1690129 | ||
|
|
d4bec15ca3 | ||
|
|
3212aca7c7 | ||
|
|
b97a2251d3 | ||
|
|
528a26ea3e | ||
|
|
13288374f1 | ||
|
|
ec759bc461 | ||
|
|
a859f6c511 | ||
|
|
081162864d | ||
|
|
090f3ae5d0 | ||
|
|
fb1116e77b | ||
|
|
879750af7c | ||
|
|
13b4be31df | ||
|
|
15f7d856db | ||
|
|
72197d1970 | ||
|
|
a56aba8b06 | ||
|
|
4acbdc47e5 | ||
|
|
ee3c292699 | ||
|
|
6c233fcc3f | ||
|
|
a4db0b4e94 | ||
|
|
81c5aa1341 | ||
|
|
8c5f6186f1 | ||
|
|
88e9d2c20d | ||
|
|
9d76cf1ea7 | ||
|
|
737d4b5f2c | ||
|
|
4485124b67 | ||
|
|
b17424d630 | ||
|
|
86f3b1e5c8 | ||
|
|
a31cbb1f5b | ||
|
|
4f4edf8442 | ||
|
|
a78e518327 | ||
|
|
2c1d7c0fd4 | ||
|
|
593c66fea6 | ||
|
|
5f8211773d | ||
|
|
64ca05c8e7 | ||
|
|
307d41c08a | ||
|
|
5c7260298f | ||
|
|
7f7858b0a6 | ||
|
|
3c4b0b3a4b | ||
|
|
d4a24ac001 | ||
|
|
737866c149 | ||
|
|
49800a6d03 | ||
|
|
0fa15e6920 | ||
|
|
95845c88fe | ||
|
|
6869b48905 | ||
|
|
90ef1e939b | ||
|
|
bff137b109 | ||
|
|
2e9fc20567 | ||
|
|
617f79e2e0 | ||
|
|
d75353fbb8 | ||
|
|
b5a20bf1ba | ||
|
|
695148410f | ||
|
|
07ab9c196d | ||
|
|
4a5901ada1 | ||
|
|
4c427ae900 | ||
|
|
22fdb0a029 | ||
|
|
1b056ab75a | ||
|
|
3a41014adb | ||
|
|
708835afa8 | ||
|
|
8364e03944 | ||
|
|
0017360b8d | ||
|
|
9ace93d9fc | ||
|
|
b127e424f9 | ||
|
|
34cffb3bf0 | ||
|
|
02cc6a30f5 | ||
|
|
c68d9dff4a | ||
|
|
1dfa99d07c | ||
|
|
f7e51e7453 | ||
|
|
2c6748610c | ||
|
|
e8ca289f4a | ||
|
|
2a97053cae | ||
|
|
38e3c9c062 | ||
|
|
877ad97a96 | ||
|
|
80de6a75d5 | ||
|
|
dcc9dcacdc | ||
|
|
3c47a3c408 | ||
|
|
d5af5f1878 | ||
|
|
9f0c86c28e | ||
|
|
08d44b1d5f | ||
|
|
1f29975737 | ||
|
|
11982d6dde | ||
|
|
6ce5b2c815 | ||
|
|
ea99def502 | ||
|
|
f51a79d3b3 | ||
|
|
2c2c1e19df | ||
|
|
c0c4c4a266 | ||
|
|
3b30beb567 | ||
|
|
9e4aa4f1f1 | ||
|
|
83ac774264 | ||
|
|
2172d6f1b9 | ||
|
|
c98be683bf | ||
|
|
079d35eada | ||
|
|
d27eb317aa | ||
|
|
940578d600 | ||
|
|
1a8c03bef0 | ||
|
|
4e17890597 | ||
|
|
7b52049333 | ||
|
|
f9c3ed784f | ||
|
|
ea524e2a53 | ||
|
|
bffea0e145 | ||
|
|
2d85fcfcc3 | ||
|
|
07118d972d | ||
|
|
84f4d51c6c | ||
|
|
1e250fc0df | ||
|
|
d4a9f4d38a | ||
|
|
4587f7686e | ||
|
|
dd50f495ab | ||
|
|
bb2477491f | ||
|
|
f4d7faaf4e | ||
|
|
cffb08ad23 | ||
|
|
8d05789749 | ||
|
|
ca5970140f | ||
|
|
ac628b6efa | ||
|
|
80665049dc | ||
|
|
881f078759 | ||
|
|
1cf9b143e0 | ||
|
|
158547f3eb | ||
|
|
ab6452065d | ||
|
|
e553c5e97e | ||
|
|
3041ff4ef7 | ||
|
|
61a7f3013b | ||
|
|
dac865c61f | ||
|
|
a40669270a | ||
|
|
f2ca2fc7c1 | ||
|
|
729b16e599 | ||
|
|
561bd681d9 | ||
|
|
0e313eec24 | ||
|
|
4216cd2986 | ||
|
|
c18899d135 | ||
|
|
20248dadb7 | ||
|
|
1a06518f1b | ||
|
|
dd72a01ecf | ||
|
|
bbfbf797d5 | ||
|
|
52db303104 | ||
|
|
5122294adf | ||
|
|
a87f828844 | ||
|
|
8088c7a591 | ||
|
|
74355a2292 | ||
|
|
a66cdccda9 | ||
|
|
06c7af058b | ||
|
|
41b50a08d4 | ||
|
|
3c45da553a | ||
|
|
8dfccfc800 | ||
|
|
021092800b | ||
|
|
aa854c5899 | ||
|
|
e41fdedd5b | ||
|
|
923cabda9a | ||
|
|
db673ed34f | ||
|
|
6465e2556a | ||
|
|
89dba7951a | ||
|
|
9308a51800 | ||
|
|
94c0091a7b | ||
|
|
f247f9a2f8 | ||
|
|
c49bd23ac5 | ||
|
|
11174a50cd | ||
|
|
dfcf9f9087 | ||
|
|
5f8a489f90 | ||
|
|
9b9c7ada7d | ||
|
|
8b31088968 | ||
|
|
00f2ee34a0 | ||
|
|
51337fbf65 | ||
|
|
ca83e8c4a0 | ||
|
|
2784f6a098 | ||
|
|
6b5010f7d5 | ||
|
|
714c4c3c44 | ||
|
|
d5c4f6cb40 | ||
|
|
7df6cde968 | ||
|
|
744984861b | ||
|
|
83fe84d11a | ||
|
|
e059059e62 | ||
|
|
06b0c46a5d | ||
|
|
8acddfd510 | ||
|
|
caf2229d3b | ||
|
|
698ebe2287 | ||
|
|
54235f0a77 | ||
|
|
05168ae12f | ||
|
|
255ad7faa9 | ||
|
|
6e4c232ff2 | ||
|
|
59360519d6 | ||
|
|
3520b6471b | ||
|
|
74061597a3 | ||
|
|
33a98c7a2c | ||
|
|
9b327ea6ba | ||
|
|
45697a0000 | ||
|
|
884cd8dc55 | ||
|
|
f8eaf2f40e | ||
|
|
0609e1d75d | ||
|
|
8c9bc96c85 | ||
|
|
68112870dc | ||
|
|
ae69f4cf1b | ||
|
|
c8ad10d653 | ||
|
|
e622b2a529 | ||
|
|
44d5e7f205 | ||
|
|
790858c31b | ||
|
|
5342f10e7f | ||
|
|
f0048d16fb | ||
|
|
84c6eb5e16 | ||
|
|
73720951d7 | ||
|
|
6d339295be | ||
|
|
f1cff0e13a | ||
|
|
e6358e7bb2 | ||
|
|
2337c3d84d |
32
.github/workflows/golang-test-darwin.yml
vendored
Normal file
32
.github/workflows/golang-test-darwin.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: Test Code Darwin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: macos-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
macos-go-
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
||||||
92
.github/workflows/golang-test-linux.yml
vendored
Normal file
92
.github/workflows/golang-test-linux.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
name: Test Code Linux
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: ['386','amd64']
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
||||||
|
|
||||||
|
test_client_on_docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: Generate Iface Test bin
|
||||||
|
run: go test -c -o iface-testing.bin ./iface/...
|
||||||
|
|
||||||
|
- name: Generate RouteManager Test bin
|
||||||
|
run: go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
|
||||||
|
|
||||||
|
- name: Generate Engine Test bin
|
||||||
|
run: go test -c -o engine-testing.bin ./client/internal/*.go
|
||||||
|
|
||||||
|
- name: Generate Peer Test bin
|
||||||
|
run: go test -c -o peer-testing.bin ./client/internal/peer/...
|
||||||
|
|
||||||
|
- run: chmod +x *testing.bin
|
||||||
|
|
||||||
|
- 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 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
|
||||||
53
.github/workflows/golang-test-windows.yml
vendored
Normal file
53
.github/workflows/golang-test-windows.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Test Code Windows
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- run: bash -x wireguard_nt.sh
|
||||||
|
working-directory: client
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: syso
|
||||||
|
path: client/*.syso
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
test:
|
||||||
|
needs: pre
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
%LocalAppData%\go-build
|
||||||
|
~\go\pkg\mod
|
||||||
|
~\AppData\Local\go-build
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: syso
|
||||||
|
path: iface\
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -tags=load_wgnt_from_rsrc -timeout 5m -p 1 ./...
|
||||||
21
.github/workflows/golangci-lint.yml
vendored
Normal file
21
.github/workflows/golangci-lint.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: golangci-lint
|
||||||
|
on: [pull_request]
|
||||||
|
jobs:
|
||||||
|
golangci:
|
||||||
|
name: lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v2
|
||||||
|
with:
|
||||||
|
# SA1019: "io/ioutil" has been deprecated since Go 1.16
|
||||||
|
args: --timeout=6m -e SA1019
|
||||||
|
|
||||||
|
|
||||||
152
.github/workflows/release.yml
vendored
152
.github/workflows/release.yml
vendored
@@ -4,6 +4,13 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*'
|
- 'v*'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
env:
|
||||||
|
SIGN_PIPE_VER: "v0.0.3"
|
||||||
|
GORELEASER_VER: "v1.6.3"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -14,11 +21,15 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # It is required for GoReleaser to work properly
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
|
||||||
|
- name: Generate syso with DLL
|
||||||
|
run: bash -x wireguard_nt.sh
|
||||||
|
working-directory: client
|
||||||
-
|
-
|
||||||
name: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
go-version: 1.16
|
go-version: 1.18
|
||||||
-
|
-
|
||||||
name: Cache Go modules
|
name: Cache Go modules
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
@@ -30,6 +41,9 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install modules
|
name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
-
|
||||||
|
name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
-
|
-
|
||||||
name: Set up QEMU
|
name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
@@ -38,15 +52,145 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v1
|
||||||
-
|
-
|
||||||
name: Login to Docker hub
|
name: Login to Docker hub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v1
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: netbirdio
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
-
|
-
|
||||||
name: Run GoReleaser
|
name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v2
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --rm-dist
|
args: release --rm-dist
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
|
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
|
-
|
||||||
|
name: upload non tags for debug purposes
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: release
|
||||||
|
path: dist/
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
release_ui:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ui-go-
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libappindicator3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-mingw-w64-x86-64
|
||||||
|
- name: Install rsrc
|
||||||
|
run: go install github.com/akavel/rsrc@v0.10.2
|
||||||
|
- name: Generate windows rsrc
|
||||||
|
run: rsrc -arch amd64 -ico client/ui/netbird.ico -manifest client/ui/manifest.xml -o client/ui/resources_windows_amd64.syso
|
||||||
|
- name: Run GoReleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v2
|
||||||
|
with:
|
||||||
|
version: ${{ env.GORELEASER_VER }}
|
||||||
|
args: release --config .goreleaser_ui.yaml --rm-dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
|
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
|
- name: upload non tags for debug purposes
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: release-ui
|
||||||
|
path: dist/
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
release_ui_darwin:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # It is required for GoReleaser to work properly
|
||||||
|
-
|
||||||
|
name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
-
|
||||||
|
name: Cache Go modules
|
||||||
|
uses: actions/cache@v1
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ui-go-
|
||||||
|
-
|
||||||
|
name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
-
|
||||||
|
name: Run GoReleaser
|
||||||
|
id: goreleaser
|
||||||
|
uses: goreleaser/goreleaser-action@v2
|
||||||
|
with:
|
||||||
|
version: ${{ env.GORELEASER_VER }}
|
||||||
|
args: release --config .goreleaser_ui_darwin.yaml --rm-dist
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
-
|
||||||
|
name: upload non tags for debug purposes
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: release-ui-darwin
|
||||||
|
path: dist/
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
trigger_windows_signer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release,release_ui]
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Trigger Windows binaries sign pipeline
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: Sign windows bin and installer
|
||||||
|
repo: netbirdio/sign-pipelines
|
||||||
|
ref: ${{ env.SIGN_PIPE_VER }}
|
||||||
|
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||||
|
|
||||||
|
trigger_darwin_signer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: release_ui_darwin
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Trigger Darwin App binaries sign pipeline
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: Sign darwin ui app with dispatch
|
||||||
|
repo: netbirdio/sign-pipelines
|
||||||
|
ref: ${{ env.SIGN_PIPE_VER }}
|
||||||
|
token: ${{ secrets.SIGN_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref }}" }'
|
||||||
80
.github/workflows/test-docker-compose-linux.yml
vendored
Normal file
80
.github/workflows/test-docker-compose-linux.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
name: Test Docker Compose Linux
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Install curl
|
||||||
|
run: sudo apt-get install -y curl
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.18.x
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: cp setup.env
|
||||||
|
run: cp infrastructure_files/tests/setup.env infrastructure_files/
|
||||||
|
|
||||||
|
- name: run configure
|
||||||
|
working-directory: infrastructure_files
|
||||||
|
run: bash -x configure.sh
|
||||||
|
env:
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_ID: ${{ secrets.CI_NETBIRD_AUTH_CLIENT_ID }}
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: check values
|
||||||
|
working-directory: infrastructure_files
|
||||||
|
env:
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_ID: ${{ secrets.CI_NETBIRD_AUTH_CLIENT_ID }}
|
||||||
|
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_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified"
|
||||||
|
CI_NETBIRD_AUTH_AUTHORITY: https://example.eu.auth0.com/
|
||||||
|
CI_NETBIRD_AUTH_JWT_CERTS: https://example.eu.auth0.com/.well-known/jwks.json
|
||||||
|
CI_NETBIRD_AUTH_TOKEN_ENDPOINT: https://example.eu.auth0.com/oauth/token
|
||||||
|
CI_NETBIRD_AUTH_DEVICE_AUTH_ENDPOINT: https://example.eu.auth0.com/oauth/device/code
|
||||||
|
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
|
||||||
|
run: |
|
||||||
|
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
||||||
|
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"
|
||||||
|
grep USE_AUTH0 docker-compose.yml | grep $CI_NETBIRD_USE_AUTH0
|
||||||
|
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "http://localhost:33073"
|
||||||
|
grep AUTH_REDIRECT_URI docker-compose.yml | grep $CI_NETBIRD_AUTH_REDIRECT_URI
|
||||||
|
grep AUTH_SILENT_REDIRECT_URI docker-compose.yml | egrep 'AUTH_SILENT_REDIRECT_URI=$'
|
||||||
|
|
||||||
|
- name: run docker compose up
|
||||||
|
working-directory: infrastructure_files
|
||||||
|
run: |
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
- name: test running containers
|
||||||
|
run: |
|
||||||
|
count=$(docker compose ps --format json | jq '.[] | select(.Project | contains("infrastructure_files")) | .State' | grep -c running)
|
||||||
|
test $count -eq 4
|
||||||
|
working-directory: infrastructure_files
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -1,2 +1,13 @@
|
|||||||
.idea
|
.idea
|
||||||
*.iml
|
.run
|
||||||
|
*.iml
|
||||||
|
dist/
|
||||||
|
bin/
|
||||||
|
.env
|
||||||
|
conf.json
|
||||||
|
http-cmds.sh
|
||||||
|
infrastructure_files/management.json
|
||||||
|
infrastructure_files/docker-compose.yml
|
||||||
|
*.syso
|
||||||
|
client/.distfiles/
|
||||||
|
infrastructure_files/setup.env
|
||||||
355
.goreleaser.yaml
355
.goreleaser.yaml
@@ -1,39 +1,115 @@
|
|||||||
project_name: wiretrustee
|
project_name: netbird
|
||||||
builds:
|
builds:
|
||||||
- env: [CGO_ENABLED=0]
|
- id: netbird
|
||||||
|
dir: client
|
||||||
|
binary: netbird
|
||||||
|
env: [CGO_ENABLED=0]
|
||||||
goos:
|
goos:
|
||||||
- linux
|
- linux
|
||||||
- darwin
|
- darwin
|
||||||
|
- windows
|
||||||
goarch:
|
goarch:
|
||||||
- arm
|
- arm
|
||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
|
- mips
|
||||||
|
- 386
|
||||||
|
gomips:
|
||||||
|
- hardfloat
|
||||||
|
- softfloat
|
||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
- goos: windows
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
|
- goos: windows
|
||||||
|
goarch: 386
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/client/system.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: [CGO_ENABLED=0]
|
||||||
|
binary: netbird-mgmt
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
|
- id: netbird-signal
|
||||||
|
dir: signal
|
||||||
|
env: [CGO_ENABLED=0]
|
||||||
|
binary: netbird-signal
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- builds:
|
||||||
|
- netbird
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
- maintainer: Wiretrustee <wiretrustee@wiretrustee.com>
|
|
||||||
description: Wiretrustee project.
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
homepage: https://wiretrustee.com/
|
description: Netbird client.
|
||||||
|
homepage: https://netbird.io/
|
||||||
|
id: netbird-deb
|
||||||
|
bindir: /usr/bin
|
||||||
|
builds:
|
||||||
|
- netbird
|
||||||
formats:
|
formats:
|
||||||
- deb
|
- deb
|
||||||
- rpm
|
|
||||||
contents:
|
|
||||||
- src: release_files/wiretrustee.service
|
|
||||||
dst: /lib/systemd/system/wiretrustee.service
|
|
||||||
|
|
||||||
- src: release_files/wiretrustee.json
|
replaces:
|
||||||
dst: /etc/wiretrustee/wiretrustee.json
|
- wiretrustee
|
||||||
type: "config|noreplace"
|
conflicts:
|
||||||
|
- wiretrustee
|
||||||
|
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: "release_files/post_install.sh"
|
postinstall: "release_files/post_install.sh"
|
||||||
|
preremove: "release_files/pre_remove.sh"
|
||||||
|
|
||||||
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
|
description: Netbird client.
|
||||||
|
homepage: https://netbird.io/
|
||||||
|
id: netbird-rpm
|
||||||
|
bindir: /usr/bin
|
||||||
|
builds:
|
||||||
|
- netbird
|
||||||
|
formats:
|
||||||
|
- rpm
|
||||||
|
|
||||||
|
replaces:
|
||||||
|
- wiretrustee
|
||||||
|
|
||||||
|
conflicts:
|
||||||
|
- wiretrustee
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
postinstall: "release_files/post_install.sh"
|
||||||
|
preremove: "release_files/pre_remove.sh"
|
||||||
dockers:
|
dockers:
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- wiretrustee/wiretrustee:signal-{{ .Version }}-amd64
|
- netbirdio/netbird:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
use_buildx: true
|
use: buildx
|
||||||
dockerfile: Dockerfile
|
dockerfile: client/Dockerfile
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--platform=linux/amd64"
|
- "--platform=linux/amd64"
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
@@ -41,12 +117,14 @@ dockers:
|
|||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- "--label=maintainer=wiretrustee@wiretrustee.com"
|
- "--label=maintainer=dev@netbird.io"
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- wiretrustee/wiretrustee:signal-{{ .Version }}-arm64v8
|
- netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
use_buildx: true
|
use: buildx
|
||||||
dockerfile: Dockerfile
|
dockerfile: client/Dockerfile
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--platform=linux/arm64"
|
- "--platform=linux/arm64"
|
||||||
- "--label=org.opencontainers.image.created={{.Date}}"
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
@@ -54,15 +132,238 @@ dockers:
|
|||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
- "--label=maintainer=wiretrustee@wiretrustee.com"
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/netbird:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: client/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-signal
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: signal/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-signal
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: signal/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-signal
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: signal/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-amd64
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: amd64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile.debug
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/amd64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm64
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile.debug
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm64"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
|
|
||||||
|
- image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm
|
||||||
|
ids:
|
||||||
|
- netbird-mgmt
|
||||||
|
goarch: arm
|
||||||
|
goarm: 6
|
||||||
|
use: buildx
|
||||||
|
dockerfile: management/Dockerfile.debug
|
||||||
|
build_flag_templates:
|
||||||
|
- "--platform=linux/arm"
|
||||||
|
- "--label=org.opencontainers.image.created={{.Date}}"
|
||||||
|
- "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||||
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
|
- "--label=maintainer=dev@netbird.io"
|
||||||
docker_manifests:
|
docker_manifests:
|
||||||
- name_template: wiretrustee/wiretrustee:signal-{{ .Version }}
|
- name_template: netbirdio/netbird:{{ .Version }}
|
||||||
image_templates:
|
image_templates:
|
||||||
- wiretrustee/wiretrustee:signal-{{ .Version }}-arm64v8
|
- netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
- wiretrustee/wiretrustee:signal-{{ .Version }}-amd64
|
- netbirdio/netbird:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird:{{ .Version }}-amd64
|
||||||
|
|
||||||
- name_template: wiretrustee/wiretrustee:signal-latest
|
- name_template: netbirdio/netbird:latest
|
||||||
image_templates:
|
image_templates:
|
||||||
- wiretrustee/wiretrustee:signal-{{ .Version }}-arm64v8
|
- netbirdio/netbird:{{ .Version }}-arm64v8
|
||||||
- wiretrustee/wiretrustee:signal-{{ .Version }}-amd64
|
- netbirdio/netbird:{{ .Version }}-arm
|
||||||
|
- netbirdio/netbird:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/signal:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm
|
||||||
|
- netbirdio/signal:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/signal:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/signal:{{ .Version }}-arm
|
||||||
|
- netbirdio/signal:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/management:{{ .Version }}
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/management:{{ .Version }}-arm
|
||||||
|
- netbirdio/management:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/management:latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-arm64v8
|
||||||
|
- netbirdio/management:{{ .Version }}-arm
|
||||||
|
- netbirdio/management:{{ .Version }}-amd64
|
||||||
|
|
||||||
|
- name_template: netbirdio/management:debug-latest
|
||||||
|
image_templates:
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm64v8
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-arm
|
||||||
|
- netbirdio/management:{{ .Version }}-debug-amd64
|
||||||
|
|
||||||
|
brews:
|
||||||
|
-
|
||||||
|
ids:
|
||||||
|
- default
|
||||||
|
tap:
|
||||||
|
owner: netbirdio
|
||||||
|
name: homebrew-tap
|
||||||
|
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
|
||||||
|
commit_author:
|
||||||
|
name: Netbird
|
||||||
|
email: dev@netbird.io
|
||||||
|
description: Netbird project.
|
||||||
|
download_strategy: CurlDownloadStrategy
|
||||||
|
homepage: https://netbird.io/
|
||||||
|
license: "BSD3"
|
||||||
|
test: |
|
||||||
|
system "#{bin}/{{ .ProjectName }} version"
|
||||||
|
conflicts:
|
||||||
|
- wiretrustee
|
||||||
|
|
||||||
|
uploads:
|
||||||
|
- name: debian
|
||||||
|
ids:
|
||||||
|
- netbird-deb
|
||||||
|
mode: archive
|
||||||
|
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=stable;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
|
||||||
|
username: dev@wiretrustee.com
|
||||||
|
method: PUT
|
||||||
|
|
||||||
|
- name: yum
|
||||||
|
ids:
|
||||||
|
- netbird-rpm
|
||||||
|
mode: archive
|
||||||
|
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||||
|
username: dev@wiretrustee.com
|
||||||
|
method: PUT
|
||||||
98
.goreleaser_ui.yaml
Normal file
98
.goreleaser_ui.yaml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
project_name: netbird-ui
|
||||||
|
builds:
|
||||||
|
- id: netbird-ui
|
||||||
|
dir: client/ui
|
||||||
|
binary: netbird-ui
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
|
- id: netbird-ui-windows
|
||||||
|
dir: client/ui
|
||||||
|
binary: netbird-ui
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
- CC=x86_64-w64-mingw32-gcc
|
||||||
|
goos:
|
||||||
|
- windows
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
- -H windowsgui
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- id: linux-arch
|
||||||
|
name_template: "{{ .ProjectName }}-linux_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
builds:
|
||||||
|
- netbird-ui
|
||||||
|
- id: windows-arch
|
||||||
|
name_template: "{{ .ProjectName }}-windows_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
builds:
|
||||||
|
- netbird-ui-windows
|
||||||
|
|
||||||
|
nfpms:
|
||||||
|
|
||||||
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
|
description: Netbird client UI.
|
||||||
|
homepage: https://netbird.io/
|
||||||
|
id: netbird-ui-deb
|
||||||
|
package_name: netbird-ui
|
||||||
|
builds:
|
||||||
|
- netbird-ui
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
contents:
|
||||||
|
- src: client/ui/netbird.desktop
|
||||||
|
dst: /usr/share/applications/netbird.desktop
|
||||||
|
- src: client/ui/disconnected.png
|
||||||
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
|
dependencies:
|
||||||
|
- libayatana-appindicator3-1
|
||||||
|
- libgtk-3-dev
|
||||||
|
- libappindicator3-dev
|
||||||
|
- netbird
|
||||||
|
|
||||||
|
- maintainer: Netbird <dev@netbird.io>
|
||||||
|
description: Netbird client UI.
|
||||||
|
homepage: https://netbird.io/
|
||||||
|
id: netbird-ui-rpm
|
||||||
|
package_name: netbird-ui
|
||||||
|
builds:
|
||||||
|
- netbird-ui
|
||||||
|
formats:
|
||||||
|
- rpm
|
||||||
|
contents:
|
||||||
|
- src: client/ui/netbird.desktop
|
||||||
|
dst: /usr/share/applications/netbird.desktop
|
||||||
|
- src: client/ui/disconnected.png
|
||||||
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
|
dependencies:
|
||||||
|
- libayatana-appindicator3-1
|
||||||
|
- libgtk-3-dev
|
||||||
|
- libappindicator3-dev
|
||||||
|
- netbird
|
||||||
|
|
||||||
|
uploads:
|
||||||
|
- name: debian
|
||||||
|
ids:
|
||||||
|
- netbird-ui-deb
|
||||||
|
mode: archive
|
||||||
|
target: https://pkgs.wiretrustee.com/debian/pool/{{ .ArtifactName }};deb.distribution=stable;deb.component=main;deb.architecture={{ if .Arm }}armhf{{ else }}{{ .Arch }}{{ end }};deb.package=
|
||||||
|
username: dev@wiretrustee.com
|
||||||
|
method: PUT
|
||||||
|
|
||||||
|
- name: yum
|
||||||
|
ids:
|
||||||
|
- netbird-ui-rpm
|
||||||
|
mode: archive
|
||||||
|
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||||
|
username: dev@wiretrustee.com
|
||||||
|
method: PUT
|
||||||
29
.goreleaser_ui_darwin.yaml
Normal file
29
.goreleaser_ui_darwin.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
project_name: netbird-ui
|
||||||
|
builds:
|
||||||
|
- id: netbird-ui-darwin
|
||||||
|
dir: client/ui
|
||||||
|
binary: netbird-ui
|
||||||
|
env: [CGO_ENABLED=1]
|
||||||
|
|
||||||
|
goos:
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
gomips:
|
||||||
|
- hardfloat
|
||||||
|
- softfloat
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X github.com/netbirdio/netbird/client/system.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.CommitDate}} -X main.builtBy=goreleaser
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
tags:
|
||||||
|
- load_wgnt_from_rsrc
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- builds:
|
||||||
|
- netbird-ui-darwin
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: "{{ .ProjectName }}_darwin_checksums.txt"
|
||||||
|
changelog:
|
||||||
|
skip: true
|
||||||
1
AUTHORS
1
AUTHORS
@@ -1,2 +1,3 @@
|
|||||||
Mikhail Bragin (https://github.com/braginini)
|
Mikhail Bragin (https://github.com/braginini)
|
||||||
Maycon Santos (https://github.com/mlsmaycon)
|
Maycon Santos (https://github.com/mlsmaycon)
|
||||||
|
Wiretrustee UG (haftungsbeschränkt)
|
||||||
|
|||||||
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
|
community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
|
any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email address,
|
||||||
|
without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
dev@wiretrustee.com.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series of
|
||||||
|
actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
|
ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
|
community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.1, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
FROM gcr.io/distroless/base:debug
|
|
||||||
EXPOSE 10000
|
|
||||||
ENTRYPOINT [ "/go/bin/wiretrustee","signal" ]
|
|
||||||
CMD ["--log-level","DEBUG"]
|
|
||||||
COPY wiretrustee /go/bin/wiretrustee
|
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2021 Wiretrustee AUTHORS
|
Copyright (c) 2022 Wiretrustee UG (haftungsbeschränkt) & AUTHORS
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|||||||
185
README.md
185
README.md
@@ -1,83 +1,108 @@
|
|||||||
# Wiretrustee
|
<p align="center">
|
||||||
|
<strong>:hatching_chick: New release! NetBird Easy SSH</strong>.
|
||||||
|
<a href="https://github.com/netbirdio/netbird/releases/tag/v0.8.0">
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<br/>
|
||||||
|
<div align="center">
|
||||||
|
<p align="center">
|
||||||
|
<img width="234" src="docs/media/logo-full.png"/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/netbirdio/netbird/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/badge/license-BSD--3-blue" />
|
||||||
|
</a>
|
||||||
|
<a href="https://www.codacy.com/gh/netbirdio/netbird/dashboard?utm_source=github.com&utm_medium=referral&utm_content=netbirdio/netbird&utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/e3013d046aec44cdb7462c8673b00976"/></a>
|
||||||
|
<br>
|
||||||
|
<a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">
|
||||||
|
<img src="https://img.shields.io/badge/slack-@netbird-red.svg?logo=slack"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
A WireGuard®-based mesh network that connects your devices into a single private network.
|
|
||||||
|
|
||||||
### Why using Wiretrustee?
|
<p align="center">
|
||||||
|
<strong>
|
||||||
* Connect multiple devices to each other via a secure peer-to-peer Wireguard VPN tunnel. At home, the office, or anywhere else.
|
Start using NetBird at <a href="https://app.netbird.io/">app.netbird.io</a>
|
||||||
* No need to open ports and expose public IPs on the device.
|
<br/>
|
||||||
* Automatically reconnects in case of network failures or switches.
|
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||||
* Automatic NAT traversal.
|
<br/>
|
||||||
* Relay server fallback in case of an unsuccessful peer-to-peer connection.
|
Join our <a href="https://join.slack.com/t/netbirdio/shared_invite/zt-vrahf41g-ik1v7fV8du6t0RwxSrJ96A">Slack channel</a>
|
||||||
* Private key never leaves your device.
|
<br/>
|
||||||
* Works on ARM devices (e.g. Raspberry Pi).
|
|
||||||
|
|
||||||
### A bit on Wiretrustee internals
|
|
||||||
* Wiretrustee uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between devices.
|
|
||||||
* A connection session negotiation between peers is achieved with the Wiretrustee Signalling server [signal](signal/)
|
|
||||||
* Contents of the messages sent between peers through the signaling server are encrypted with Wireguard keys, making it impossible to inspect them.
|
|
||||||
The routing of the messages on a Signalling server is based on public Wireguard keys.
|
|
||||||
* Occasionally, the NAT-traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT).
|
|
||||||
For that matter, there is support for a relay server fallback (TURN) and a secure Wireguard tunnel is established via TURN server.
|
|
||||||
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in Wiretrustee setups.
|
|
||||||
|
|
||||||
### What Wiretrustee is not doing:
|
|
||||||
* Wireguard key management. In consequence, you need to generate peer keys and specify them on Wiretrustee initialization step.
|
|
||||||
* Peer address management. You have to specify a unique peer local address (e.g. 10.30.30.1/24) when configuring Wiretrustee
|
|
||||||
|
|
||||||
### Client Installation
|
|
||||||
1. Checkout Wiretrustee [releases](https://github.com/wiretrustee/wiretrustee/releases)
|
|
||||||
2. Download the latest release:
|
|
||||||
```shell
|
|
||||||
wget https://github.com/wiretrustee/wiretrustee/releases/download/v0.0.4/wiretrustee_0.0.4_linux_amd64.rpm
|
|
||||||
```
|
|
||||||
3. Install the package
|
|
||||||
```shell
|
|
||||||
sudo dpkg -i wiretrustee_0.0.4_linux_amd64.deb
|
|
||||||
```
|
|
||||||
### Client Configuration
|
|
||||||
1. Initialize Wiretrustee:
|
|
||||||
```shell
|
|
||||||
sudo wiretrustee init \
|
|
||||||
--stunURLs stun:stun.wiretrustee.com:3468,stun:stun.l.google.com:19302 \
|
|
||||||
--turnURLs <TURN User>:<TURN password>@turn:stun.wiretrustee.com:3468 \
|
|
||||||
--signalAddr signal.wiretrustee.com:10000 \
|
|
||||||
--wgLocalAddr 10.30.30.1/24 \
|
|
||||||
--log-level info
|
|
||||||
```
|
|
||||||
It is important to mention that the ```wgLocalAddr``` parameter has to be unique across your network.
|
|
||||||
E.g. if you have Peer A with ```wgLocalAddr=10.30.30.1/24``` then another Peer B can have ```wgLocalAddr=10.30.30.2/24```
|
|
||||||
|
|
||||||
If for some reason, you already have a generated Wireguard key, you can specify it with the ```--wgKey``` parameter.
|
|
||||||
If not specified, then a new one will be generated, and its corresponding public key will be output to the log.
|
|
||||||
A new config will be generated and stored under ```/etc/wiretrustee/config.json```
|
|
||||||
|
|
||||||
2. Add a peer to connect to.
|
|
||||||
```shell
|
|
||||||
sudo wiretrustee add-peer --allowedIPs 10.30.30.2/32 --key '<REMOTE PEER WIREUARD PUBLIC KEY>'
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Restart Wiretrustee to reload changes
|
|
||||||
```shell
|
|
||||||
sudo systemctl restart wiretrustee.service
|
|
||||||
sudo systemctl status wiretrustee.service
|
|
||||||
```
|
|
||||||
### Running the Signal service
|
|
||||||
After installing the application, you can run the signal using the command below:
|
|
||||||
````shell
|
|
||||||
/usr/local/bin/wiretrustee signal --log-level INFO
|
|
||||||
````
|
|
||||||
This will launch the signal service on port 10000, in case you want to change the port, use the flag --port.
|
|
||||||
#### Docker image
|
|
||||||
We have packed the signal into docker images. You can pull the images from the Docker Hub and execute it with the following commands:
|
|
||||||
````shell
|
|
||||||
docker pull wiretrustee/wiretrustee:signal-latest
|
|
||||||
docker run -d --name wiretrustee-signal -p 10000:10000 wiretrustee/wiretrustee:signal-latest
|
|
||||||
````
|
|
||||||
The default log-level is set to INFO, if you need you can change it using by updating the docker cmd as followed:
|
|
||||||
````shell
|
|
||||||
docker run -d --name wiretrustee-signal -p 10000:10000 wiretrustee/wiretrustee:signal-latest --log-level DEBUG
|
|
||||||
````
|
|
||||||
### Roadmap
|
|
||||||
* Android app
|
|
||||||
|
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
**NetBird is an open-source VPN management platform built on top of WireGuard® making it easy to create secure private networks for your organization or home.**
|
||||||
|
|
||||||
|
It requires zero configuration effort leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|
||||||
|
NetBird creates an overlay peer-to-peer network connecting machines automatically regardless of their location (home, office, datacenter, container, cloud or edge environments) unifying virtual private network management experience.
|
||||||
|
|
||||||
|
**Key features:**
|
||||||
|
- \[x] Automatic IP allocation and network management with a Web UI ([separate repo](https://github.com/netbirdio/dashboard))
|
||||||
|
- \[x] Automatic WireGuard peer (machine) discovery and configuration.
|
||||||
|
- \[x] Encrypted peer-to-peer connections without a central VPN gateway.
|
||||||
|
- \[x] Connection relay fallback in case a peer-to-peer connection is not possible.
|
||||||
|
- \[x] Desktop client applications for Linux, MacOS, and Windows (systray).
|
||||||
|
- \[x] Multiuser support - sharing network between multiple users.
|
||||||
|
- \[x] SSO and MFA support.
|
||||||
|
- \[x] Multicloud and hybrid-cloud support.
|
||||||
|
- \[x] Kernel WireGuard usage when possible.
|
||||||
|
- \[x] Access Controls - groups & rules.
|
||||||
|
- \[x] Remote SSH access without managing SSH keys.
|
||||||
|
- \[x] Network Routes.
|
||||||
|
|
||||||
|
**Coming soon:**
|
||||||
|
- \[ ] Private DNS.
|
||||||
|
- \[ ] Mobile clients.
|
||||||
|
- \[ ] Network Activity Monitoring.
|
||||||
|
|
||||||
|
### Secure peer-to-peer VPN with SSO and MFA in minutes
|
||||||
|
<p float="left" align="middle">
|
||||||
|
<img src="docs/media/netbird-sso-mfa-demo.gif" width="800"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
||||||
|
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
||||||
|
|
||||||
|
### Start using NetBird
|
||||||
|
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
||||||
|
- See our documentation for [Quickstart Guide](https://netbird.io/docs/getting-started/quickstart).
|
||||||
|
- If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://netbird.io/docs/getting-started/self-hosting).
|
||||||
|
- Step-by-step [Installation Guide](https://netbird.io/docs/getting-started/installation) for different platforms.
|
||||||
|
- Web UI [repository](https://github.com/netbirdio/dashboard).
|
||||||
|
- 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
||||||
|
|
||||||
|
|
||||||
|
### A bit on NetBird internals
|
||||||
|
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
||||||
|
- Every agent connects to [Management Service](management/) that holds network state, manages peer IPs, and distributes network updates to agents (peers).
|
||||||
|
- NetBird agent uses WebRTC ICE implemented in [pion/ice library](https://github.com/pion/ice) to discover connection candidates when establishing a peer-to-peer connection between machines.
|
||||||
|
- Connection candidates are discovered with a help of [STUN](https://en.wikipedia.org/wiki/STUN) servers.
|
||||||
|
- Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages with candidates.
|
||||||
|
- Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and p2p connection isn't possible. When this occurs the system falls back to a relay server called [TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT), and a secure WireGuard tunnel is established via the TURN server.
|
||||||
|
|
||||||
|
[Coturn](https://github.com/coturn/coturn) is the one that has been successfully used for STUN and TURN in NetBird setups.
|
||||||
|
|
||||||
|
<p float="left" align="middle">
|
||||||
|
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
See a complete [architecture overview](https://netbird.io/docs/overview/architecture) for details.
|
||||||
|
|
||||||
|
### Roadmap
|
||||||
|
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
|
||||||
|
|
||||||
|
### Community projects
|
||||||
|
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
|
||||||
|
|
||||||
|
### Testimonials
|
||||||
|
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), and [Coturn](https://github.com/coturn/coturn). We very much appreciate the work these guys are doing and we'd greatly appreciate if you could support them in any way (e.g. giving a star or a contribution).
|
||||||
|
|
||||||
|
### Legal
|
||||||
|
[WireGuard](https://wireguard.com/) is a registered trademark of Jason A. Donenfeld.
|
||||||
|
|
||||||
|
|||||||
7
client/Dockerfile
Normal file
7
client/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM gcr.io/distroless/base:debug
|
||||||
|
ENV WT_LOG_FILE=console
|
||||||
|
ENV PATH=/sbin:/usr/sbin:/bin:/usr/bin:/busybox
|
||||||
|
SHELL ["/busybox/sh","-c"]
|
||||||
|
RUN sed -i -E 's/(^root:.+)\/sbin\/nologin/\1\/busybox\/sh/g' /etc/passwd
|
||||||
|
ENTRYPOINT [ "/go/bin/netbird","up"]
|
||||||
|
COPY netbird /go/bin/netbird
|
||||||
46
client/cmd/down.go
Normal file
46
client/cmd/down.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var downCmd = &cobra.Command{
|
||||||
|
Use: "down",
|
||||||
|
Short: "down netbird connections",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := util.InitLog(logLevel, "console")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed initializing log %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to connect to service CLI interface %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
daemonClient := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
|
if _, err := daemonClient.Down(ctx, &proto.DownRequest{}); err != nil {
|
||||||
|
log.Errorf("call service down method: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
203
client/cmd/login.go
Normal file
203
client/cmd/login.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var loginCmd = &cobra.Command{
|
||||||
|
Use: "login",
|
||||||
|
Short: "login to the Netbird Management Service (first run)",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := util.InitLog(logLevel, "console")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(context.Background())
|
||||||
|
|
||||||
|
// workaround to run without service
|
||||||
|
if logFile == "console" {
|
||||||
|
err = handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := internal.GetConfig(managementURL, adminURL, configPath, preSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
||||||
|
|
||||||
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
|
}
|
||||||
|
cmd.Println("Logging successfully")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
|
"If the daemon is not running please run: "+
|
||||||
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
|
loginRequest := proto.LoginRequest{
|
||||||
|
SetupKey: setupKey,
|
||||||
|
PreSharedKey: preSharedKey,
|
||||||
|
ManagementUrl: managementURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginErr error
|
||||||
|
|
||||||
|
var loginResp *proto.LoginResponse
|
||||||
|
|
||||||
|
err = WithBackOff(func() error {
|
||||||
|
var backOffErr error
|
||||||
|
loginResp, backOffErr = client.Login(ctx, &loginRequest)
|
||||||
|
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
||||||
|
s.Code() == codes.PermissionDenied ||
|
||||||
|
s.Code() == codes.NotFound ||
|
||||||
|
s.Code() == codes.Unimplemented) {
|
||||||
|
loginErr = backOffErr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backOffErr
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("login backoff cycle failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginErr != nil {
|
||||||
|
return fmt.Errorf("login failed: %v", loginErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.NeedsSSOLogin {
|
||||||
|
openURL(cmd, loginResp.VerificationURIComplete)
|
||||||
|
|
||||||
|
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("waiting sso login failed with: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Logging successfully")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.Config, setupKey string) error {
|
||||||
|
needsLogin := false
|
||||||
|
|
||||||
|
err := WithBackOff(func() error {
|
||||||
|
err := internal.Login(ctx, config, "", "")
|
||||||
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||||
|
needsLogin = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("backoff cycle failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwtToken := ""
|
||||||
|
if setupKey == "" && needsLogin {
|
||||||
|
tokenInfo, err := foregroundGetTokenInfo(ctx, cmd, config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("interactive sso login failed: %v", err)
|
||||||
|
}
|
||||||
|
jwtToken = tokenInfo.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
err = WithBackOff(func() error {
|
||||||
|
err := internal.Login(ctx, config, setupKey, jwtToken)
|
||||||
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("backoff cycle failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*internal.TokenInfo, error) {
|
||||||
|
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
s, ok := gstatus.FromError(err)
|
||||||
|
if ok && s.Code() == codes.NotFound {
|
||||||
|
return nil, fmt.Errorf("no SSO provider returned from management. " +
|
||||||
|
"If you are using hosting Netbird see documentation at " +
|
||||||
|
"https://github.com/netbirdio/netbird/tree/main/management for details")
|
||||||
|
} else if ok && s.Code() == codes.Unimplemented {
|
||||||
|
mgmtURL := managementURL
|
||||||
|
if mgmtURL == "" {
|
||||||
|
mgmtURL = internal.ManagementURLDefault().String()
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
||||||
|
"please update your servver or use Setup Keys to login", mgmtURL)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostedClient := internal.NewHostedDeviceFlow(
|
||||||
|
providerConfig.ProviderConfig.Audience,
|
||||||
|
providerConfig.ProviderConfig.ClientID,
|
||||||
|
providerConfig.ProviderConfig.TokenEndpoint,
|
||||||
|
providerConfig.ProviderConfig.DeviceAuthEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
flowInfo, err := hostedClient.RequestDeviceCode(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("getting a request device code failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
openURL(cmd, flowInfo.VerificationURIComplete)
|
||||||
|
|
||||||
|
waitTimeout := time.Duration(flowInfo.ExpiresIn)
|
||||||
|
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
|
||||||
|
defer c()
|
||||||
|
|
||||||
|
tokenInfo, err := hostedClient.WaitToken(waitCTX, flowInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("waiting for browser login failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tokenInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + " \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")
|
||||||
|
}
|
||||||
|
}
|
||||||
53
client/cmd/login_test.go
Normal file
53
client/cmd/login_test.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLogin(t *testing.T) {
|
||||||
|
mgmAddr := startTestingServices(t)
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
confPath := tempDir + "/config.json"
|
||||||
|
mgmtURL := fmt.Sprintf("http://%s", mgmAddr)
|
||||||
|
rootCmd.SetArgs([]string{
|
||||||
|
"login",
|
||||||
|
"--config",
|
||||||
|
confPath,
|
||||||
|
"--log-file",
|
||||||
|
"console",
|
||||||
|
"--setup-key",
|
||||||
|
strings.ToUpper("a2c8e62b-38f5-4553-b31e-dd66c696cebb"),
|
||||||
|
"--management-url",
|
||||||
|
mgmtURL,
|
||||||
|
})
|
||||||
|
err := rootCmd.Execute()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate generated config
|
||||||
|
actualConf := &internal.Config{}
|
||||||
|
_, err = util.ReadJson(confPath, actualConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected proper config file written, got broken %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualConf.ManagementURL.String() != mgmtURL {
|
||||||
|
t.Errorf("expected management URL %s got %s", mgmtURL, actualConf.ManagementURL.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if actualConf.WgIface != iface.WgInterfaceDefault {
|
||||||
|
t.Errorf("expected WgIfaceName %s got %s", iface.WgInterfaceDefault, actualConf.WgIface)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(actualConf.PrivateKey) == 0 {
|
||||||
|
t.Errorf("expected non empty Private key, got empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
289
client/cmd/root.go
Normal file
289
client/cmd/root.go
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configPath string
|
||||||
|
defaultConfigPathDir string
|
||||||
|
defaultConfigPath string
|
||||||
|
oldDefaultConfigPathDir string
|
||||||
|
oldDefaultConfigPath string
|
||||||
|
logLevel string
|
||||||
|
defaultLogFileDir string
|
||||||
|
defaultLogFile string
|
||||||
|
oldDefaultLogFileDir string
|
||||||
|
oldDefaultLogFile string
|
||||||
|
logFile string
|
||||||
|
daemonAddr string
|
||||||
|
managementURL string
|
||||||
|
adminURL string
|
||||||
|
setupKey string
|
||||||
|
preSharedKey string
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "netbird",
|
||||||
|
Short: "",
|
||||||
|
Long: "",
|
||||||
|
SilenceUsage: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Execute executes the root command.
|
||||||
|
func Execute() error {
|
||||||
|
return rootCmd.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
defaultConfigPathDir = "/etc/netbird/"
|
||||||
|
defaultLogFileDir = "/var/log/netbird/"
|
||||||
|
|
||||||
|
oldDefaultConfigPathDir = "/etc/wiretrustee/"
|
||||||
|
oldDefaultLogFileDir = "/var/log/wiretrustee/"
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
defaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\"
|
||||||
|
defaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Netbird\\"
|
||||||
|
|
||||||
|
oldDefaultConfigPathDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\"
|
||||||
|
oldDefaultLogFileDir = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfigPath = defaultConfigPathDir + "config.json"
|
||||||
|
defaultLogFile = defaultLogFileDir + "client.log"
|
||||||
|
|
||||||
|
oldDefaultConfigPath = oldDefaultConfigPathDir + "config.json"
|
||||||
|
oldDefaultLogFile = oldDefaultLogFileDir + "client.log"
|
||||||
|
|
||||||
|
defaultDaemonAddr := "unix:///var/run/netbird.sock"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
||||||
|
}
|
||||||
|
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&managementURL, "management-url", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.ManagementURLDefault().String()))
|
||||||
|
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "https://app.netbird.io", "Admin Panel URL [http|https]://[host]:[port]")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&configPath, "config", defaultConfigPath, "Netbird config file location")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "sets Netbird log level")
|
||||||
|
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().StringVar(&setupKey, "setup-key", "", "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.AddCommand(serviceCmd)
|
||||||
|
rootCmd.AddCommand(upCmd)
|
||||||
|
rootCmd.AddCommand(downCmd)
|
||||||
|
rootCmd.AddCommand(statusCmd)
|
||||||
|
rootCmd.AddCommand(loginCmd)
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
rootCmd.AddCommand(sshCmd)
|
||||||
|
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
||||||
|
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupCloseHandler handles SIGTERM signal and exits with success
|
||||||
|
func SetupCloseHandler(ctx context.Context, cancel context.CancelFunc) {
|
||||||
|
termCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(termCh, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
done := ctx.Done()
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-termCh:
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("shutdown signal received")
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlagsFromEnvVars reads and updates flag values from environment variables with prefix WT_
|
||||||
|
func SetFlagsFromEnvVars() {
|
||||||
|
flags := rootCmd.PersistentFlags()
|
||||||
|
flags.VisitAll(func(f *pflag.Flag) {
|
||||||
|
oldEnvVar := FlagNameToEnvVar(f.Name, "WT_")
|
||||||
|
|
||||||
|
if value, present := os.LookupEnv(oldEnvVar); present {
|
||||||
|
err := flags.Set(f.Name, value)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, oldEnvVar, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newEnvVar := FlagNameToEnvVar(f.Name, "NB_")
|
||||||
|
|
||||||
|
if value, present := os.LookupEnv(newEnvVar); present {
|
||||||
|
err := flags.Set(f.Name, value)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, newEnvVar, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlagNameToEnvVar converts flag name to environment var name adding a prefix,
|
||||||
|
// replacing dashes and making all uppercase (e.g. setup-keys is converted to NB_SETUP_KEYS according to the input prefix)
|
||||||
|
func FlagNameToEnvVar(cmdFlag string, prefix string) string {
|
||||||
|
parsed := strings.ReplaceAll(cmdFlag, "-", "_")
|
||||||
|
upper := strings.ToUpper(parsed)
|
||||||
|
return prefix + upper
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialClientGRPCServer returns client connection to the dameno server.
|
||||||
|
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
strings.TrimPrefix(addr, "tcp://"),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithBlock(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBackOff execute function in backoff cycle.
|
||||||
|
func WithBackOff(bf func() error) error {
|
||||||
|
return backoff.RetryNotify(bf, CLIBackOffSettings, func(err error, duration time.Duration) {
|
||||||
|
log.Warnf("retrying Login to the Management service in %v due to error %v", duration, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLIBackOffSettings is default backoff settings for CLI commands.
|
||||||
|
var CLIBackOffSettings = &backoff.ExponentialBackOff{
|
||||||
|
InitialInterval: time.Second,
|
||||||
|
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||||
|
Multiplier: backoff.DefaultMultiplier,
|
||||||
|
MaxInterval: 10 * time.Second,
|
||||||
|
MaxElapsedTime: 30 * time.Second,
|
||||||
|
Stop: backoff.Stop,
|
||||||
|
Clock: backoff.SystemClock,
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRebrand(cmd *cobra.Command) error {
|
||||||
|
var err error
|
||||||
|
if logFile == defaultLogFile {
|
||||||
|
if migrateToNetbird(oldDefaultLogFile, defaultLogFile) {
|
||||||
|
cmd.Printf("will copy Log dir %s and its content to %s\n", oldDefaultLogFileDir, defaultLogFileDir)
|
||||||
|
err = cpDir(oldDefaultLogFileDir, defaultLogFileDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if configPath == defaultConfigPath {
|
||||||
|
if migrateToNetbird(oldDefaultConfigPath, defaultConfigPath) {
|
||||||
|
cmd.Printf("will copy Config dir %s and its content to %s\n", oldDefaultConfigPathDir, defaultConfigPathDir)
|
||||||
|
err = cpDir(oldDefaultConfigPathDir, defaultConfigPathDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpFile(src, dst string) error {
|
||||||
|
var err error
|
||||||
|
var srcfd *os.File
|
||||||
|
var dstfd *os.File
|
||||||
|
var srcinfo os.FileInfo
|
||||||
|
|
||||||
|
if srcfd, err = os.Open(src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcfd.Close()
|
||||||
|
|
||||||
|
if dstfd, err = os.Create(dst); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstfd.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(dstfd, srcfd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if srcinfo, err = os.Stat(src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Chmod(dst, srcinfo.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func copySymLink(source, dest string) error {
|
||||||
|
link, err := os.Readlink(source)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Symlink(link, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cpDir(src string, dst string) error {
|
||||||
|
var err error
|
||||||
|
var fds []os.FileInfo
|
||||||
|
var srcinfo os.FileInfo
|
||||||
|
|
||||||
|
if srcinfo, err = os.Stat(src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fds, err = ioutil.ReadDir(src); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, fd := range fds {
|
||||||
|
srcfp := path.Join(src, fd.Name())
|
||||||
|
dstfp := path.Join(dst, fd.Name())
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(srcfp)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fouldn't get fileInfo; %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch fileInfo.Mode() & os.ModeType {
|
||||||
|
case os.ModeSymlink:
|
||||||
|
if err = copySymLink(srcfp, dstfp); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
|
||||||
|
}
|
||||||
|
case os.ModeDir:
|
||||||
|
if err = cpDir(srcfp, dstfp); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err = cpFile(srcfp, dstfp); err != nil {
|
||||||
|
return fmt.Errorf("failed to copy from %s to %s; %v", srcfp, dstfp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateToNetbird(oldPath, newPath string) bool {
|
||||||
|
_, errOld := os.Stat(oldPath)
|
||||||
|
_, errNew := os.Stat(newPath)
|
||||||
|
|
||||||
|
if errors.Is(errOld, fs.ErrNotExist) || errNew == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
50
client/cmd/service.go
Normal file
50
client/cmd/service.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type program struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
serv *grpc.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
|
||||||
|
ctx = internal.CtxInitState(ctx)
|
||||||
|
return &program{ctx: ctx, cancel: cancel}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSVCConfig() *service.Config {
|
||||||
|
name := "netbird"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
name = "Netbird"
|
||||||
|
}
|
||||||
|
return &service.Config{
|
||||||
|
Name: name,
|
||||||
|
DisplayName: "Netbird",
|
||||||
|
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSVC(prg *program, conf *service.Config) (service.Service, error) {
|
||||||
|
s, err := service.New(prg, conf)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceCmd = &cobra.Command{
|
||||||
|
Use: "service",
|
||||||
|
Short: "manages Netbird service",
|
||||||
|
}
|
||||||
216
client/cmd/service_controller.go
Normal file
216
client/cmd/service_controller.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kardianos/service"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/client/server"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *program) Start(svc service.Service) error {
|
||||||
|
// Start should not block. Do the actual work async.
|
||||||
|
log.Info("starting Netbird service") //nolint
|
||||||
|
// in any case, even if configuration does not exists we run daemon to serve CLI gRPC API.
|
||||||
|
p.serv = grpc.NewServer()
|
||||||
|
|
||||||
|
split := strings.Split(daemonAddr, "://")
|
||||||
|
switch split[0] {
|
||||||
|
case "unix":
|
||||||
|
// cleanup failed close
|
||||||
|
stat, err := os.Stat(split[1])
|
||||||
|
if err == nil && !stat.IsDir() {
|
||||||
|
if err := os.Remove(split[1]); err != nil {
|
||||||
|
log.Debugf("remove socket file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "tcp":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported daemon address protocol: %v", split[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
listen, err := net.Listen(split[0], split[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to listen daemon interface: %w", err)
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer listen.Close()
|
||||||
|
|
||||||
|
if split[0] == "unix" {
|
||||||
|
err = os.Chmod(split[1], 0666)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed setting daemon permissions: %v", split[1])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serverInstance := server.New(p.ctx, managementURL, adminURL, configPath, logFile)
|
||||||
|
if err := serverInstance.Start(); err != nil {
|
||||||
|
log.Fatalf("failed to start daemon: %v", err)
|
||||||
|
}
|
||||||
|
proto.RegisterDaemonServiceServer(p.serv, serverInstance)
|
||||||
|
|
||||||
|
log.Printf("started daemon server: %v", split[1])
|
||||||
|
if err := p.serv.Serve(listen); err != nil {
|
||||||
|
log.Errorf("failed to serve daemon requests: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *program) Stop(srv service.Service) error {
|
||||||
|
p.cancel()
|
||||||
|
|
||||||
|
if p.serv != nil {
|
||||||
|
p.serv.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
log.Info("stopped Netbird service") //nolint
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var runCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "runs Netbird as service",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.InitLog(logLevel, logFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
SetupCloseHandler(ctx, cancel)
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.Run()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Printf("Netbird service is running")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var startCmd = &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "starts Netbird service",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.InitLog(logLevel, logFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.Start()
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird service has been started")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var stopCmd = &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "stops Netbird service",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.InitLog(logLevel, logFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird service has been stopped")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var restartCmd = &cobra.Command{
|
||||||
|
Use: "restart",
|
||||||
|
Short: "restarts Netbird service",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.InitLog(logLevel, logFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.Restart()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird service has been restarted")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
89
client/cmd/service_installer.go
Normal file
89
client/cmd/service_installer.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var installCmd = &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "installs Netbird service",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
svcConfig := newSVCConfig()
|
||||||
|
|
||||||
|
svcConfig.Arguments = []string{
|
||||||
|
"service",
|
||||||
|
"run",
|
||||||
|
"--config",
|
||||||
|
configPath,
|
||||||
|
"--log-level",
|
||||||
|
logLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
if managementURL != "" {
|
||||||
|
svcConfig.Arguments = append(svcConfig.Arguments, "--management-url")
|
||||||
|
svcConfig.Arguments = append(svcConfig.Arguments, managementURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
// Respected only by systemd systems
|
||||||
|
svcConfig.Dependencies = []string{"After=network.target syslog.target"}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Install()
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird service has been installed")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var uninstallCmd = &cobra.Command{
|
||||||
|
Use: "uninstall",
|
||||||
|
Short: "uninstalls Netbird service from system",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|
||||||
|
s, err := newSVC(newProgram(ctx, cancel), newSVCConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Uninstall()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird has been uninstalled")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
115
client/cmd/ssh.go
Normal file
115
client/cmd/ssh.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
port int
|
||||||
|
user = "root"
|
||||||
|
host string
|
||||||
|
)
|
||||||
|
|
||||||
|
var sshCmd = &cobra.Command{
|
||||||
|
Use: "ssh",
|
||||||
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return errors.New("requires a host argument")
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(args[0], "@")
|
||||||
|
if len(split) == 2 {
|
||||||
|
user = split[0]
|
||||||
|
host = split[1]
|
||||||
|
} else {
|
||||||
|
host = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Short: "connect to a remote SSH server",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := util.InitLog(logLevel, "console")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !util.IsAdmin() {
|
||||||
|
cmd.Printf("error: you must have Administrator privileges to run this command\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(cmd.Context())
|
||||||
|
|
||||||
|
config, err := internal.ReadConfig("", "", configPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
|
||||||
|
sshctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// blocking
|
||||||
|
if err := runSSH(sshctx, host, []byte(config.SSHKey), cmd); err != nil {
|
||||||
|
log.Print(err)
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-sig:
|
||||||
|
cancel()
|
||||||
|
case <-sshctx.Done():
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command) error {
|
||||||
|
c, err := nbssh.DialWithKey(fmt.Sprintf("%s:%d", addr, port), user, pemKey)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Printf("Error: %v\n", err)
|
||||||
|
cmd.Printf("Couldn't connect. " +
|
||||||
|
"You might be disconnected from the NetBird network, or the NetBird agent isn't running.\n" +
|
||||||
|
"Run the status command: \n\n" +
|
||||||
|
" netbird status\n\n" +
|
||||||
|
"It might also be that the SSH server is disabled on the agent you are trying to connect to.\n")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
err = c.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = c.OpenTerminal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
sshCmd.PersistentFlags().IntVarP(&port, "port", "p", nbssh.DefaultSSHPort, "Sets remote SSH port. Defaults to "+fmt.Sprint(nbssh.DefaultSSHPort))
|
||||||
|
}
|
||||||
314
client/cmd/status.go
Normal file
314
client/cmd/status.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
detailFlag bool
|
||||||
|
ipv4Flag bool
|
||||||
|
ipsFilter []string
|
||||||
|
statusFilter string
|
||||||
|
ipsFilterMap map[string]struct{}
|
||||||
|
)
|
||||||
|
|
||||||
|
var statusCmd = &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "status of the Netbird Service",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := parseFilters()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.InitLog(logLevel, "console")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(context.Background())
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
|
"If the daemon is not running please run: "+
|
||||||
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
daemonStatus := fmt.Sprintf("Daemon status: %s\n", resp.GetStatus())
|
||||||
|
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
|
||||||
|
|
||||||
|
cmd.Printf("%s\n"+
|
||||||
|
"Run UP command to log in with SSO (interactive login):\n\n"+
|
||||||
|
" netbird up \n\n"+
|
||||||
|
"If you are running a self-hosted version and no SSO provider has been configured in your Management Server,\n"+
|
||||||
|
"you can use a setup-key:\n\n netbird up --management-url <YOUR_MANAGEMENT_URL> --setup-key <YOUR_SETUP_KEY>\n\n"+
|
||||||
|
"More info: https://www.netbird.io/docs/overview/setup-keys\n\n",
|
||||||
|
daemonStatus,
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pbFullStatus := resp.GetFullStatus()
|
||||||
|
fullStatus := fromProtoFullStatus(pbFullStatus)
|
||||||
|
|
||||||
|
cmd.Print(parseFullStatus(fullStatus, detailFlag, daemonStatus, resp.GetDaemonVersion(), ipv4Flag))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ipsFilterMap = make(map[string]struct{})
|
||||||
|
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information")
|
||||||
|
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
||||||
|
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
||||||
|
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFilters() error {
|
||||||
|
switch strings.ToLower(statusFilter) {
|
||||||
|
case "", "disconnected", "connected":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ipsFilter) > 0 {
|
||||||
|
for _, addr := range ipsFilter {
|
||||||
|
_, err := netip.ParseAddr(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("got an invalid IP address in the filter: address %s, error %s", addr, err)
|
||||||
|
}
|
||||||
|
ipsFilterMap[addr] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromProtoFullStatus(pbFullStatus *proto.FullStatus) nbStatus.FullStatus {
|
||||||
|
var fullStatus nbStatus.FullStatus
|
||||||
|
managementState := pbFullStatus.GetManagementState()
|
||||||
|
fullStatus.ManagementState.URL = managementState.GetURL()
|
||||||
|
fullStatus.ManagementState.Connected = managementState.GetConnected()
|
||||||
|
|
||||||
|
signalState := pbFullStatus.GetSignalState()
|
||||||
|
fullStatus.SignalState.URL = signalState.GetURL()
|
||||||
|
fullStatus.SignalState.Connected = signalState.GetConnected()
|
||||||
|
|
||||||
|
localPeerState := pbFullStatus.GetLocalPeerState()
|
||||||
|
fullStatus.LocalPeerState.IP = localPeerState.GetIP()
|
||||||
|
fullStatus.LocalPeerState.PubKey = localPeerState.GetPubKey()
|
||||||
|
fullStatus.LocalPeerState.KernelInterface = localPeerState.GetKernelInterface()
|
||||||
|
|
||||||
|
var peersState []nbStatus.PeerState
|
||||||
|
|
||||||
|
for _, pbPeerState := range pbFullStatus.GetPeers() {
|
||||||
|
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
|
||||||
|
peerState := nbStatus.PeerState{
|
||||||
|
IP: pbPeerState.GetIP(),
|
||||||
|
PubKey: pbPeerState.GetPubKey(),
|
||||||
|
ConnStatus: pbPeerState.GetConnStatus(),
|
||||||
|
ConnStatusUpdate: timeLocal,
|
||||||
|
Relayed: pbPeerState.GetRelayed(),
|
||||||
|
Direct: pbPeerState.GetDirect(),
|
||||||
|
LocalIceCandidateType: pbPeerState.GetLocalIceCandidateType(),
|
||||||
|
RemoteIceCandidateType: pbPeerState.GetRemoteIceCandidateType(),
|
||||||
|
}
|
||||||
|
peersState = append(peersState, peerState)
|
||||||
|
}
|
||||||
|
|
||||||
|
fullStatus.Peers = peersState
|
||||||
|
|
||||||
|
return fullStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFullStatus(fullStatus nbStatus.FullStatus, printDetail bool, daemonStatus string, daemonVersion string, flag bool) string {
|
||||||
|
|
||||||
|
interfaceIP := fullStatus.LocalPeerState.IP
|
||||||
|
|
||||||
|
ip, _, err := net.ParseCIDR(interfaceIP)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipv4Flag {
|
||||||
|
return fmt.Sprintf("%s\n", ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
managementStatusURL = ""
|
||||||
|
signalStatusURL = ""
|
||||||
|
managementConnString = "Disconnected"
|
||||||
|
signalConnString = "Disconnected"
|
||||||
|
interfaceTypeString = "Userspace"
|
||||||
|
)
|
||||||
|
|
||||||
|
if printDetail {
|
||||||
|
managementStatusURL = fmt.Sprintf(" to %s", fullStatus.ManagementState.URL)
|
||||||
|
signalStatusURL = fmt.Sprintf(" to %s", fullStatus.SignalState.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullStatus.ManagementState.Connected {
|
||||||
|
managementConnString = "Connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullStatus.SignalState.Connected {
|
||||||
|
signalConnString = "Connected"
|
||||||
|
}
|
||||||
|
|
||||||
|
if fullStatus.LocalPeerState.KernelInterface {
|
||||||
|
interfaceTypeString = "Kernel"
|
||||||
|
} else if fullStatus.LocalPeerState.IP == "" {
|
||||||
|
interfaceTypeString = "N/A"
|
||||||
|
interfaceIP = "N/A"
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedPeersString, peersConnected := parsePeers(fullStatus.Peers, printDetail)
|
||||||
|
|
||||||
|
peersCountString := fmt.Sprintf("%d/%d Connected", peersConnected, len(fullStatus.Peers))
|
||||||
|
|
||||||
|
summary := fmt.Sprintf(
|
||||||
|
"Daemon version: %s\n"+
|
||||||
|
"CLI version: %s\n"+
|
||||||
|
"%s"+ // daemon status
|
||||||
|
"Management: %s%s\n"+
|
||||||
|
"Signal: %s%s\n"+
|
||||||
|
"NetBird IP: %s\n"+
|
||||||
|
"Interface type: %s\n"+
|
||||||
|
"Peers count: %s\n",
|
||||||
|
daemonVersion,
|
||||||
|
system.NetbirdVersion(),
|
||||||
|
daemonStatus,
|
||||||
|
managementConnString,
|
||||||
|
managementStatusURL,
|
||||||
|
signalConnString,
|
||||||
|
signalStatusURL,
|
||||||
|
interfaceIP,
|
||||||
|
interfaceTypeString,
|
||||||
|
peersCountString,
|
||||||
|
)
|
||||||
|
|
||||||
|
if printDetail {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Peers detail:"+
|
||||||
|
"%s\n"+
|
||||||
|
"%s",
|
||||||
|
parsedPeersString,
|
||||||
|
summary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePeers(peers []nbStatus.PeerState, printDetail bool) (string, int) {
|
||||||
|
var (
|
||||||
|
peersString = ""
|
||||||
|
peersConnected = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(peers) > 0 {
|
||||||
|
sort.SliceStable(peers, func(i, j int) bool {
|
||||||
|
iAddr, _ := netip.ParseAddr(peers[i].IP)
|
||||||
|
jAddr, _ := netip.ParseAddr(peers[j].IP)
|
||||||
|
return iAddr.Compare(jAddr) == -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedStatusString := peer.StatusConnected.String()
|
||||||
|
|
||||||
|
for _, peerState := range peers {
|
||||||
|
peerConnectionStatus := false
|
||||||
|
if peerState.ConnStatus == connectedStatusString {
|
||||||
|
peersConnected = peersConnected + 1
|
||||||
|
peerConnectionStatus = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if printDetail {
|
||||||
|
|
||||||
|
if skipDetailByFilters(peerState, peerConnectionStatus) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
localICE := "-"
|
||||||
|
remoteICE := "-"
|
||||||
|
connType := "-"
|
||||||
|
|
||||||
|
if peerConnectionStatus {
|
||||||
|
localICE = peerState.LocalIceCandidateType
|
||||||
|
remoteICE = peerState.RemoteIceCandidateType
|
||||||
|
connType = "P2P"
|
||||||
|
if peerState.Relayed {
|
||||||
|
connType = "Relayed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerString := fmt.Sprintf(
|
||||||
|
"\n Peer:\n"+
|
||||||
|
" NetBird IP: %s\n"+
|
||||||
|
" Public key: %s\n"+
|
||||||
|
" Status: %s\n"+
|
||||||
|
" -- detail --\n"+
|
||||||
|
" Connection type: %s\n"+
|
||||||
|
" Direct: %t\n"+
|
||||||
|
" ICE candidate (Local/Remote): %s/%s\n"+
|
||||||
|
" Last connection update: %s\n",
|
||||||
|
peerState.IP,
|
||||||
|
peerState.PubKey,
|
||||||
|
peerState.ConnStatus,
|
||||||
|
connType,
|
||||||
|
peerState.Direct,
|
||||||
|
localICE,
|
||||||
|
remoteICE,
|
||||||
|
peerState.ConnStatusUpdate.Format("2006-01-02 15:04:05"),
|
||||||
|
)
|
||||||
|
|
||||||
|
peersString = peersString + peerString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return peersString, peersConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
func skipDetailByFilters(peerState nbStatus.PeerState, isConnected bool) bool {
|
||||||
|
statusEval := false
|
||||||
|
ipEval := false
|
||||||
|
|
||||||
|
if statusFilter != "" {
|
||||||
|
lowerStatusFilter := strings.ToLower(statusFilter)
|
||||||
|
if lowerStatusFilter == "disconnected" && isConnected {
|
||||||
|
statusEval = true
|
||||||
|
} else if lowerStatusFilter == "connected" && !isConnected {
|
||||||
|
statusEval = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ipsFilter) > 0 {
|
||||||
|
_, ok := ipsFilterMap[peerState.IP]
|
||||||
|
if !ok {
|
||||||
|
ipEval = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return statusEval || ipEval
|
||||||
|
}
|
||||||
113
client/cmd/testutil.go
Normal file
113
client/cmd/testutil.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
|
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 {
|
||||||
|
config := &mgmt.Config{}
|
||||||
|
_, err := util.ReadJson("../testdata/management.json", config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
testDir := t.TempDir()
|
||||||
|
config.Datadir = testDir
|
||||||
|
err = util.CopyFileContents("../testdata/store.json", filepath.Join(testDir, "store.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, signalLis := startSignal(t)
|
||||||
|
signalAddr := signalLis.Addr().String()
|
||||||
|
config.Signal.URI = signalAddr
|
||||||
|
|
||||||
|
_, mgmLis := startManagement(t, config)
|
||||||
|
mgmAddr := mgmLis.Addr().String()
|
||||||
|
return mgmAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
|
||||||
|
lis, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := grpc.NewServer()
|
||||||
|
sigProto.RegisterSignalExchangeServer(s, sig.NewServer())
|
||||||
|
go func() {
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, lis
|
||||||
|
}
|
||||||
|
|
||||||
|
func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Listener) {
|
||||||
|
lis, err := net.Listen("tcp", ":0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := grpc.NewServer()
|
||||||
|
store, err := mgmt.NewStore(config.Datadir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
||||||
|
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||||
|
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
||||||
|
go func() {
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, lis
|
||||||
|
}
|
||||||
|
|
||||||
|
func startClientDaemon(
|
||||||
|
t *testing.T, ctx context.Context, managementURL, configPath string,
|
||||||
|
) (*grpc.Server, net.Listener) {
|
||||||
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := grpc.NewServer()
|
||||||
|
|
||||||
|
server := client.New(ctx, managementURL, adminURL, configPath, "")
|
||||||
|
if err := server.Start(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
clientProto.RegisterDaemonServiceServer(s, server)
|
||||||
|
go func() {
|
||||||
|
if err := s.Serve(lis); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
return s, lis
|
||||||
|
}
|
||||||
128
client/cmd/up.go
Normal file
128
client/cmd/up.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upCmd = &cobra.Command{
|
||||||
|
Use: "up",
|
||||||
|
Short: "install, login and start Netbird client",
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
SetFlagsFromEnvVars()
|
||||||
|
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
err := util.InitLog(logLevel, "console")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(cmd.Context())
|
||||||
|
|
||||||
|
// workaround to run without service
|
||||||
|
if logFile == "console" {
|
||||||
|
err = handleRebrand(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := internal.GetConfig(managementURL, adminURL, configPath, preSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get config file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
||||||
|
|
||||||
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("foreground login failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
|
SetupCloseHandler(ctx, cancel)
|
||||||
|
return internal.RunClient(ctx, config, nbStatus.NewRecorder())
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
|
"If the daemon is not running please run: "+
|
||||||
|
"\nnetbird service install \nnetbird service start\n", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed closing dameon gRPC client connection %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
|
status, err := client.Status(ctx, &proto.StatusRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to get daemon status: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status == string(internal.StatusConnected) {
|
||||||
|
cmd.Println("Already connected")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
loginRequest := proto.LoginRequest{
|
||||||
|
SetupKey: setupKey,
|
||||||
|
PreSharedKey: preSharedKey,
|
||||||
|
ManagementUrl: managementURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginErr error
|
||||||
|
|
||||||
|
var loginResp *proto.LoginResponse
|
||||||
|
|
||||||
|
err = WithBackOff(func() error {
|
||||||
|
var backOffErr error
|
||||||
|
loginResp, backOffErr = client.Login(ctx, &loginRequest)
|
||||||
|
if s, ok := gstatus.FromError(backOffErr); ok && (s.Code() == codes.InvalidArgument ||
|
||||||
|
s.Code() == codes.PermissionDenied ||
|
||||||
|
s.Code() == codes.NotFound ||
|
||||||
|
s.Code() == codes.Unimplemented) {
|
||||||
|
loginErr = backOffErr
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return backOffErr
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("login backoff cycle failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginErr != nil {
|
||||||
|
return fmt.Errorf("login failed: %v", loginErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.NeedsSSOLogin {
|
||||||
|
|
||||||
|
openURL(cmd, loginResp.VerificationURIComplete)
|
||||||
|
|
||||||
|
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("waiting sso login failed with: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.Up(ctx, &proto.UpRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("call service up method: %v", err)
|
||||||
|
}
|
||||||
|
cmd.Println("Connected")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
75
client/cmd/up_daemon_test.go
Normal file
75
client/cmd/up_daemon_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
var cliAddr string
|
||||||
|
|
||||||
|
func TestUpDaemon(t *testing.T) {
|
||||||
|
mgmAddr := startTestingServices(t)
|
||||||
|
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
confPath := tempDir + "/config.json"
|
||||||
|
|
||||||
|
ctx := internal.CtxInitState(context.Background())
|
||||||
|
state := internal.CtxGetState(ctx)
|
||||||
|
|
||||||
|
_, cliLis := startClientDaemon(t, ctx, "http://"+mgmAddr, confPath)
|
||||||
|
|
||||||
|
cliAddr = cliLis.Addr().String()
|
||||||
|
|
||||||
|
daemonAddr = "tcp://" + cliAddr
|
||||||
|
rootCmd.SetArgs([]string{
|
||||||
|
"login",
|
||||||
|
"--daemon-addr", "tcp://" + cliAddr,
|
||||||
|
"--setup-key", "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
|
||||||
|
"--log-file", "",
|
||||||
|
})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Errorf("expected no error while running up command, got %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
if status, err := state.Status(); err != nil && status != internal.StatusIdle {
|
||||||
|
t.Errorf("wrong status after login: %s, %v", internal.StatusIdle, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.SetArgs([]string{
|
||||||
|
"up",
|
||||||
|
"--daemon-addr", "tcp://" + cliAddr,
|
||||||
|
"--log-file", "",
|
||||||
|
})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Errorf("expected no error while running up command, got %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
if status, err := state.Status(); err != nil && status != internal.StatusConnected {
|
||||||
|
t.Errorf("wrong status after connect: %s, %v", status, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCmd.SetArgs([]string{
|
||||||
|
"status",
|
||||||
|
"--daemon-addr", "tcp://" + cliAddr,
|
||||||
|
})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Errorf("expected no error while running up command, got %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
|
||||||
|
rootCmd.SetErr(nil)
|
||||||
|
rootCmd.SetArgs([]string{"down", "--daemon-addr", "tcp://" + cliAddr})
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
t.Errorf("expected no error while running up command, got %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// we can't check status here, because context already canceled
|
||||||
|
}
|
||||||
17
client/cmd/version.go
Normal file
17
client/cmd/version.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "prints Netbird version",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
cmd.Println(system.NetbirdVersion())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
215
client/installer.nsis
Normal file
215
client/installer.nsis
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
!define APP_NAME "Netbird"
|
||||||
|
!define COMP_NAME "Netbird"
|
||||||
|
!define WEB_SITE "Netbird.io"
|
||||||
|
!define VERSION $%APPVER%
|
||||||
|
!define COPYRIGHT "Netbird Authors, 2022"
|
||||||
|
!define DESCRIPTION "A WireGuard®-based mesh network that connects your devices into a single private network"
|
||||||
|
!define INSTALLER_NAME "netbird-installer.exe"
|
||||||
|
!define MAIN_APP_EXE "Netbird"
|
||||||
|
!define ICON "ui\\netbird.ico"
|
||||||
|
!define BANNER "ui\\banner.bmp"
|
||||||
|
!define LICENSE_DATA "..\\LICENSE"
|
||||||
|
|
||||||
|
!define INSTALL_DIR "$PROGRAMFILES64\${APP_NAME}"
|
||||||
|
!define INSTALL_TYPE "SetShellVarContext all"
|
||||||
|
!define REG_ROOT "HKLM"
|
||||||
|
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}"
|
||||||
|
!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
|
||||||
|
|
||||||
|
!define UI_APP_NAME "Netbird UI"
|
||||||
|
!define UI_APP_EXE "Netbird-ui"
|
||||||
|
|
||||||
|
!define UI_REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${UI_APP_EXE}"
|
||||||
|
!define UI_UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UI_APP_NAME}"
|
||||||
|
|
||||||
|
Unicode True
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
VIProductVersion "${VERSION}"
|
||||||
|
VIAddVersionKey "ProductName" "${APP_NAME}"
|
||||||
|
VIAddVersionKey "CompanyName" "${COMP_NAME}"
|
||||||
|
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
|
||||||
|
VIAddVersionKey "FileDescription" "${DESCRIPTION}"
|
||||||
|
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
SetCompressor /SOLID Lzma
|
||||||
|
Name "${APP_NAME}"
|
||||||
|
Caption "${APP_NAME}"
|
||||||
|
OutFile "..\\${INSTALLER_NAME}"
|
||||||
|
BrandingText "${APP_NAME}"
|
||||||
|
InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" ""
|
||||||
|
InstallDir "${INSTALL_DIR}"
|
||||||
|
LicenseData "${LICENSE_DATA}"
|
||||||
|
ShowInstDetails Show
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
!define MUI_ICON "${ICON}"
|
||||||
|
!define MUI_UNICON "${ICON}"
|
||||||
|
!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}"
|
||||||
|
!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}"
|
||||||
|
!define MUI_FINISHPAGE_RUN
|
||||||
|
!define MUI_FINISHPAGE_RUN_TEXT "Start ${UI_APP_NAME}"
|
||||||
|
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
!include "MUI2.nsh"
|
||||||
|
!include LogicLib.nsh
|
||||||
|
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
!define MUI_UNABORTWARNING
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_LICENSE "${LICENSE_DATA}"
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
Function GetAppFromCommand
|
||||||
|
Exch $1
|
||||||
|
Push $2
|
||||||
|
StrCpy $2 $1 1 0
|
||||||
|
StrCmp $2 '"' 0 done
|
||||||
|
Push $3
|
||||||
|
StrCpy $3 ""
|
||||||
|
loop:
|
||||||
|
IntOp $3 $3 + 1
|
||||||
|
StrCpy $2 $1 1 $3
|
||||||
|
StrCmp $2 '' +2
|
||||||
|
StrCmp $2 '"' 0 loop
|
||||||
|
StrCpy $1 $1 $3
|
||||||
|
StrCpy $1 $1 "" 1 ; Remove starting quote
|
||||||
|
Pop $3
|
||||||
|
done:
|
||||||
|
Pop $2
|
||||||
|
Exch $1
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
!macro GetAppFromCommand in out
|
||||||
|
Push "${in}"
|
||||||
|
Call GetAppFromCommand
|
||||||
|
Pop ${out}
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
!macro UninstallPreviousNSIS UninstCommand CustomParameters
|
||||||
|
Push $0
|
||||||
|
Push $1
|
||||||
|
Push $2
|
||||||
|
Push '${CustomParameters}'
|
||||||
|
Push '${UninstCommand}'
|
||||||
|
Call GetAppFromCommand ; Remove quotes and parameters from UninstCommand
|
||||||
|
Pop $0
|
||||||
|
Pop $1
|
||||||
|
GetFullPathName $2 "$0\.."
|
||||||
|
ExecWait '"$0" /S $1 _?=$2'
|
||||||
|
Delete "$0" ; Extra cleanup because we used _?=
|
||||||
|
RMDir "$2"
|
||||||
|
Pop $2
|
||||||
|
Pop $1
|
||||||
|
Pop $0
|
||||||
|
!macroend
|
||||||
|
|
||||||
|
Function .onInit
|
||||||
|
StrCpy $INSTDIR "${INSTALL_DIR}"
|
||||||
|
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
|
||||||
|
${If} $R0 != ""
|
||||||
|
# if silent install jump to uninstall step
|
||||||
|
IfSilent uninstall
|
||||||
|
|
||||||
|
MessageBox MB_YESNO|MB_ICONQUESTION "NetBird is already installed. We must remove it before installing upgrading NetBird. Proceed?" IDNO done IDYES uninstall
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
!insertmacro UninstallPreviousNSIS $R0 "/NoMsgBox"
|
||||||
|
done:
|
||||||
|
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
######################################################################
|
||||||
|
Section -MainProgram
|
||||||
|
${INSTALL_TYPE}
|
||||||
|
# SetOverwrite ifnewer
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
File /r "..\\dist\\netbird_windows_amd64\\"
|
||||||
|
SectionEnd
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
Section -Icons_Reg
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
WriteUninstaller "$INSTDIR\netbird_uninstall.exe"
|
||||||
|
|
||||||
|
WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\netbird_uninstall.exe"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}"
|
||||||
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
|
||||||
|
|
||||||
|
WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
|
||||||
|
EnVar::SetHKLM
|
||||||
|
EnVar::AddValueEx "path" "$INSTDIR"
|
||||||
|
|
||||||
|
SetShellVarContext current
|
||||||
|
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
SetShellVarContext all
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section -Post
|
||||||
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service install'
|
||||||
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service start'
|
||||||
|
# sleep a bit for visibility
|
||||||
|
Sleep 1000
|
||||||
|
SectionEnd
|
||||||
|
######################################################################
|
||||||
|
|
||||||
|
Section Uninstall
|
||||||
|
${INSTALL_TYPE}
|
||||||
|
|
||||||
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service stop'
|
||||||
|
ExecWait '"$INSTDIR\${MAIN_APP_EXE}" service uninstall'
|
||||||
|
|
||||||
|
# kill ui client
|
||||||
|
ExecWait `taskkill /im ${UI_APP_EXE}.exe`
|
||||||
|
|
||||||
|
# wait the service uninstall take unblock the executable
|
||||||
|
Sleep 3000
|
||||||
|
Delete "$INSTDIR\${UI_APP_EXE}"
|
||||||
|
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
|
RmDir /r "$INSTDIR"
|
||||||
|
|
||||||
|
SetShellVarContext current
|
||||||
|
Delete "$DESKTOP\${APP_NAME}.lnk"
|
||||||
|
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
|
||||||
|
SetShellVarContext all
|
||||||
|
|
||||||
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
|
EnVar::SetHKLM
|
||||||
|
EnVar::DeleteValue "path" "$INSTDIR"
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
|
||||||
|
Function LaunchLink
|
||||||
|
SetShellVarContext current
|
||||||
|
SetOutPath $INSTDIR
|
||||||
|
ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk"
|
||||||
|
SetShellVarContext all
|
||||||
|
FunctionEnd
|
||||||
302
client/internal/config.go
Normal file
302
client/internal/config.go
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var managementURLDefault *url.URL
|
||||||
|
|
||||||
|
func ManagementURLDefault() *url.URL {
|
||||||
|
return managementURLDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
managementURL, err := ParseURL("Management URL", "https://api.wiretrustee.com:443")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
managementURLDefault = managementURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config Configuration type
|
||||||
|
type Config struct {
|
||||||
|
// Wireguard private key of local peer
|
||||||
|
PrivateKey string
|
||||||
|
PreSharedKey string
|
||||||
|
ManagementURL *url.URL
|
||||||
|
AdminURL *url.URL
|
||||||
|
WgIface string
|
||||||
|
WgPort int
|
||||||
|
IFaceBlackList []string
|
||||||
|
// SSHKey is a private SSH key in a PEM format
|
||||||
|
SSHKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
||||||
|
func createNewConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
|
||||||
|
wgKey := generateKey()
|
||||||
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config := &Config{
|
||||||
|
SSHKey: string(pem),
|
||||||
|
PrivateKey: wgKey,
|
||||||
|
WgIface: iface.WgInterfaceDefault,
|
||||||
|
WgPort: iface.DefaultWgPort,
|
||||||
|
IFaceBlackList: []string{},
|
||||||
|
}
|
||||||
|
if managementURL != "" {
|
||||||
|
URL, err := ParseURL("Management URL", managementURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.ManagementURL = URL
|
||||||
|
} else {
|
||||||
|
config.ManagementURL = managementURLDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
if preSharedKey != "" {
|
||||||
|
config.PreSharedKey = preSharedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminURL != "" {
|
||||||
|
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.AdminURL = newURL
|
||||||
|
}
|
||||||
|
|
||||||
|
config.IFaceBlackList = []string{iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
|
||||||
|
"Tailscale", "tailscale", "docker", "vet"}
|
||||||
|
|
||||||
|
err = util.WriteJson(configPath, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseURL parses and validates management URL
|
||||||
|
func ParseURL(serviceName, managementURL string) (*url.URL, error) {
|
||||||
|
parsedMgmtURL, err := url.ParseRequestURI(managementURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed parsing management URL %s: [%s]", managementURL, err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedMgmtURL.Scheme != "https" && parsedMgmtURL.Scheme != "http" {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"invalid %s URL provided %s. Supported format [http|https]://[host]:[port]",
|
||||||
|
serviceName, managementURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedMgmtURL, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadConfig reads existing config. In case provided managementURL is not empty overrides the read property
|
||||||
|
func ReadConfig(managementURL, adminURL, configPath string, preSharedKey *string) (*Config, error) {
|
||||||
|
config := &Config{}
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
return nil, status.Errorf(codes.NotFound, "config file doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := util.ReadJson(configPath, config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh := false
|
||||||
|
|
||||||
|
if managementURL != "" && config.ManagementURL.String() != managementURL {
|
||||||
|
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
||||||
|
managementURL, config.ManagementURL)
|
||||||
|
newURL, err := ParseURL("Management URL", managementURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.ManagementURL = newURL
|
||||||
|
refresh = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if adminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != adminURL) {
|
||||||
|
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
||||||
|
adminURL, config.AdminURL)
|
||||||
|
newURL, err := ParseURL("Admin Panel URL", adminURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.AdminURL = newURL
|
||||||
|
refresh = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if preSharedKey != nil && config.PreSharedKey != *preSharedKey {
|
||||||
|
log.Infof("new pre-shared key provided, updated to %s (old value %s)",
|
||||||
|
*preSharedKey, config.PreSharedKey)
|
||||||
|
config.PreSharedKey = *preSharedKey
|
||||||
|
refresh = true
|
||||||
|
}
|
||||||
|
if config.SSHKey == "" {
|
||||||
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
config.SSHKey = string(pem)
|
||||||
|
refresh = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.WgPort == 0 {
|
||||||
|
config.WgPort = iface.DefaultWgPort
|
||||||
|
refresh = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if refresh {
|
||||||
|
// since we have new management URL, we need to update config file
|
||||||
|
if err := util.WriteJson(configPath, config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig reads existing config or generates a new one
|
||||||
|
func GetConfig(managementURL, adminURL, configPath, preSharedKey string) (*Config, error) {
|
||||||
|
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||||
|
log.Infof("generating new config %s", configPath)
|
||||||
|
return createNewConfig(managementURL, adminURL, configPath, preSharedKey)
|
||||||
|
} else {
|
||||||
|
// don't overwrite pre-shared key if we receive asterisks from UI
|
||||||
|
pk := &preSharedKey
|
||||||
|
if preSharedKey == "**********" {
|
||||||
|
pk = nil
|
||||||
|
}
|
||||||
|
return ReadConfig(managementURL, adminURL, configPath, pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateKey generates a new Wireguard private key
|
||||||
|
func generateKey() string {
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return key.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAuthorizationFlow represents Device Authorization Flow information
|
||||||
|
type DeviceAuthorizationFlow struct {
|
||||||
|
Provider string
|
||||||
|
ProviderConfig ProviderConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProviderConfig has all attributes needed to initiate a device authorization flow
|
||||||
|
type ProviderConfig struct {
|
||||||
|
// ClientID An IDP application client id
|
||||||
|
ClientID string
|
||||||
|
// ClientSecret An IDP application client secret
|
||||||
|
ClientSecret string
|
||||||
|
// Domain An IDP API domain
|
||||||
|
// Deprecated. Use OIDCConfigEndpoint instead
|
||||||
|
Domain string
|
||||||
|
// Audience An Audience for to authorization validation
|
||||||
|
Audience string
|
||||||
|
// TokenEndpoint is the endpoint of an IDP manager where clients can obtain access token
|
||||||
|
TokenEndpoint string
|
||||||
|
// DeviceAuthEndpoint is the endpoint of an IDP manager where clients can obtain device authorization code
|
||||||
|
DeviceAuthEndpoint string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDeviceAuthorizationFlowInfo(ctx context.Context, config *Config) (DeviceAuthorizationFlow, error) {
|
||||||
|
// validate our peer's Wireguard PRIVATE key
|
||||||
|
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var mgmTlsEnabled bool
|
||||||
|
if config.ManagementURL.Scheme == "https" {
|
||||||
|
mgmTlsEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("connecting to Management Service %s", config.ManagementURL.String())
|
||||||
|
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed connecting to Management Service %s %v", config.ManagementURL.String(), err)
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
}
|
||||||
|
log.Debugf("connected to the Management service %s", config.ManagementURL.String())
|
||||||
|
defer func() {
|
||||||
|
err = mgmClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to close the Management service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
serverKey, err := mgmClient.GetServerPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed while getting Management Service public key: %v", err)
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
protoDeviceAuthorizationFlow, err := mgmClient.GetDeviceAuthorizationFlow(*serverKey)
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
|
||||||
|
log.Warnf("server couldn't find device flow, contact admin: %v", err)
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
} else {
|
||||||
|
log.Errorf("failed to retrieve device flow: %v", err)
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceAuthorizationFlow := DeviceAuthorizationFlow{
|
||||||
|
Provider: protoDeviceAuthorizationFlow.Provider.String(),
|
||||||
|
|
||||||
|
ProviderConfig: ProviderConfig{
|
||||||
|
Audience: protoDeviceAuthorizationFlow.GetProviderConfig().GetAudience(),
|
||||||
|
ClientID: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientID(),
|
||||||
|
ClientSecret: protoDeviceAuthorizationFlow.GetProviderConfig().GetClientSecret(),
|
||||||
|
Domain: protoDeviceAuthorizationFlow.GetProviderConfig().Domain,
|
||||||
|
TokenEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetTokenEndpoint(),
|
||||||
|
DeviceAuthEndpoint: protoDeviceAuthorizationFlow.GetProviderConfig().GetDeviceAuthEndpoint(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = isProviderConfigValid(deviceAuthorizationFlow.ProviderConfig)
|
||||||
|
if err != nil {
|
||||||
|
return DeviceAuthorizationFlow{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceAuthorizationFlow, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isProviderConfigValid(config ProviderConfig) error {
|
||||||
|
errorMSGFormat := "invalid provider configuration received from management: %s value is empty. Contact your NetBird administrator"
|
||||||
|
if config.Audience == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Audience")
|
||||||
|
}
|
||||||
|
if config.ClientID == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Client ID")
|
||||||
|
}
|
||||||
|
if config.TokenEndpoint == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Token Endpoint")
|
||||||
|
}
|
||||||
|
if config.DeviceAuthEndpoint == "" {
|
||||||
|
return fmt.Errorf(errorMSGFormat, "Device Auth Endpoint")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
60
client/internal/config_test.go
Normal file
60
client/internal/config_test.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReadConfig(t *testing.T) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetConfig(t *testing.T) {
|
||||||
|
managementURL := "https://test.management.url:33071"
|
||||||
|
adminURL := "https://app.admin.url"
|
||||||
|
path := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
preSharedKey := "preSharedKey"
|
||||||
|
|
||||||
|
// case 1: new config has to be generated
|
||||||
|
config, err := GetConfig(managementURL, adminURL, path, preSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, config.ManagementURL.String(), managementURL)
|
||||||
|
assert.Equal(t, config.PreSharedKey, preSharedKey)
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Errorf("config file was expected to be created under path %s", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// case 2: existing config -> fetch it
|
||||||
|
config, err = GetConfig(managementURL, adminURL, path, preSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, config.ManagementURL.String(), managementURL)
|
||||||
|
assert.Equal(t, config.PreSharedKey, preSharedKey)
|
||||||
|
|
||||||
|
// case 3: existing config, but new managementURL has been provided -> update config
|
||||||
|
newManagementURL := "https://test.newManagement.url:33071"
|
||||||
|
config, err = GetConfig(newManagementURL, adminURL, path, preSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, config.ManagementURL.String(), newManagementURL)
|
||||||
|
assert.Equal(t, config.PreSharedKey, preSharedKey)
|
||||||
|
|
||||||
|
// read once more to make sure that config file has been updated with the new management URL
|
||||||
|
readConf, err := util.ReadJson(path, config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, readConf.(*Config).ManagementURL.String(), newManagementURL)
|
||||||
|
}
|
||||||
313
client/internal/connect.go
Normal file
313
client/internal/connect.go
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunClient with main logic.
|
||||||
|
func RunClient(ctx context.Context, config *Config, statusRecorder *nbStatus.Status) error {
|
||||||
|
backOff := &backoff.ExponentialBackOff{
|
||||||
|
InitialInterval: time.Second,
|
||||||
|
RandomizationFactor: 1,
|
||||||
|
Multiplier: 1.7,
|
||||||
|
MaxInterval: 15 * time.Second,
|
||||||
|
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
|
||||||
|
Stop: backoff.Stop,
|
||||||
|
Clock: backoff.SystemClock,
|
||||||
|
}
|
||||||
|
|
||||||
|
state := CtxGetState(ctx)
|
||||||
|
defer func() {
|
||||||
|
s, err := state.Status()
|
||||||
|
if err != nil || s != StatusNeedsLogin {
|
||||||
|
state.Set(StatusIdle)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wrapErr := state.Wrap
|
||||||
|
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var mgmTlsEnabled bool
|
||||||
|
if config.ManagementURL.Scheme == "https" {
|
||||||
|
mgmTlsEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
publicSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
managementURL := config.ManagementURL.String()
|
||||||
|
statusRecorder.MarkManagementDisconnected(managementURL)
|
||||||
|
|
||||||
|
operation := func() error {
|
||||||
|
// if context cancelled we not start new backoff cycle
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Set(StatusConnecting)
|
||||||
|
|
||||||
|
engineCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer func() {
|
||||||
|
statusRecorder.MarkManagementDisconnected(managementURL)
|
||||||
|
statusRecorder.CleanLocalPeerState()
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Debugf("conecting to the Management service %s", config.ManagementURL.Host)
|
||||||
|
mgmClient, err := mgm.NewClient(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||||
|
if err != nil {
|
||||||
|
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
|
||||||
|
}
|
||||||
|
log.Debugf("connected to the Management service %s", config.ManagementURL.Host)
|
||||||
|
defer func() {
|
||||||
|
err = mgmClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to close the Management service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
|
||||||
|
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug(err)
|
||||||
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.PermissionDenied) {
|
||||||
|
state.Set(StatusNeedsLogin)
|
||||||
|
return backoff.Permanent(wrapErr(err)) // unrecoverable error
|
||||||
|
}
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
statusRecorder.MarkManagementConnected(managementURL)
|
||||||
|
|
||||||
|
localPeerState := nbStatus.LocalPeerState{
|
||||||
|
IP: loginResp.GetPeerConfig().GetAddress(),
|
||||||
|
PubKey: myPrivateKey.PublicKey().String(),
|
||||||
|
KernelInterface: iface.WireguardModuleIsLoaded(),
|
||||||
|
}
|
||||||
|
|
||||||
|
statusRecorder.UpdateLocalPeerState(localPeerState)
|
||||||
|
|
||||||
|
signalURL := fmt.Sprintf("%s://%s",
|
||||||
|
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
|
||||||
|
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
|
||||||
|
)
|
||||||
|
|
||||||
|
statusRecorder.MarkSignalDisconnected(signalURL)
|
||||||
|
defer statusRecorder.MarkSignalDisconnected(signalURL)
|
||||||
|
|
||||||
|
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
|
||||||
|
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = signalClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed closing Signal service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
statusRecorder.MarkSignalConnected(signalURL)
|
||||||
|
|
||||||
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
|
|
||||||
|
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, statusRecorder)
|
||||||
|
err = engine.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
|
||||||
|
state.Set(StatusConnected)
|
||||||
|
|
||||||
|
<-engineCtx.Done()
|
||||||
|
|
||||||
|
backOff.Reset()
|
||||||
|
|
||||||
|
err = engine.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed stopping engine %v", err)
|
||||||
|
return wrapErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("stopped NetBird client")
|
||||||
|
|
||||||
|
if _, err := state.Status(); err == ErrResetConnection {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = backoff.Retry(operation, backOff)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createEngineConfig converts configuration received from Management Service to EngineConfig
|
||||||
|
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
|
||||||
|
|
||||||
|
engineConf := &EngineConfig{
|
||||||
|
WgIfaceName: config.WgIface,
|
||||||
|
WgAddr: peerConfig.Address,
|
||||||
|
IFaceBlackList: config.IFaceBlackList,
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: config.WgPort,
|
||||||
|
SSHKey: []byte(config.SSHKey),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.PreSharedKey != "" {
|
||||||
|
preSharedKey, err := wgtypes.ParseKey(config.PreSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
engineConf.PreSharedKey = &preSharedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
return engineConf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// connectToSignal creates Signal Service client and established a connection
|
||||||
|
func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) {
|
||||||
|
var sigTLSEnabled bool
|
||||||
|
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
|
||||||
|
sigTLSEnabled = true
|
||||||
|
} else {
|
||||||
|
sigTLSEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
signalClient, err := signal.NewClient(ctx, wtConfig.Signal.Uri, ourPrivateKey, sigTLSEnabled)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while connecting to the Signal Exchange Service %s: %s", wtConfig.Signal.Uri, err)
|
||||||
|
return nil, gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Signal Service : %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return signalClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
|
||||||
|
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
|
||||||
|
|
||||||
|
serverPublicKey, err := client.GetServerPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, gstatus.Errorf(codes.FailedPrecondition, "failed while getting Management Service public key: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sysInfo := system.GetInfo(ctx)
|
||||||
|
loginResp, err := client.Login(*serverPublicKey, sysInfo, pubSSHKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
|
||||||
|
// It is used for backward compatibility now.
|
||||||
|
// NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import
|
||||||
|
const ManagementLegacyPort = 33073
|
||||||
|
|
||||||
|
// UpdateOldManagementPort checks whether client can switch to the new Management port 443.
|
||||||
|
// If it can switch, then it updates the config and returns a new one. Otherwise, it returns the provided config.
|
||||||
|
// The check is performed only for the NetBird's managed version.
|
||||||
|
func UpdateOldManagementPort(ctx context.Context, config *Config, configPath string) (*Config, error) {
|
||||||
|
|
||||||
|
if config.ManagementURL.Hostname() != ManagementURLDefault().Hostname() {
|
||||||
|
// only do the check for the NetBird's managed version
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var mgmTlsEnabled bool
|
||||||
|
if config.ManagementURL.Scheme == "https" {
|
||||||
|
mgmTlsEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mgmTlsEnabled {
|
||||||
|
// only do the check for HTTPs scheme (the hosted version of the Management service is always HTTPs)
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if mgmTlsEnabled && config.ManagementURL.Port() == fmt.Sprintf("%d", ManagementLegacyPort) {
|
||||||
|
|
||||||
|
newURL, err := ParseURL("Management URL", fmt.Sprintf("%s://%s:%d",
|
||||||
|
config.ManagementURL.Scheme, config.ManagementURL.Hostname(), 443))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// here we check whether we could switch from the legacy 33073 port to the new 443
|
||||||
|
log.Infof("attempting to switch from the legacy Management URL %s to the new one %s",
|
||||||
|
config.ManagementURL.String(), newURL.String())
|
||||||
|
key, err := wgtypes.ParseKey(config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := mgm.NewClient(ctx, newURL.Host, key, mgmTlsEnabled)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return config, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = client.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to close the Management service client %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// gRPC check
|
||||||
|
_, err = client.GetServerPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything is alright => update the config
|
||||||
|
newConfig, err := ReadConfig(newURL.String(), "", configPath, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("couldn't switch to the new Management %s", newURL.String())
|
||||||
|
return config, fmt.Errorf("failed updating config file: %v", err)
|
||||||
|
}
|
||||||
|
log.Infof("successfully switched to the new Management URL: %s", newURL.String())
|
||||||
|
|
||||||
|
return newConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
859
client/internal/engine.go
Normal file
859
client/internal/engine.go
Normal file
@@ -0,0 +1,859 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
|
sProto "github.com/netbirdio/netbird/signal/proto"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/pion/ice/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PeerConnectionTimeoutMax is a timeout of an initial connection attempt to a remote peer.
|
||||||
|
// E.g. this peer will wait PeerConnectionTimeoutMax for the remote peer to respond,
|
||||||
|
// if not successful then it will retry the connection attempt.
|
||||||
|
// Todo pass timeout at EnginConfig
|
||||||
|
const (
|
||||||
|
PeerConnectionTimeoutMax = 45000 // ms
|
||||||
|
PeerConnectionTimeoutMin = 30000 // ms
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrResetConnection = fmt.Errorf("reset connection")
|
||||||
|
|
||||||
|
// EngineConfig is a config for the Engine
|
||||||
|
type EngineConfig struct {
|
||||||
|
WgPort int
|
||||||
|
WgIfaceName string
|
||||||
|
|
||||||
|
// WgAddr is a Wireguard local address (Netbird Network IP)
|
||||||
|
WgAddr string
|
||||||
|
|
||||||
|
// WgPrivateKey is a Wireguard private key of our peer (it MUST never leave the machine)
|
||||||
|
WgPrivateKey wgtypes.Key
|
||||||
|
|
||||||
|
// IFaceBlackList is a list of network interfaces to ignore when discovering connection candidates (ICE related)
|
||||||
|
IFaceBlackList []string
|
||||||
|
|
||||||
|
PreSharedKey *wgtypes.Key
|
||||||
|
|
||||||
|
// UDPMuxPort default value 0 - the system will pick an available port
|
||||||
|
UDPMuxPort int
|
||||||
|
|
||||||
|
// UDPMuxSrflxPort default value 0 - the system will pick an available port
|
||||||
|
UDPMuxSrflxPort int
|
||||||
|
|
||||||
|
// SSHKey is a private SSH key in a PEM format
|
||||||
|
SSHKey []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
|
||||||
|
type Engine struct {
|
||||||
|
// signal is a Signal Service client
|
||||||
|
signal signal.Client
|
||||||
|
// mgmClient is a Management Service client
|
||||||
|
mgmClient mgm.Client
|
||||||
|
// peerConns is a map that holds all the peers that are known to this peer
|
||||||
|
peerConns map[string]*peer.Conn
|
||||||
|
|
||||||
|
// syncMsgMux is used to guarantee sequential Management Service message processing
|
||||||
|
syncMsgMux *sync.Mutex
|
||||||
|
|
||||||
|
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
|
||||||
|
TURNs []*ice.URL
|
||||||
|
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
wgInterface *iface.WGIface
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
sshServerFunc func(hostKeyPEM []byte, addr string) (nbssh.Server, error)
|
||||||
|
sshServer nbssh.Server
|
||||||
|
|
||||||
|
statusRecorder *nbstatus.Status
|
||||||
|
|
||||||
|
routeManager routemanager.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peer is an instance of the Connection Peer
|
||||||
|
type Peer struct {
|
||||||
|
WgPubKey string
|
||||||
|
WgAllowedIps string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEngine creates a new Connection Engine
|
||||||
|
func NewEngine(
|
||||||
|
ctx context.Context, cancel context.CancelFunc,
|
||||||
|
signalClient signal.Client, mgmClient mgm.Client,
|
||||||
|
config *EngineConfig, statusRecorder *nbstatus.Status,
|
||||||
|
) *Engine {
|
||||||
|
return &Engine{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
signal: signalClient,
|
||||||
|
mgmClient: mgmClient,
|
||||||
|
peerConns: map[string]*peer.Conn{},
|
||||||
|
syncMsgMux: &sync.Mutex{},
|
||||||
|
config: config,
|
||||||
|
STUNs: []*ice.URL{},
|
||||||
|
TURNs: []*ice.URL{},
|
||||||
|
networkSerial: 0,
|
||||||
|
sshServerFunc: nbssh.DefaultSSHServer,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) Stop() error {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
err := e.removeAllPeers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// very ugly but we want to remove peers from the WireGuard interface first before removing interface.
|
||||||
|
// Removing peers happens in the conn.CLose() asynchronously
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
log.Debugf("removing Netbird interface %s", e.config.WgIfaceName)
|
||||||
|
if e.wgInterface.Interface != nil {
|
||||||
|
err = e.wgInterface.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed closing Netbird interface %s %v", e.config.WgIfaceName, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.udpMux != nil {
|
||||||
|
if err := e.udpMux.Close(); err != nil {
|
||||||
|
log.Debugf("close udp mux: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.udpMuxSrflx != nil {
|
||||||
|
if err := e.udpMuxSrflx.Close(); err != nil {
|
||||||
|
log.Debugf("close server reflexive udp mux: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.udpMuxConn != nil {
|
||||||
|
if err := e.udpMuxConn.Close(); err != nil {
|
||||||
|
log.Debugf("close udp mux connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
log.Warnf("failed stopping the SSH server: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.routeManager != nil {
|
||||||
|
e.routeManager.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("stopped Netbird Engine")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start creates a new Wireguard tunnel interface and listens to events from Signal and Management services
|
||||||
|
// Connections to remote peers are not established here.
|
||||||
|
// However, they will be established once an event with a list of peers to connect to will be received from Management Service
|
||||||
|
func (e *Engine) Start() error {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
wgIfaceName := e.config.WgIfaceName
|
||||||
|
wgAddr := e.config.WgAddr
|
||||||
|
myPrivateKey := e.config.WgPrivateKey
|
||||||
|
var err error
|
||||||
|
|
||||||
|
e.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, iface.DefaultMTU)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed creating wireguard interface instance %s: [%s]", wgIfaceName, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.udpMuxConn, err = net.ListenUDP("udp4", &net.UDPAddr{Port: e.config.UDPMuxPort})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxPort, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.udpMuxConnSrflx, err = net.ListenUDP("udp4", &net.UDPAddr{Port: e.config.UDPMuxSrflxPort})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed listening on UDP port %d: [%s]", e.config.UDPMuxSrflxPort, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.udpMux = ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: e.udpMuxConn})
|
||||||
|
e.udpMuxSrflx = ice.NewUniversalUDPMuxDefault(ice.UniversalUDPMuxParams{UDPConn: e.udpMuxConnSrflx})
|
||||||
|
|
||||||
|
err = e.wgInterface.Create()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed creating tunnel interface %s: [%s]", wgIfaceName, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.wgInterface.Configure(myPrivateKey.String(), e.config.WgPort)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed configuring Wireguard interface [%s]: %s", wgIfaceName, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder)
|
||||||
|
|
||||||
|
e.receiveSignalEvents()
|
||||||
|
e.receiveManagementEvents()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// modifyPeers updates peers that have been modified (e.g. IP address has been changed).
|
||||||
|
// It closes the existing connection, removes it from the peerConns map, and creates a new one.
|
||||||
|
func (e *Engine) modifyPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||||
|
|
||||||
|
// first, check if peers have been modified
|
||||||
|
var modified []*mgmProto.RemotePeerConfig
|
||||||
|
for _, p := range peersUpdate {
|
||||||
|
if peerConn, ok := e.peerConns[p.GetWgPubKey()]; ok {
|
||||||
|
if peerConn.GetConf().ProxyConfig.AllowedIps != strings.Join(p.AllowedIps, ",") {
|
||||||
|
modified = append(modified, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// second, close all modified connections and remove them from the state map
|
||||||
|
for _, p := range modified {
|
||||||
|
err := e.removePeer(p.GetWgPubKey())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// third, add the peer connections again
|
||||||
|
for _, p := range modified {
|
||||||
|
err := e.addNewPeer(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removePeers finds and removes peers that do not exist anymore in the network map received from the Management Service.
|
||||||
|
// It also removes peers that have been modified (e.g. change of IP address). They will be added again in addPeers method.
|
||||||
|
func (e *Engine) removePeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||||
|
currentPeers := make([]string, 0, len(e.peerConns))
|
||||||
|
for p := range e.peerConns {
|
||||||
|
currentPeers = append(currentPeers, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPeers := make([]string, 0, len(peersUpdate))
|
||||||
|
for _, p := range peersUpdate {
|
||||||
|
newPeers = append(newPeers, p.GetWgPubKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
toRemove := util.SliceDiff(currentPeers, newPeers)
|
||||||
|
|
||||||
|
for _, p := range toRemove {
|
||||||
|
err := e.removePeer(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("removed peer %s", p)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) removeAllPeers() error {
|
||||||
|
log.Debugf("removing all peer connections")
|
||||||
|
for p := range e.peerConns {
|
||||||
|
err := e.removePeer(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removePeer closes an existing peer connection, removes a peer, and clears authorized key of the SSH server
|
||||||
|
func (e *Engine) removePeer(peerKey string) error {
|
||||||
|
log.Debugf("removing peer from engine %s", peerKey)
|
||||||
|
|
||||||
|
if !isNil(e.sshServer) {
|
||||||
|
e.sshServer.RemoveAuthorizedKey(peerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := e.statusRecorder.RemovePeer(peerKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("received error when removing peer %s from status recorder: %v", peerKey, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, exists := e.peerConns[peerKey]
|
||||||
|
if exists {
|
||||||
|
delete(e.peerConns, peerKey)
|
||||||
|
err := conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case *peer.ConnectionAlreadyClosedError:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeerConnectionStatus returns a connection Status or nil if peer connection wasn't found
|
||||||
|
func (e *Engine) GetPeerConnectionStatus(peerKey string) peer.ConnStatus {
|
||||||
|
conn, exists := e.peerConns[peerKey]
|
||||||
|
if exists && conn != nil {
|
||||||
|
return conn.Status()
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) GetPeers() []string {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
peers := []string{}
|
||||||
|
for s := range e.peerConns {
|
||||||
|
peers = append(peers, s)
|
||||||
|
}
|
||||||
|
return peers
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConnectedPeers returns a connection Status or nil if peer connection wasn't found
|
||||||
|
func (e *Engine) GetConnectedPeers() []string {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
peers := []string{}
|
||||||
|
for s, conn := range e.peerConns {
|
||||||
|
if conn.Status() == peer.StatusConnected {
|
||||||
|
peers = append(peers, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return peers
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalCandidate(candidate ice.Candidate, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client) error {
|
||||||
|
err := s.Send(&sProto.Message{
|
||||||
|
Key: myKey.PublicKey().String(),
|
||||||
|
RemoteKey: remoteKey.String(),
|
||||||
|
Body: &sProto.Body{
|
||||||
|
Type: sProto.Body_CANDIDATE,
|
||||||
|
Payload: candidate.Marshal(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalOfferAnswer signals either an offer or an answer to remote peer
|
||||||
|
func SignalOfferAnswer(offerAnswer peer.OfferAnswer, myKey wgtypes.Key, remoteKey wgtypes.Key, s signal.Client, isAnswer bool) error {
|
||||||
|
var t sProto.Body_Type
|
||||||
|
if isAnswer {
|
||||||
|
t = sProto.Body_ANSWER
|
||||||
|
} else {
|
||||||
|
t = sProto.Body_OFFER
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := signal.MarshalCredential(myKey, offerAnswer.WgListenPort, remoteKey, &signal.Credential{
|
||||||
|
UFrag: offerAnswer.IceCredentials.UFrag,
|
||||||
|
Pwd: offerAnswer.IceCredentials.Pwd,
|
||||||
|
}, t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.Send(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
if update.GetWiretrusteeConfig() != nil {
|
||||||
|
err := e.updateTURNs(update.GetWiretrusteeConfig().GetTurns())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.updateSTUNs(update.GetWiretrusteeConfig().GetStuns())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo update signal
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.GetNetworkMap() != nil {
|
||||||
|
// only apply new changes and ignore old ones
|
||||||
|
err := e.updateNetworkMap(update.GetNetworkMap())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNil(server nbssh.Server) bool {
|
||||||
|
return server == nil || reflect.ValueOf(server).IsNil()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
|
||||||
|
if sshConf.GetSshEnabled() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
log.Warnf("running SSH server on Windows is not supported")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// start SSH server if it wasn't running
|
||||||
|
if isNil(e.sshServer) {
|
||||||
|
//nil sshServer means it has not yet been started
|
||||||
|
var err error
|
||||||
|
e.sshServer, err = e.sshServerFunc(e.config.SSHKey,
|
||||||
|
fmt.Sprintf("%s:%d", e.wgInterface.Address.IP.String(), nbssh.DefaultSSHPort))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// blocking
|
||||||
|
err = e.sshServer.Start()
|
||||||
|
if err != nil {
|
||||||
|
// will throw error when we stop it even if it is a graceful stop
|
||||||
|
log.Debugf("stopped SSH server with error %v", err)
|
||||||
|
}
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
defer e.syncMsgMux.Unlock()
|
||||||
|
e.sshServer = nil
|
||||||
|
log.Infof("stopped SSH server")
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
log.Debugf("SSH server is already running")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Disable SSH server request, so stop it if it was running
|
||||||
|
if !isNil(e.sshServer) {
|
||||||
|
err := e.sshServer.Stop()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to stop SSH server %v", err)
|
||||||
|
}
|
||||||
|
e.sshServer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
|
||||||
|
if e.wgInterface.Address.String() != conf.Address {
|
||||||
|
oldAddr := e.wgInterface.Address.String()
|
||||||
|
log.Debugf("updating peer address from %s to %s", oldAddr, conf.Address)
|
||||||
|
err := e.wgInterface.UpdateAddr(conf.Address)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.config.WgAddr = conf.Address
|
||||||
|
log.Infof("updated peer address from %s to %s", oldAddr, conf.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.GetSshConfig() != nil {
|
||||||
|
err := e.updateSSH(conf.GetSshConfig())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed handling SSH server setup %v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiveManagementEvents connects to the Management Service event stream to receive updates from the management service
|
||||||
|
// E.g. when a new peer has been registered and we are allowed to connect to it.
|
||||||
|
func (e *Engine) receiveManagementEvents() {
|
||||||
|
go func() {
|
||||||
|
err := e.mgmClient.Sync(func(update *mgmProto.SyncResponse) error {
|
||||||
|
return e.handleSync(update)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// happens if management is unavailable for a long time.
|
||||||
|
// We want to cancel the operation of the whole client
|
||||||
|
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
||||||
|
e.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Debugf("stopped receiving updates from Management Service")
|
||||||
|
}()
|
||||||
|
log.Debugf("connecting to Management Service updates stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateSTUNs(stuns []*mgmProto.HostConfig) error {
|
||||||
|
if len(stuns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var newSTUNs []*ice.URL
|
||||||
|
log.Debugf("got STUNs update from Management Service, updating")
|
||||||
|
for _, stun := range stuns {
|
||||||
|
url, err := ice.ParseURL(stun.Uri)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newSTUNs = append(newSTUNs, url)
|
||||||
|
}
|
||||||
|
e.STUNs = newSTUNs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateTURNs(turns []*mgmProto.ProtectedHostConfig) error {
|
||||||
|
if len(turns) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var newTURNs []*ice.URL
|
||||||
|
log.Debugf("got TURNs update from Management Service, updating")
|
||||||
|
for _, turn := range turns {
|
||||||
|
url, err := ice.ParseURL(turn.HostConfig.Uri)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
url.Username = turn.User
|
||||||
|
url.Password = turn.Password
|
||||||
|
newTURNs = append(newTURNs, url)
|
||||||
|
}
|
||||||
|
e.TURNs = newTURNs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
||||||
|
|
||||||
|
// intentionally leave it before checking serial because for now it can happen that peer IP changed but serial didn't
|
||||||
|
if networkMap.GetPeerConfig() != nil {
|
||||||
|
err := e.updateConfig(networkMap.GetPeerConfig())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serial := networkMap.GetSerial()
|
||||||
|
if e.networkSerial > serial {
|
||||||
|
log.Debugf("received outdated NetworkMap with serial %d, ignoring", serial)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("got peers update from Management Service, total peers to connect to = %d", len(networkMap.GetRemotePeers()))
|
||||||
|
|
||||||
|
// cleanup request, most likely our peer has been deleted
|
||||||
|
if networkMap.GetRemotePeersIsEmpty() {
|
||||||
|
err := e.removeAllPeers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := e.removePeers(networkMap.GetRemotePeers())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.modifyPeers(networkMap.GetRemotePeers())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.addNewPeers(networkMap.GetRemotePeers())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// update SSHServer by adding remote peer SSH keys
|
||||||
|
if !isNil(e.sshServer) {
|
||||||
|
for _, config := range networkMap.GetRemotePeers() {
|
||||||
|
if config.GetSshConfig() != nil && config.GetSshConfig().GetSshPubKey() != nil {
|
||||||
|
err := e.sshServer.AddAuthorizedKey(config.WgPubKey, string(config.GetSshConfig().GetSshPubKey()))
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed adding authroized key to SSH DefaultServer %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protoRoutes := networkMap.GetRoutes()
|
||||||
|
if protoRoutes == nil {
|
||||||
|
protoRoutes = []*mgmProto.Route{}
|
||||||
|
}
|
||||||
|
err := e.routeManager.UpdateRoutes(serial, toRoutes(protoRoutes))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to update routes, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.networkSerial = serial
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
|
||||||
|
routes := make([]*route.Route, 0)
|
||||||
|
for _, protoRoute := range protoRoutes {
|
||||||
|
_, prefix, _ := route.ParseNetwork(protoRoute.Network)
|
||||||
|
convertedRoute := &route.Route{
|
||||||
|
ID: protoRoute.ID,
|
||||||
|
Network: prefix,
|
||||||
|
NetID: protoRoute.NetID,
|
||||||
|
NetworkType: route.NetworkType(protoRoute.NetworkType),
|
||||||
|
Peer: protoRoute.Peer,
|
||||||
|
Metric: int(protoRoute.Metric),
|
||||||
|
Masquerade: protoRoute.Masquerade,
|
||||||
|
}
|
||||||
|
routes = append(routes, convertedRoute)
|
||||||
|
}
|
||||||
|
return routes
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewPeers adds peers that were not know before but arrived from the Management service with the update
|
||||||
|
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||||
|
for _, p := range peersUpdate {
|
||||||
|
err := e.addNewPeer(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNewPeer add peer if connection doesn't exist
|
||||||
|
func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
||||||
|
peerKey := peerConfig.GetWgPubKey()
|
||||||
|
peerIPs := peerConfig.GetAllowedIps()
|
||||||
|
if _, ok := e.peerConns[peerKey]; !ok {
|
||||||
|
conn, err := e.createPeerConn(peerKey, strings.Join(peerIPs, ","))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.peerConns[peerKey] = conn
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine) connWorker(conn *peer.Conn, peerKey string) {
|
||||||
|
for {
|
||||||
|
|
||||||
|
// randomize starting time a bit
|
||||||
|
min := 500
|
||||||
|
max := 2000
|
||||||
|
time.Sleep(time.Duration(rand.Intn(max-min)+min) * time.Millisecond)
|
||||||
|
|
||||||
|
// if peer has been removed -> give up
|
||||||
|
if !e.peerExists(peerKey) {
|
||||||
|
log.Debugf("peer %s doesn't exist anymore, won't retry connection", peerKey)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !e.signal.Ready() {
|
||||||
|
log.Infof("signal client isn't ready, skipping connection attempt %s", peerKey)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// we might have received new STUN and TURN servers meanwhile, so update them
|
||||||
|
e.syncMsgMux.Lock()
|
||||||
|
conf := conn.GetConf()
|
||||||
|
conf.StunTurn = append(e.STUNs, e.TURNs...)
|
||||||
|
conn.UpdateConf(conf)
|
||||||
|
e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
err := conn.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("connection to peer %s failed: %v", peerKey, err)
|
||||||
|
switch err.(type) {
|
||||||
|
case *peer.ConnectionClosedError:
|
||||||
|
// conn has been forced to close, so we exit the loop
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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{
|
||||||
|
Key: pubKey,
|
||||||
|
LocalKey: e.config.WgPrivateKey.PublicKey().String(),
|
||||||
|
StunTurn: stunTurn,
|
||||||
|
InterfaceBlackList: e.config.IFaceBlackList,
|
||||||
|
Timeout: timeout,
|
||||||
|
UDPMux: e.udpMux,
|
||||||
|
UDPMuxSrflx: e.udpMuxSrflx,
|
||||||
|
ProxyConfig: proxyConfig,
|
||||||
|
LocalWgPort: e.config.WgPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConn, err := peer.NewConn(config, e.statusRecorder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
wgPubKey, err := wgtypes.ParseKey(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signalOffer := func(offerAnswer peer.OfferAnswer) error {
|
||||||
|
return SignalOfferAnswer(offerAnswer, e.config.WgPrivateKey, wgPubKey, e.signal, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
signalCandidate := func(candidate ice.Candidate) error {
|
||||||
|
return signalCandidate(candidate, e.config.WgPrivateKey, wgPubKey, e.signal)
|
||||||
|
}
|
||||||
|
|
||||||
|
signalAnswer := func(offerAnswer peer.OfferAnswer) error {
|
||||||
|
return SignalOfferAnswer(offerAnswer, e.config.WgPrivateKey, wgPubKey, e.signal, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConn.SetSignalCandidate(signalCandidate)
|
||||||
|
peerConn.SetSignalOffer(signalOffer)
|
||||||
|
peerConn.SetSignalAnswer(signalAnswer)
|
||||||
|
|
||||||
|
return peerConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiveSignalEvents connects to the Signal Service event stream to negotiate connection with remote peers
|
||||||
|
func (e *Engine) receiveSignalEvents() {
|
||||||
|
go func() {
|
||||||
|
// connect to a stream of messages coming from the signal server
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.GetBody().Type {
|
||||||
|
case sProto.Body_OFFER:
|
||||||
|
remoteCred, err := signal.UnMarshalCredential(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.OnRemoteOffer(peer.OfferAnswer{
|
||||||
|
IceCredentials: peer.IceCredentials{
|
||||||
|
UFrag: remoteCred.UFrag,
|
||||||
|
Pwd: remoteCred.Pwd,
|
||||||
|
},
|
||||||
|
WgListenPort: int(msg.GetBody().GetWgListenPort()),
|
||||||
|
Version: msg.GetBody().GetNetBirdVersion(),
|
||||||
|
})
|
||||||
|
case sProto.Body_ANSWER:
|
||||||
|
remoteCred, err := signal.UnMarshalCredential(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.OnRemoteAnswer(peer.OfferAnswer{
|
||||||
|
IceCredentials: peer.IceCredentials{
|
||||||
|
UFrag: remoteCred.UFrag,
|
||||||
|
Pwd: remoteCred.Pwd,
|
||||||
|
},
|
||||||
|
WgListenPort: int(msg.GetBody().GetWgListenPort()),
|
||||||
|
Version: msg.GetBody().GetNetBirdVersion(),
|
||||||
|
})
|
||||||
|
case sProto.Body_CANDIDATE:
|
||||||
|
candidate, err := ice.UnmarshalCandidate(msg.GetBody().Payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed on parsing remote candidate %s -> %s", candidate, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.OnRemoteCandidate(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// happens if signal is unavailable for a long time.
|
||||||
|
// We want to cancel the operation of the whole client
|
||||||
|
_ = CtxGetState(e.ctx).Wrap(ErrResetConnection)
|
||||||
|
e.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
e.signal.WaitStreamConnected()
|
||||||
|
}
|
||||||
781
client/internal/engine_test.go
Normal file
781
client/internal/engine_test.go
Normal file
@@ -0,0 +1,781 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
mgmt "github.com/netbirdio/netbird/management/client"
|
||||||
|
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||||
|
"github.com/netbirdio/netbird/management/server"
|
||||||
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
|
"github.com/netbirdio/netbird/signal/proto"
|
||||||
|
signalServer "github.com/netbirdio/netbird/signal/server"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kaep = keepalive.EnforcementPolicy{
|
||||||
|
MinTime: 15 * time.Second,
|
||||||
|
PermitWithoutStream: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
kasp = keepalive.ServerParameters{
|
||||||
|
MaxConnectionIdle: 15 * time.Second,
|
||||||
|
MaxConnectionAgeGrace: 5 * time.Second,
|
||||||
|
Time: 5 * time.Second,
|
||||||
|
Timeout: 2 * time.Second,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEngine_SSH(t *testing.T) {
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("skipping TestEngine_SSH on Windows")
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||||
|
WgIfaceName: "utun101",
|
||||||
|
WgAddr: "100.64.0.1/24",
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
}, nbstatus.NewRecorder())
|
||||||
|
|
||||||
|
var sshKeysAdded []string
|
||||||
|
var sshPeersRemoved []string
|
||||||
|
|
||||||
|
sshCtx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
engine.sshServerFunc = func(hostKeyPEM []byte, addr string) (ssh.Server, error) {
|
||||||
|
return &ssh.MockServer{
|
||||||
|
Ctx: sshCtx,
|
||||||
|
StopFunc: func() error {
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
StartFunc: func() error {
|
||||||
|
<-ctx.Done()
|
||||||
|
return ctx.Err()
|
||||||
|
},
|
||||||
|
AddAuthorizedKeyFunc: func(peer, newKey string) error {
|
||||||
|
sshKeysAdded = append(sshKeysAdded, newKey)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
RemoveAuthorizedKeyFunc: func(peer string) {
|
||||||
|
sshPeersRemoved = append(sshPeersRemoved, peer)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
err = engine.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := engine.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
peerWithSSH := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.21/24"},
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{
|
||||||
|
SshPubKey: []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFATYCqaQw/9id1Qkq3n16JYhDhXraI6Pc1fgB8ynEfQ"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSH server is not enabled so SSH config of a remote peer should be ignored
|
||||||
|
networkMap := &mgmtProto.NetworkMap{
|
||||||
|
Serial: 6,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, engine.sshServer)
|
||||||
|
|
||||||
|
// SSH server is enabled, therefore SSH config should be applied
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 7,
|
||||||
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{SshEnabled: true}},
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(250 * time.Millisecond)
|
||||||
|
assert.NotNil(t, engine.sshServer)
|
||||||
|
assert.Contains(t, sshKeysAdded, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFATYCqaQw/9id1Qkq3n16JYhDhXraI6Pc1fgB8ynEfQ")
|
||||||
|
|
||||||
|
// now remove peer
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 8,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//time.Sleep(250 * time.Millisecond)
|
||||||
|
assert.NotNil(t, engine.sshServer)
|
||||||
|
assert.Contains(t, sshPeersRemoved, "MNHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=")
|
||||||
|
|
||||||
|
// now disable SSH server
|
||||||
|
networkMap = &mgmtProto.NetworkMap{
|
||||||
|
Serial: 9,
|
||||||
|
PeerConfig: &mgmtProto.PeerConfig{Address: "100.64.0.1/24",
|
||||||
|
SshConfig: &mgmtProto.SSHConfig{SshEnabled: false}},
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peerWithSSH},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, engine.sshServer)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine_UpdateNetworkMap(t *testing.T) {
|
||||||
|
// test setup
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||||
|
WgIfaceName: "utun102",
|
||||||
|
WgAddr: "100.64.0.1/24",
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
}, nbstatus.NewRecorder())
|
||||||
|
engine.wgInterface, err = iface.NewWGIFace("utun102", "100.64.0.1/24", iface.DefaultMTU)
|
||||||
|
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), engine.wgInterface, engine.statusRecorder)
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
networkMap *mgmtProto.NetworkMap
|
||||||
|
|
||||||
|
expectedLen int
|
||||||
|
expectedPeers []*mgmtProto.RemotePeerConfig
|
||||||
|
expectedSerial uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
peer1 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.10/24"},
|
||||||
|
}
|
||||||
|
|
||||||
|
peer2 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.11/24"},
|
||||||
|
}
|
||||||
|
|
||||||
|
peer3 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.12/24"},
|
||||||
|
}
|
||||||
|
|
||||||
|
modifiedPeer3 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.20/24"},
|
||||||
|
}
|
||||||
|
|
||||||
|
case1 := testCase{
|
||||||
|
name: "input with a new peer to add",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
||||||
|
peer1,
|
||||||
|
},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
expectedLen: 1,
|
||||||
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1},
|
||||||
|
expectedSerial: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2nd case - one extra peer added and network map has CurrentSerial grater than local => apply the update
|
||||||
|
case2 := testCase{
|
||||||
|
name: "input with an old peer and a new peer to add",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 2,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
||||||
|
peer1, peer2,
|
||||||
|
},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
||||||
|
expectedSerial: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
case3 := testCase{
|
||||||
|
name: "input with outdated (old) update to ignore",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 0,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
||||||
|
peer1, peer2, peer3,
|
||||||
|
},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer1, peer2},
|
||||||
|
expectedSerial: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
case4 := testCase{
|
||||||
|
name: "input with one peer to remove and one new to add",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 4,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
||||||
|
peer2, peer3,
|
||||||
|
},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, peer3},
|
||||||
|
expectedSerial: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
case5 := testCase{
|
||||||
|
name: "input with one peer to modify",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 4,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{
|
||||||
|
modifiedPeer3, peer2,
|
||||||
|
},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedPeers: []*mgmtProto.RemotePeerConfig{peer2, modifiedPeer3},
|
||||||
|
expectedSerial: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
case6 := testCase{
|
||||||
|
name: "input with all peers to remove",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 5,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{},
|
||||||
|
RemotePeersIsEmpty: true,
|
||||||
|
},
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedPeers: nil,
|
||||||
|
expectedSerial: 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range []testCase{case1, case2, case3, case4, case5, case6} {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
err = engine.updateNetworkMap(c.networkMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(engine.peerConns) != c.expectedLen {
|
||||||
|
t.Errorf("expecting Engine.peerConns to be of size %d, got %d", c.expectedLen, len(engine.peerConns))
|
||||||
|
}
|
||||||
|
|
||||||
|
if engine.networkSerial != c.expectedSerial {
|
||||||
|
t.Errorf("expecting Engine.networkSerial to be equal to %d, actual %d", c.expectedSerial, engine.networkSerial)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range c.expectedPeers {
|
||||||
|
conn, ok := engine.peerConns[p.GetWgPubKey()]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("expecting Engine.peerConns to contain peer %s", p)
|
||||||
|
}
|
||||||
|
expectedAllowedIPs := strings.Join(p.AllowedIps, ",")
|
||||||
|
if conn.GetConf().ProxyConfig.AllowedIps != expectedAllowedIPs {
|
||||||
|
t.Errorf("expecting peer %s to have AllowedIPs= %s, got %s", p.GetWgPubKey(),
|
||||||
|
expectedAllowedIPs, conn.GetConf().ProxyConfig.AllowedIps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine_Sync(t *testing.T) {
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// feed updates to Engine via mocked Management client
|
||||||
|
updates := make(chan *mgmtProto.SyncResponse)
|
||||||
|
defer close(updates)
|
||||||
|
syncFunc := func(msgHandler func(msg *mgmtProto.SyncResponse) error) error {
|
||||||
|
for msg := range updates {
|
||||||
|
err := msgHandler(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{SyncFunc: syncFunc}, &EngineConfig{
|
||||||
|
WgIfaceName: "utun103",
|
||||||
|
WgAddr: "100.64.0.1/24",
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
}, nbstatus.NewRecorder())
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := engine.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = engine.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
peer1 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.10/24"},
|
||||||
|
}
|
||||||
|
peer2 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "LLHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.11/24"},
|
||||||
|
}
|
||||||
|
peer3 := &mgmtProto.RemotePeerConfig{
|
||||||
|
WgPubKey: "GGHf3Ma6z6mdLbriAJbqhX9+nM/B71lgw2+91q3LlhU=",
|
||||||
|
AllowedIps: []string{"100.64.0.12/24"},
|
||||||
|
}
|
||||||
|
// 1st update with just 1 peer and serial larger than the current serial of the engine => apply update
|
||||||
|
updates <- &mgmtProto.SyncResponse{
|
||||||
|
NetworkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 10,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeers: []*mgmtProto.RemotePeerConfig{peer1, peer2, peer3},
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.After(time.Second * 2)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeout:
|
||||||
|
t.Fatalf("timeout while waiting for test to finish")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(engine.GetPeers()) == 3 && engine.networkSerial == 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine_UpdateNetworkMapWithRoutes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputErr error
|
||||||
|
networkMap *mgmtProto.NetworkMap
|
||||||
|
expectedLen int
|
||||||
|
expectedRoutes []*route.Route
|
||||||
|
expectedSerial uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Routes Update Should Be Passed To Manager",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: []*mgmtProto.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
Network: "192.168.0.0/24",
|
||||||
|
NetID: "n1",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
Network: "192.168.1.0/24",
|
||||||
|
NetID: "n2",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedLen: 2,
|
||||||
|
expectedRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
Network: netip.MustParsePrefix("192.168.0.0/24"),
|
||||||
|
NetID: "n1",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
Network: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
NetID: "n2",
|
||||||
|
Peer: "p1",
|
||||||
|
NetworkType: 1,
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty Routes Update Should Be Passed",
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: nil,
|
||||||
|
},
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedRoutes: []*route.Route{},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error Shouldn't Break Engine",
|
||||||
|
inputErr: fmt.Errorf("mocking error"),
|
||||||
|
networkMap: &mgmtProto.NetworkMap{
|
||||||
|
Serial: 1,
|
||||||
|
PeerConfig: nil,
|
||||||
|
RemotePeersIsEmpty: false,
|
||||||
|
Routes: nil,
|
||||||
|
},
|
||||||
|
expectedLen: 0,
|
||||||
|
expectedRoutes: []*route.Route{},
|
||||||
|
expectedSerial: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
// test setup
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
wgIfaceName := fmt.Sprintf("utun%d", 104+n)
|
||||||
|
wgAddr := fmt.Sprintf("100.66.%d.1/24", n)
|
||||||
|
|
||||||
|
engine := NewEngine(ctx, cancel, &signal.MockClient{}, &mgmt.MockClient{}, &EngineConfig{
|
||||||
|
WgIfaceName: wgIfaceName,
|
||||||
|
WgAddr: wgAddr,
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: 33100,
|
||||||
|
}, nbstatus.NewRecorder())
|
||||||
|
engine.wgInterface, err = iface.NewWGIFace(wgIfaceName, wgAddr, iface.DefaultMTU)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
input := struct {
|
||||||
|
inputSerial uint64
|
||||||
|
inputRoutes []*route.Route
|
||||||
|
}{}
|
||||||
|
|
||||||
|
mockRouteManager := &routemanager.MockManager{
|
||||||
|
UpdateRoutesFunc: func(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
|
input.inputSerial = updateSerial
|
||||||
|
input.inputRoutes = newRoutes
|
||||||
|
return testCase.inputErr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.routeManager = mockRouteManager
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
exitErr := engine.Stop()
|
||||||
|
if exitErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = engine.updateNetworkMap(testCase.networkMap)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
assert.Equal(t, testCase.expectedSerial, input.inputSerial, "serial should match")
|
||||||
|
assert.Len(t, input.inputRoutes, testCase.expectedLen, "routes len should match")
|
||||||
|
assert.Equal(t, testCase.expectedRoutes, input.inputRoutes, "routes should match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEngine_MultiplePeers(t *testing.T) {
|
||||||
|
// log.SetLevel(log.DebugLevel)
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
err := util.CopyFileContents("../testdata/store.json", filepath.Join(dir, "store.json"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err = os.Remove(filepath.Join(dir, "store.json")) //nolint
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(CtxInitState(context.Background()))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
sport := 10010
|
||||||
|
sigServer, err := startSignal(sport)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer sigServer.Stop()
|
||||||
|
mport := 33081
|
||||||
|
mgmtServer, err := startManagement(mport, dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer mgmtServer.GracefulStop()
|
||||||
|
|
||||||
|
setupKey := "A2C8E62B-38F5-4553-B31E-DD66C696CEBB"
|
||||||
|
|
||||||
|
mu := sync.Mutex{}
|
||||||
|
engines := []*Engine{}
|
||||||
|
numPeers := 10
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(numPeers)
|
||||||
|
// create and start peers
|
||||||
|
for i := 0; i < numPeers; i++ {
|
||||||
|
j := i
|
||||||
|
go func() {
|
||||||
|
engine, err := createEngine(ctx, cancel, setupKey, j, mport, sport)
|
||||||
|
if err != nil {
|
||||||
|
wg.Done()
|
||||||
|
t.Errorf("unable to create the engine for peer %d with error %v", j, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
err = engine.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to start engine for peer %d with error %v", j, err)
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
engines = append(engines, engine)
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until all have been created and started
|
||||||
|
wg.Wait()
|
||||||
|
if len(engines) != numPeers {
|
||||||
|
t.Fatal("not all peers was started")
|
||||||
|
}
|
||||||
|
// check whether all the peer have expected peers connected
|
||||||
|
|
||||||
|
expectedConnected := numPeers * (numPeers - 1)
|
||||||
|
|
||||||
|
// adjust according to timeouts
|
||||||
|
timeout := 50 * time.Second
|
||||||
|
timeoutChan := time.After(timeout)
|
||||||
|
ticker := time.NewTicker(time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timeoutChan:
|
||||||
|
t.Fatalf("waiting for expected connections timeout after %s", timeout.String())
|
||||||
|
break loop
|
||||||
|
case <-ticker.C:
|
||||||
|
totalConnected := 0
|
||||||
|
for _, engine := range engines {
|
||||||
|
totalConnected = totalConnected + len(engine.GetConnectedPeers())
|
||||||
|
}
|
||||||
|
if totalConnected == expectedConnected {
|
||||||
|
log.Infof("total connected=%d", totalConnected)
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
log.Infof("total connected=%d", totalConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cleanup test
|
||||||
|
for n, peerEngine := range engines {
|
||||||
|
t.Logf("stopping peer with interface %s from multipeer test, loopIndex %d", peerEngine.wgInterface.Name, n)
|
||||||
|
errStop := peerEngine.mgmClient.Close()
|
||||||
|
if errStop != nil {
|
||||||
|
log.Infoln("got error trying to close management clients from engine: ", errStop)
|
||||||
|
}
|
||||||
|
errStop = peerEngine.Stop()
|
||||||
|
if errStop != nil {
|
||||||
|
log.Infoln("got error trying to close testing peers engine: ", errStop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEngine(ctx context.Context, cancel context.CancelFunc, setupKey string, i int, mport int, sport int) (*Engine, error) {
|
||||||
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mgmtClient, err := mgmt.NewClient(ctx, fmt.Sprintf("localhost:%d", mport), key, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signalClient, err := signal.NewClient(ctx, fmt.Sprintf("localhost:%d", sport), key, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
publicKey, err := mgmtClient.GetServerPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := system.GetInfo(ctx)
|
||||||
|
resp, err := mgmtClient.Register(*publicKey, setupKey, "", info, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ifaceName string
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
ifaceName = fmt.Sprintf("utun1%d", i)
|
||||||
|
} else {
|
||||||
|
ifaceName = fmt.Sprintf("wt%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wgPort := 33100 + i
|
||||||
|
conf := &EngineConfig{
|
||||||
|
WgIfaceName: ifaceName,
|
||||||
|
WgAddr: resp.PeerConfig.Address,
|
||||||
|
WgPrivateKey: key,
|
||||||
|
WgPort: wgPort,
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewEngine(ctx, cancel, signalClient, mgmtClient, conf, nbstatus.NewRecorder()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startSignal(port int) (*grpc.Server, error) {
|
||||||
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proto.RegisterSignalExchangeServer(s, signalServer.NewServer())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err = s.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startManagement(port int, dataDir string) (*grpc.Server, error) {
|
||||||
|
config := &server.Config{
|
||||||
|
Stuns: []*server.Host{},
|
||||||
|
TURNConfig: &server.TURNConfig{},
|
||||||
|
Signal: &server.Host{
|
||||||
|
Proto: "http",
|
||||||
|
URI: "localhost:10000",
|
||||||
|
},
|
||||||
|
Datadir: dataDir,
|
||||||
|
HttpConfig: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s := grpc.NewServer(grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
|
store, err := server.NewStore(config.Datadir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
||||||
|
}
|
||||||
|
peersUpdateManager := server.NewPeersUpdateManager()
|
||||||
|
accountManager, err := server.BuildManager(store, peersUpdateManager, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||||
|
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
||||||
|
go func() {
|
||||||
|
if err = s.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
107
client/internal/login.go
Normal file
107
client/internal/login.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Login(ctx context.Context, config *Config, setupKey string, jwtToken string) error {
|
||||||
|
// validate our peer's Wireguard PRIVATE key
|
||||||
|
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var mgmTlsEnabled bool
|
||||||
|
if config.ManagementURL.Scheme == "https" {
|
||||||
|
mgmTlsEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("connecting to the Management service %s", config.ManagementURL.String())
|
||||||
|
mgmClient, err := mgm.NewClient(ctx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed connecting to the Management service %s %v", config.ManagementURL.String(), err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("connected to the Management service %s", config.ManagementURL.String())
|
||||||
|
defer func() {
|
||||||
|
err = mgmClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
cStatus, ok := status.FromError(err)
|
||||||
|
if !ok || ok && cStatus.Code() != codes.Canceled {
|
||||||
|
log.Warnf("failed to close the Management service client, err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
serverKey, err := mgmClient.GetServerPublicKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed while getting Management Service public key: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = loginPeer(ctx, *serverKey, mgmClient, setupKey, jwtToken, pubSSHKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed logging-in peer on Management Service : %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infof("peer has successfully logged-in to the Management service %s", config.ManagementURL.String())
|
||||||
|
|
||||||
|
err = mgmClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to close the Management service client: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginPeer attempts to login to Management Service. If peer wasn't registered, tries the registration flow.
|
||||||
|
func loginPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
|
||||||
|
sysInfo := system.GetInfo(ctx)
|
||||||
|
loginResp, err := client.Login(serverPublicKey, sysInfo, pubSSHKey)
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := status.FromError(err); ok && s.Code() == codes.PermissionDenied {
|
||||||
|
log.Debugf("peer registration required")
|
||||||
|
return registerPeer(ctx, serverPublicKey, client, setupKey, jwtToken, pubSSHKey)
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerPeer checks whether setupKey was provided via cmd line and if not then it prompts user to enter a key.
|
||||||
|
// Otherwise tries to register with the provided setupKey via command line.
|
||||||
|
func registerPeer(ctx context.Context, serverPublicKey wgtypes.Key, client *mgm.GrpcClient, setupKey string, jwtToken string, pubSSHKey []byte) (*mgmProto.LoginResponse, error) {
|
||||||
|
validSetupKey, err := uuid.Parse(setupKey)
|
||||||
|
if err != nil && jwtToken == "" {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "invalid setup-key or no sso information provided, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("sending peer registration request to Management Service")
|
||||||
|
info := system.GetInfo(ctx)
|
||||||
|
loginResp, err := client.Register(serverPublicKey, validSetupKey.String(), jwtToken, info, pubSSHKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed registering peer %v,%s", err, validSetupKey.String())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("peer has been successfully registered on Management Service")
|
||||||
|
|
||||||
|
return loginResp, nil
|
||||||
|
}
|
||||||
278
client/internal/oauth.go
Normal file
278
client/internal/oauth.go
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OAuthClient is a OAuth client interface for various idp providers
|
||||||
|
type OAuthClient interface {
|
||||||
|
RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error)
|
||||||
|
WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error)
|
||||||
|
GetClientID(ctx context.Context) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPClient http client interface for API calls
|
||||||
|
type HTTPClient interface {
|
||||||
|
Do(req *http.Request) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceAuthInfo holds information for the OAuth device login flow
|
||||||
|
type DeviceAuthInfo struct {
|
||||||
|
DeviceCode string `json:"device_code"`
|
||||||
|
UserCode string `json:"user_code"`
|
||||||
|
VerificationURI string `json:"verification_uri"`
|
||||||
|
VerificationURIComplete string `json:"verification_uri_complete"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Interval int `json:"interval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenInfo holds information of issued access token
|
||||||
|
type TokenInfo struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostedGrantType grant type for device flow on Hosted
|
||||||
|
const (
|
||||||
|
HostedGrantType = "urn:ietf:params:oauth:grant-type:device_code"
|
||||||
|
HostedRefreshGrant = "refresh_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hosted client
|
||||||
|
type Hosted struct {
|
||||||
|
// Hosted API Audience for validation
|
||||||
|
Audience string
|
||||||
|
// Hosted Native application client id
|
||||||
|
ClientID string
|
||||||
|
// TokenEndpoint to request access token
|
||||||
|
TokenEndpoint string
|
||||||
|
// DeviceAuthEndpoint to request device authorization code
|
||||||
|
DeviceAuthEndpoint string
|
||||||
|
|
||||||
|
HTTPClient HTTPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestDeviceCodePayload used for request device code payload for auth0
|
||||||
|
type RequestDeviceCodePayload struct {
|
||||||
|
Audience string `json:"audience"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenRequestPayload used for requesting the auth0 token
|
||||||
|
type TokenRequestPayload struct {
|
||||||
|
GrantType string `json:"grant_type"`
|
||||||
|
DeviceCode string `json:"device_code,omitempty"`
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TokenRequestResponse used for parsing Hosted token's response
|
||||||
|
type TokenRequestResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
ErrorDescription string `json:"error_description"`
|
||||||
|
TokenInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Claims used when validating the access token
|
||||||
|
type Claims struct {
|
||||||
|
Audience interface{} `json:"aud"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHostedDeviceFlow returns an Hosted OAuth client
|
||||||
|
func NewHostedDeviceFlow(audience string, clientID string, tokenEndpoint string, deviceAuthEndpoint string) *Hosted {
|
||||||
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
|
httpTransport.MaxIdleConns = 5
|
||||||
|
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
Transport: httpTransport,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Hosted{
|
||||||
|
Audience: audience,
|
||||||
|
ClientID: clientID,
|
||||||
|
TokenEndpoint: tokenEndpoint,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
DeviceAuthEndpoint: deviceAuthEndpoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientID returns the provider client id
|
||||||
|
func (h *Hosted) GetClientID(ctx context.Context) string {
|
||||||
|
return h.ClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestDeviceCode requests a device code login flow information from Hosted
|
||||||
|
func (h *Hosted) RequestDeviceCode(ctx context.Context) (DeviceAuthInfo, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("client_id", h.ClientID)
|
||||||
|
form.Add("audience", h.Audience)
|
||||||
|
req, err := http.NewRequest("POST", h.DeviceAuthEndpoint,
|
||||||
|
strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return DeviceAuthInfo{}, fmt.Errorf("creating request failed with error: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
res, err := h.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return DeviceAuthInfo{}, fmt.Errorf("doing request failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return DeviceAuthInfo{}, fmt.Errorf("reading body failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return DeviceAuthInfo{}, fmt.Errorf("request device code returned status %d error: %s", res.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceCode := DeviceAuthInfo{}
|
||||||
|
err = json.Unmarshal(body, &deviceCode)
|
||||||
|
if err != nil {
|
||||||
|
return DeviceAuthInfo{}, fmt.Errorf("unmarshaling response failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hosted) requestToken(info DeviceAuthInfo) (TokenRequestResponse, error) {
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("client_id", h.ClientID)
|
||||||
|
form.Add("grant_type", HostedGrantType)
|
||||||
|
form.Add("device_code", info.DeviceCode)
|
||||||
|
req, err := http.NewRequest("POST", h.TokenEndpoint, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("failed to create request access token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
res, err := h.HTTPClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("failed to request access token with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := res.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("failed reading access token response body with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode > 499 {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("access token response returned code: %s", string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenResponse := TokenRequestResponse{}
|
||||||
|
err = json.Unmarshal(body, &tokenResponse)
|
||||||
|
if err != nil {
|
||||||
|
return TokenRequestResponse{}, fmt.Errorf("parsing token response failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitToken waits user's login and authorize the app. Once the user's authorize
|
||||||
|
// it retrieves the access token from Hosted's endpoint and validates it before returning
|
||||||
|
func (h *Hosted) WaitToken(ctx context.Context, info DeviceAuthInfo) (TokenInfo, error) {
|
||||||
|
interval := time.Duration(info.Interval) * time.Second
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return TokenInfo{}, ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
|
||||||
|
tokenResponse, err := h.requestToken(info)
|
||||||
|
if err != nil {
|
||||||
|
return TokenInfo{}, fmt.Errorf("parsing token response failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenResponse.Error != "" {
|
||||||
|
if tokenResponse.Error == "authorization_pending" {
|
||||||
|
continue
|
||||||
|
} else if tokenResponse.Error == "slow_down" {
|
||||||
|
interval = interval + (3 * time.Second)
|
||||||
|
ticker.Reset(interval)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return TokenInfo{}, fmt.Errorf(tokenResponse.ErrorDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = isValidAccessToken(tokenResponse.AccessToken, h.Audience)
|
||||||
|
if err != nil {
|
||||||
|
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenInfo := TokenInfo{
|
||||||
|
AccessToken: tokenResponse.AccessToken,
|
||||||
|
TokenType: tokenResponse.TokenType,
|
||||||
|
RefreshToken: tokenResponse.RefreshToken,
|
||||||
|
IDToken: tokenResponse.IDToken,
|
||||||
|
ExpiresIn: tokenResponse.ExpiresIn,
|
||||||
|
}
|
||||||
|
return tokenInfo, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidAccessToken is a simple validation of the access token
|
||||||
|
func isValidAccessToken(token string, audience string) error {
|
||||||
|
if token == "" {
|
||||||
|
return fmt.Errorf("token received is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
encodedClaims := strings.Split(token, ".")[1]
|
||||||
|
claimsString, err := base64.RawURLEncoding.DecodeString(encodedClaims)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims := Claims{}
|
||||||
|
err = json.Unmarshal(claimsString, &claims)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if claims.Audience == nil {
|
||||||
|
return fmt.Errorf("required token field audience is absent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audience claim of JWT can be a string or an array of strings
|
||||||
|
typ := reflect.TypeOf(claims.Audience)
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.String:
|
||||||
|
if claims.Audience == audience {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case reflect.Slice:
|
||||||
|
for _, aud := range claims.Audience.([]interface{}) {
|
||||||
|
if audience == aud {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid JWT token audience field")
|
||||||
|
}
|
||||||
295
client/internal/oauth_test.go
Normal file
295
client/internal/oauth_test.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockHTTPClient struct {
|
||||||
|
code int
|
||||||
|
resBody string
|
||||||
|
reqBody string
|
||||||
|
MaxReqs int
|
||||||
|
count int
|
||||||
|
countResBody string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
|
body, err := io.ReadAll(req.Body)
|
||||||
|
if err == nil {
|
||||||
|
c.reqBody = string(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.MaxReqs > c.count {
|
||||||
|
c.count++
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: c.code,
|
||||||
|
Body: io.NopCloser(strings.NewReader(c.countResBody)),
|
||||||
|
}, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Response{
|
||||||
|
StatusCode: c.code,
|
||||||
|
Body: io.NopCloser(strings.NewReader(c.resBody)),
|
||||||
|
}, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHosted_RequestDeviceCode(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
inputResBody string
|
||||||
|
inputReqCode int
|
||||||
|
inputReqError error
|
||||||
|
testingErrFunc require.ErrorAssertionFunc
|
||||||
|
expectedErrorMSG string
|
||||||
|
testingFunc require.ComparisonAssertionFunc
|
||||||
|
expectedOut DeviceAuthInfo
|
||||||
|
expectedMSG string
|
||||||
|
expectPayload string
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAudience := "ok"
|
||||||
|
expectedClientID := "bla"
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("audience", expectedAudience)
|
||||||
|
form.Add("client_id", expectedClientID)
|
||||||
|
expectPayload := form.Encode()
|
||||||
|
|
||||||
|
testCase1 := test{
|
||||||
|
name: "Payload Is Valid",
|
||||||
|
expectPayload: expectPayload,
|
||||||
|
inputReqCode: 200,
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase2 := test{
|
||||||
|
name: "Exit On Network Error",
|
||||||
|
inputReqError: fmt.Errorf("error"),
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
expectedErrorMSG: "should return error",
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: expectPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase3 := test{
|
||||||
|
name: "Exit On Exit Code",
|
||||||
|
inputReqCode: 400,
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
expectedErrorMSG: "should return error",
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: expectPayload,
|
||||||
|
}
|
||||||
|
testCase4Out := DeviceAuthInfo{ExpiresIn: 10}
|
||||||
|
testCase4 := test{
|
||||||
|
name: "Got Device Code",
|
||||||
|
inputResBody: fmt.Sprintf("{\"expires_in\":%d}", testCase4Out.ExpiresIn),
|
||||||
|
expectPayload: expectPayload,
|
||||||
|
inputReqCode: 200,
|
||||||
|
testingErrFunc: require.NoError,
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
expectedOut: testCase4Out,
|
||||||
|
expectedMSG: "out should match",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4} {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
httpClient := mockHTTPClient{
|
||||||
|
resBody: testCase.inputResBody,
|
||||||
|
code: testCase.inputReqCode,
|
||||||
|
err: testCase.inputReqError,
|
||||||
|
}
|
||||||
|
|
||||||
|
hosted := Hosted{
|
||||||
|
Audience: expectedAudience,
|
||||||
|
ClientID: expectedClientID,
|
||||||
|
TokenEndpoint: "test.hosted.com/token",
|
||||||
|
DeviceAuthEndpoint: "test.hosted.com/device/auth",
|
||||||
|
HTTPClient: &httpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
authInfo, err := hosted.RequestDeviceCode(context.TODO())
|
||||||
|
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
||||||
|
|
||||||
|
require.EqualValues(t, expectPayload, httpClient.reqBody, "payload should match")
|
||||||
|
|
||||||
|
testCase.testingFunc(t, testCase.expectedOut, authInfo, testCase.expectedMSG)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHosted_WaitToken(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
name string
|
||||||
|
inputResBody string
|
||||||
|
inputReqCode int
|
||||||
|
inputReqError error
|
||||||
|
inputMaxReqs int
|
||||||
|
inputCountResBody string
|
||||||
|
inputTimeout time.Duration
|
||||||
|
inputInfo DeviceAuthInfo
|
||||||
|
inputAudience string
|
||||||
|
testingErrFunc require.ErrorAssertionFunc
|
||||||
|
expectedErrorMSG string
|
||||||
|
testingFunc require.ComparisonAssertionFunc
|
||||||
|
expectedOut TokenInfo
|
||||||
|
expectedMSG string
|
||||||
|
expectPayload string
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultInfo := DeviceAuthInfo{
|
||||||
|
DeviceCode: "test",
|
||||||
|
ExpiresIn: 10,
|
||||||
|
Interval: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
clientID := "test"
|
||||||
|
|
||||||
|
form := url.Values{}
|
||||||
|
form.Add("grant_type", HostedGrantType)
|
||||||
|
form.Add("device_code", defaultInfo.DeviceCode)
|
||||||
|
form.Add("client_id", clientID)
|
||||||
|
tokenReqPayload := form.Encode()
|
||||||
|
|
||||||
|
testCase1 := test{
|
||||||
|
name: "Payload Is Valid",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
|
||||||
|
inputReqCode: 200,
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: tokenReqPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase2 := test{
|
||||||
|
name: "Exit On Network Error",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
|
||||||
|
expectPayload: tokenReqPayload,
|
||||||
|
inputReqError: fmt.Errorf("error"),
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
expectedErrorMSG: "should return error",
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase3 := test{
|
||||||
|
name: "Exit On 4XX When Not Pending",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
|
||||||
|
inputReqCode: 400,
|
||||||
|
expectPayload: tokenReqPayload,
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
expectedErrorMSG: "should return error",
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase4 := test{
|
||||||
|
name: "Exit On Exit Code 5XX",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
|
||||||
|
inputReqCode: 500,
|
||||||
|
expectPayload: tokenReqPayload,
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
expectedErrorMSG: "should return error",
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase5 := test{
|
||||||
|
name: "Exit On Content Timeout",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputTimeout: 0 * time.Second,
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
expectedErrorMSG: "should return error",
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
}
|
||||||
|
|
||||||
|
audience := "test"
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{"aud": audience})
|
||||||
|
var hmacSampleSecret []byte
|
||||||
|
tokenString, _ := token.SignedString(hmacSampleSecret)
|
||||||
|
|
||||||
|
testCase6 := test{
|
||||||
|
name: "Exit On Invalid Audience",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
|
||||||
|
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
|
||||||
|
inputReqCode: 200,
|
||||||
|
inputAudience: "super test",
|
||||||
|
testingErrFunc: require.Error,
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: tokenReqPayload,
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase7 := test{
|
||||||
|
name: "Received Token Info",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
|
||||||
|
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
|
||||||
|
inputReqCode: 200,
|
||||||
|
inputAudience: audience,
|
||||||
|
testingErrFunc: require.NoError,
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: tokenReqPayload,
|
||||||
|
expectedOut: TokenInfo{AccessToken: tokenString},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCase8 := test{
|
||||||
|
name: "Received Token Info after Multiple tries",
|
||||||
|
inputInfo: defaultInfo,
|
||||||
|
inputResBody: fmt.Sprintf("{\"access_token\":\"%s\"}", tokenString),
|
||||||
|
inputTimeout: time.Duration(defaultInfo.ExpiresIn) * time.Second,
|
||||||
|
inputMaxReqs: 2,
|
||||||
|
inputCountResBody: "{\"error\":\"authorization_pending\"}",
|
||||||
|
inputReqCode: 200,
|
||||||
|
inputAudience: audience,
|
||||||
|
testingErrFunc: require.NoError,
|
||||||
|
testingFunc: require.EqualValues,
|
||||||
|
expectPayload: tokenReqPayload,
|
||||||
|
expectedOut: TokenInfo{AccessToken: tokenString},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range []test{testCase1, testCase2, testCase3, testCase4, testCase5, testCase6, testCase7, testCase8} {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
httpClient := mockHTTPClient{
|
||||||
|
resBody: testCase.inputResBody,
|
||||||
|
code: testCase.inputReqCode,
|
||||||
|
err: testCase.inputReqError,
|
||||||
|
MaxReqs: testCase.inputMaxReqs,
|
||||||
|
countResBody: testCase.inputCountResBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
hosted := Hosted{
|
||||||
|
Audience: testCase.inputAudience,
|
||||||
|
ClientID: clientID,
|
||||||
|
TokenEndpoint: "test.hosted.com/token",
|
||||||
|
DeviceAuthEndpoint: "test.hosted.com/device/auth",
|
||||||
|
HTTPClient: &httpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), testCase.inputTimeout)
|
||||||
|
defer cancel()
|
||||||
|
tokenInfo, err := hosted.WaitToken(ctx, testCase.inputInfo)
|
||||||
|
testCase.testingErrFunc(t, err, testCase.expectedErrorMSG)
|
||||||
|
|
||||||
|
require.EqualValues(t, testCase.expectPayload, httpClient.reqBody, "payload should match")
|
||||||
|
|
||||||
|
testCase.testingFunc(t, testCase.expectedOut, tokenInfo, testCase.expectedMSG)
|
||||||
|
|
||||||
|
require.GreaterOrEqualf(t, testCase.inputMaxReqs, httpClient.count, "should run %d times", testCase.inputMaxReqs)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
599
client/internal/peer/conn.go
Normal file
599
client/internal/peer/conn.go
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||||
|
"github.com/pion/ice/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnConfig is a peer Connection configuration
|
||||||
|
type ConnConfig struct {
|
||||||
|
|
||||||
|
// Key is a public key of a remote peer
|
||||||
|
Key string
|
||||||
|
// LocalKey is a public key of a local peer
|
||||||
|
LocalKey string
|
||||||
|
|
||||||
|
// StunTurn is a list of STUN and TURN URLs
|
||||||
|
StunTurn []*ice.URL
|
||||||
|
|
||||||
|
// InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering
|
||||||
|
// (e.g. if eth0 is in the list, host candidate of this interface won't be used)
|
||||||
|
InterfaceBlackList []string
|
||||||
|
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
ProxyConfig proxy.Config
|
||||||
|
|
||||||
|
UDPMux ice.UDPMux
|
||||||
|
UDPMuxSrflx ice.UniversalUDPMux
|
||||||
|
|
||||||
|
LocalWgPort int
|
||||||
|
}
|
||||||
|
|
||||||
|
// OfferAnswer represents a session establishment offer or answer
|
||||||
|
type OfferAnswer struct {
|
||||||
|
IceCredentials IceCredentials
|
||||||
|
// WgListenPort is a remote WireGuard listen port.
|
||||||
|
// This field is used when establishing a direct WireGuard connection without any proxy.
|
||||||
|
// We can set the remote peer's endpoint with this port.
|
||||||
|
WgListenPort int
|
||||||
|
|
||||||
|
// Version of NetBird Agent
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IceCredentials ICE protocol credentials struct
|
||||||
|
type IceCredentials struct {
|
||||||
|
UFrag string
|
||||||
|
Pwd string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Conn struct {
|
||||||
|
config ConnConfig
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
// signalCandidate is a handler function to signal remote peer about local connection candidate
|
||||||
|
signalCandidate func(candidate ice.Candidate) error
|
||||||
|
// signalOffer is a handler function to signal remote peer our connection offer (credentials)
|
||||||
|
signalOffer func(OfferAnswer) error
|
||||||
|
signalAnswer func(OfferAnswer) error
|
||||||
|
|
||||||
|
// remoteOffersCh is a channel used to wait for remote credentials to proceed with the connection
|
||||||
|
remoteOffersCh chan OfferAnswer
|
||||||
|
// remoteAnswerCh is a channel used to wait for remote credentials answer (confirmation of our offer) to proceed with the connection
|
||||||
|
remoteAnswerCh chan OfferAnswer
|
||||||
|
closeCh chan struct{}
|
||||||
|
ctx context.Context
|
||||||
|
notifyDisconnected context.CancelFunc
|
||||||
|
|
||||||
|
agent *ice.Agent
|
||||||
|
status ConnStatus
|
||||||
|
|
||||||
|
statusRecorder *nbStatus.Status
|
||||||
|
|
||||||
|
proxy proxy.Proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConf returns the connection config
|
||||||
|
func (conn *Conn) GetConf() ConnConfig {
|
||||||
|
return conn.config
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConf updates the connection config
|
||||||
|
func (conn *Conn) UpdateConf(conf ConnConfig) {
|
||||||
|
conn.config = conf
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConn creates a new not opened Conn to the remote peer.
|
||||||
|
// To establish a connection run Conn.Open
|
||||||
|
func NewConn(config ConnConfig, statusRecorder *nbStatus.Status) (*Conn, error) {
|
||||||
|
return &Conn{
|
||||||
|
config: config,
|
||||||
|
mu: sync.Mutex{},
|
||||||
|
status: StatusDisconnected,
|
||||||
|
closeCh: make(chan struct{}),
|
||||||
|
remoteOffersCh: make(chan OfferAnswer),
|
||||||
|
remoteAnswerCh: make(chan OfferAnswer),
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// interfaceFilter is a function passed to ICE Agent to filter out not allowed interfaces
|
||||||
|
// to avoid building tunnel over them
|
||||||
|
func interfaceFilter(blackList []string) func(string) bool {
|
||||||
|
|
||||||
|
return func(iFace string) bool {
|
||||||
|
for _, s := range blackList {
|
||||||
|
if strings.HasPrefix(iFace, s) {
|
||||||
|
log.Debugf("ignoring interface %s - it is not allowed", iFace)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// look for unlisted WireGuard interfaces
|
||||||
|
wg, err := wgctrl.New()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("trying to create a wgctrl client failed with: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := wg.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = wg.Device(iFace)
|
||||||
|
return err != nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Conn) reCreateAgent() error {
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
|
failedTimeout := 6 * time.Second
|
||||||
|
var err error
|
||||||
|
conn.agent, err = ice.NewAgent(&ice.AgentConfig{
|
||||||
|
MulticastDNSMode: ice.MulticastDNSModeDisabled,
|
||||||
|
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4},
|
||||||
|
Urls: conn.config.StunTurn,
|
||||||
|
CandidateTypes: []ice.CandidateType{ice.CandidateTypeHost, ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay},
|
||||||
|
FailedTimeout: &failedTimeout,
|
||||||
|
InterfaceFilter: interfaceFilter(conn.config.InterfaceBlackList),
|
||||||
|
UDPMux: conn.config.UDPMux,
|
||||||
|
UDPMuxSrflx: conn.config.UDPMuxSrflx,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.agent.OnCandidate(conn.onICECandidate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.agent.OnConnectionStateChange(conn.onICEConnectionStateChange)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.agent.OnSelectedCandidatePairChange(conn.onICESelectedCandidatePair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func (conn *Conn) Open() error {
|
||||||
|
log.Debugf("trying to connect to peer %s", conn.config.Key)
|
||||||
|
|
||||||
|
peerState := nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
|
|
||||||
|
peerState.IP = strings.Split(conn.config.ProxyConfig.AllowedIps, "/")[0]
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
|
||||||
|
err := conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := conn.cleanup()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("error while cleaning up peer connection %s: %v", conn.config.Key, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = conn.reCreateAgent()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.sendOffer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("connection offer sent to peer %s, waiting for the confirmation", conn.config.Key)
|
||||||
|
|
||||||
|
// Only continue once we got a connection confirmation from the remote peer.
|
||||||
|
// The connection timeout could have happened before a confirmation received from the remote.
|
||||||
|
// The connection could have also been closed externally (e.g. when we received an update from the management that peer shouldn't be connected)
|
||||||
|
var remoteOfferAnswer OfferAnswer
|
||||||
|
select {
|
||||||
|
case remoteOfferAnswer = <-conn.remoteOffersCh:
|
||||||
|
// received confirmation from the remote peer -> ready to proceed
|
||||||
|
err = conn.sendAnswer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case remoteOfferAnswer = <-conn.remoteAnswerCh:
|
||||||
|
case <-time.After(conn.config.Timeout):
|
||||||
|
return NewConnectionTimeoutError(conn.config.Key, conn.config.Timeout)
|
||||||
|
case <-conn.closeCh:
|
||||||
|
// closed externally
|
||||||
|
return NewConnectionClosedError(conn.config.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("received connection confirmation from peer %s running version %s and with remote WireGuard listen port %d",
|
||||||
|
conn.config.Key, remoteOfferAnswer.Version, remoteOfferAnswer.WgListenPort)
|
||||||
|
|
||||||
|
// at this point we received offer/answer and we are ready to gather candidates
|
||||||
|
conn.mu.Lock()
|
||||||
|
conn.status = StatusConnecting
|
||||||
|
conn.ctx, conn.notifyDisconnected = context.WithCancel(context.Background())
|
||||||
|
defer conn.notifyDisconnected()
|
||||||
|
conn.mu.Unlock()
|
||||||
|
|
||||||
|
peerState = nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
|
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
err = conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("erro while updating the state of peer %s,err: %v", conn.config.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.agent.GatherCandidates()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// will block until connection succeeded
|
||||||
|
// but it won't release if ICE Agent went into Disconnected or Failed state,
|
||||||
|
// so we have to cancel it with the provided context once agent detected a broken connection
|
||||||
|
isControlling := conn.config.LocalKey > conn.config.Key
|
||||||
|
var remoteConn *ice.Conn
|
||||||
|
if isControlling {
|
||||||
|
remoteConn, err = conn.agent.Dial(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
|
} else {
|
||||||
|
remoteConn, err = conn.agent.Accept(conn.ctx, remoteOfferAnswer.IceCredentials.UFrag, remoteOfferAnswer.IceCredentials.Pwd)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamically set remote WireGuard port is other side specified a different one from the default one
|
||||||
|
remoteWgPort := iface.DefaultWgPort
|
||||||
|
if remoteOfferAnswer.WgListenPort != 0 {
|
||||||
|
remoteWgPort = remoteOfferAnswer.WgListenPort
|
||||||
|
}
|
||||||
|
// the ice connection has been established successfully so we are ready to start the proxy
|
||||||
|
err = conn.startProxy(remoteConn, remoteWgPort)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.proxy.Type() == proxy.TypeNoProxy {
|
||||||
|
host, _, _ := net.SplitHostPort(remoteConn.LocalAddr().String())
|
||||||
|
rhost, _, _ := net.SplitHostPort(remoteConn.RemoteAddr().String())
|
||||||
|
// direct Wireguard connection
|
||||||
|
log.Infof("directly connected to peer %s [laddr <-> raddr] [%s:%d <-> %s:%d]", conn.config.Key, host, iface.DefaultWgPort, rhost, iface.DefaultWgPort)
|
||||||
|
} else {
|
||||||
|
log.Infof("connected to peer %s [laddr <-> raddr] [%s <-> %s]", conn.config.Key, remoteConn.LocalAddr().String(), remoteConn.RemoteAddr().String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until connection disconnected or has been closed externally (upper layer, e.g. engine)
|
||||||
|
select {
|
||||||
|
case <-conn.closeCh:
|
||||||
|
// closed externally
|
||||||
|
return NewConnectionClosedError(conn.config.Key)
|
||||||
|
case <-conn.ctx.Done():
|
||||||
|
// disconnected from the remote peer
|
||||||
|
return NewConnectionDisconnectedError(conn.config.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// useProxy determines whether a direct connection (without a go proxy) is possible
|
||||||
|
// There are 3 cases: one of the peers has a public IP or both peers are in the same private network
|
||||||
|
// Please note, that this check happens when peers were already able to ping each other using ICE layer.
|
||||||
|
func shouldUseProxy(pair *ice.CandidatePair) bool {
|
||||||
|
remoteIP := net.ParseIP(pair.Remote.Address())
|
||||||
|
myIp := net.ParseIP(pair.Local.Address())
|
||||||
|
remoteIsPublic := IsPublicIP(remoteIP)
|
||||||
|
myIsPublic := IsPublicIP(myIp)
|
||||||
|
|
||||||
|
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//one of the hosts has a public IP
|
||||||
|
if remoteIsPublic && pair.Remote.Type() == ice.CandidateTypeHost {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if myIsPublic && pair.Local.Type() == ice.CandidateTypeHost {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.Local.Type() == ice.CandidateTypeHost && pair.Remote.Type() == ice.CandidateTypeHost {
|
||||||
|
if !remoteIsPublic && !myIsPublic {
|
||||||
|
//both hosts are in the same private network
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPublicIP indicates whether IP is public or not.
|
||||||
|
func IsPublicIP(ip net.IP) bool {
|
||||||
|
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()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
|
var pair *ice.CandidatePair
|
||||||
|
pair, err := conn.agent.GetSelectedCandidatePair()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
peerState := nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
|
useProxy := shouldUseProxy(pair)
|
||||||
|
var p proxy.Proxy
|
||||||
|
if useProxy {
|
||||||
|
p = proxy.NewWireguardProxy(conn.config.ProxyConfig)
|
||||||
|
peerState.Direct = false
|
||||||
|
} else {
|
||||||
|
p = proxy.NewNoProxy(conn.config.ProxyConfig, remoteWgPort)
|
||||||
|
peerState.Direct = true
|
||||||
|
}
|
||||||
|
conn.proxy = p
|
||||||
|
err = p.Start(remoteConn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.status = StatusConnected
|
||||||
|
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
peerState.LocalIceCandidateType = pair.Local.Type().String()
|
||||||
|
peerState.RemoteIceCandidateType = pair.Remote.Type().String()
|
||||||
|
if pair.Local.Type() == ice.CandidateTypeRelay || pair.Remote.Type() == ice.CandidateTypeRelay {
|
||||||
|
peerState.Relayed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to save peer's state, got error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup closes all open resources and sets status to StatusDisconnected
|
||||||
|
func (conn *Conn) cleanup() error {
|
||||||
|
log.Debugf("trying to cleanup %s", conn.config.Key)
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
|
if conn.agent != nil {
|
||||||
|
err := conn.agent.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.agent = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.proxy != nil {
|
||||||
|
err := conn.proxy.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn.proxy = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if conn.notifyDisconnected != nil {
|
||||||
|
conn.notifyDisconnected()
|
||||||
|
conn.notifyDisconnected = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.status = StatusDisconnected
|
||||||
|
|
||||||
|
peerState := nbStatus.PeerState{PubKey: conn.config.Key}
|
||||||
|
peerState.ConnStatus = conn.status.String()
|
||||||
|
peerState.ConnStatusUpdate = time.Now()
|
||||||
|
|
||||||
|
err := conn.statusRecorder.UpdatePeerState(peerState)
|
||||||
|
if err != nil {
|
||||||
|
// pretty common error because by that time Engine can already remove the peer and status won't be available.
|
||||||
|
//todo rethink status updates
|
||||||
|
log.Debugf("error while updating peer's %s state, err: %v", conn.config.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("cleaned up connection to peer %s", conn.config.Key)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSignalOffer sets a handler function to be triggered by Conn when a new connection offer has to be signalled to the remote peer
|
||||||
|
func (conn *Conn) SetSignalOffer(handler func(offer OfferAnswer) error) {
|
||||||
|
conn.signalOffer = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSignalAnswer sets a handler function to be triggered by Conn when a new connection answer has to be signalled to the remote peer
|
||||||
|
func (conn *Conn) SetSignalAnswer(handler func(answer OfferAnswer) error) {
|
||||||
|
conn.signalAnswer = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSignalCandidate sets a handler function to be triggered by Conn when a new ICE local connection candidate has to be signalled to the remote peer
|
||||||
|
func (conn *Conn) SetSignalCandidate(handler func(candidate ice.Candidate) error) {
|
||||||
|
conn.signalCandidate = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// onICECandidate is a callback attached to an ICE Agent to receive new local connection candidates
|
||||||
|
// and then signals them to the remote peer
|
||||||
|
func (conn *Conn) onICECandidate(candidate ice.Candidate) {
|
||||||
|
if candidate != nil {
|
||||||
|
log.Debugf("discovered local candidate %s", candidate.String())
|
||||||
|
go func() {
|
||||||
|
err := conn.signalCandidate(candidate)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed signaling candidate to the remote peer %s %s", conn.config.Key, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Conn) onICESelectedCandidatePair(c1 ice.Candidate, c2 ice.Candidate) {
|
||||||
|
log.Debugf("selected candidate pair [local <-> remote] -> [%s <-> %s], peer %s", c1.String(), c2.String(),
|
||||||
|
conn.config.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// onICEConnectionStateChange registers callback of an ICE Agent to track connection state
|
||||||
|
func (conn *Conn) onICEConnectionStateChange(state ice.ConnectionState) {
|
||||||
|
log.Debugf("peer %s ICE ConnectionState has changed to %s", conn.config.Key, state.String())
|
||||||
|
if state == ice.ConnectionStateFailed || state == ice.ConnectionStateDisconnected {
|
||||||
|
conn.notifyDisconnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Conn) sendAnswer() error {
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
|
localUFrag, localPwd, err := conn.agent.GetLocalUserCredentials()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("sending answer to %s", conn.config.Key)
|
||||||
|
err = conn.signalAnswer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{localUFrag, localPwd},
|
||||||
|
WgListenPort: conn.config.LocalWgPort,
|
||||||
|
Version: system.NetbirdVersion(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendOffer prepares local user credentials and signals them to the remote peer
|
||||||
|
func (conn *Conn) sendOffer() error {
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
|
localUFrag, localPwd, err := conn.agent.GetLocalUserCredentials()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = conn.signalOffer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{localUFrag, localPwd},
|
||||||
|
WgListenPort: conn.config.LocalWgPort,
|
||||||
|
Version: system.NetbirdVersion(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes this peer Conn issuing a close event to the Conn closeCh
|
||||||
|
func (conn *Conn) Close() error {
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
select {
|
||||||
|
case conn.closeCh <- struct{}{}:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
// probably could happen when peer has been added and removed right after not even starting to connect
|
||||||
|
// todo further investigate
|
||||||
|
// this really happens due to unordered messages coming from management
|
||||||
|
// more importantly it causes inconsistency -> 2 Conn objects for the same peer
|
||||||
|
// e.g. this flow:
|
||||||
|
// update from management has peers: [1,2,3,4]
|
||||||
|
// engine creates a Conn for peers: [1,2,3,4] and schedules Open in ~1sec
|
||||||
|
// before conn.Open() another update from management arrives with peers: [1,2,3]
|
||||||
|
// engine removes peer 4 and calls conn.Close() which does nothing (this default clause)
|
||||||
|
// before conn.Open() another update from management arrives with peers: [1,2,3,4,5]
|
||||||
|
// engine adds a new Conn for 4 and 5
|
||||||
|
// therefore peer 4 has 2 Conn objects
|
||||||
|
log.Warnf("connection has been already closed or attempted closing not started coonection %s", conn.config.Key)
|
||||||
|
return NewConnectionAlreadyClosed(conn.config.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status returns current status of the Conn
|
||||||
|
func (conn *Conn) Status() ConnStatus {
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
return conn.status
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRemoteOffer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
|
||||||
|
// doesn't block, discards the message if connection wasn't ready
|
||||||
|
func (conn *Conn) OnRemoteOffer(offer OfferAnswer) bool {
|
||||||
|
log.Debugf("OnRemoteOffer from peer %s on status %s", conn.config.Key, conn.status.String())
|
||||||
|
|
||||||
|
select {
|
||||||
|
case conn.remoteOffersCh <- offer:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
log.Debugf("OnRemoteOffer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
|
||||||
|
// connection might not be ready yet to receive so we ignore the message
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRemoteAnswer handles an offer from the remote peer and returns true if the message was accepted, false otherwise
|
||||||
|
// doesn't block, discards the message if connection wasn't ready
|
||||||
|
func (conn *Conn) OnRemoteAnswer(answer OfferAnswer) bool {
|
||||||
|
log.Debugf("OnRemoteAnswer from peer %s on status %s", conn.config.Key, conn.status.String())
|
||||||
|
|
||||||
|
select {
|
||||||
|
case conn.remoteAnswerCh <- answer:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
// connection might not be ready yet to receive so we ignore the message
|
||||||
|
log.Debugf("OnRemoteAnswer skipping message from peer %s on status %s because is not ready", conn.config.Key, conn.status.String())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
||||||
|
func (conn *Conn) OnRemoteCandidate(candidate ice.Candidate) {
|
||||||
|
log.Debugf("OnRemoteCandidate from peer %s -> %s", conn.config.Key, candidate.String())
|
||||||
|
go func() {
|
||||||
|
conn.mu.Lock()
|
||||||
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
|
if conn.agent == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := conn.agent.AddRemoteCandidate(candidate)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while handling remote candidate from peer %s", conn.config.Key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Conn) GetKey() string {
|
||||||
|
return conn.config.Key
|
||||||
|
}
|
||||||
167
client/internal/peer/conn_test.go
Normal file
167
client/internal/peer/conn_test.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/magiconair/properties/assert"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/proxy"
|
||||||
|
nbstatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/pion/ice/v2"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var connConf = ConnConfig{
|
||||||
|
Key: "LLHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
LocalKey: "RRHf3Ma6z6mdLbriAJbqhX7+nM/B71lgw2+91q3LfhU=",
|
||||||
|
StunTurn: []*ice.URL{},
|
||||||
|
InterfaceBlackList: nil,
|
||||||
|
Timeout: time.Second,
|
||||||
|
ProxyConfig: proxy.Config{},
|
||||||
|
LocalWgPort: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConn_interfaceFilter(t *testing.T) {
|
||||||
|
ignore := []string{iface.WgInterfaceDefault, "tun0", "zt", "ZeroTier", "utun", "wg", "ts",
|
||||||
|
"Tailscale", "tailscale"}
|
||||||
|
|
||||||
|
filter := interfaceFilter(ignore)
|
||||||
|
|
||||||
|
for _, s := range ignore {
|
||||||
|
assert.Equal(t, filter(s), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConn_GetKey(t *testing.T) {
|
||||||
|
conn, err := NewConn(connConf, nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
got := conn.GetKey()
|
||||||
|
|
||||||
|
assert.Equal(t, got, connConf.Key, "they should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConn_OnRemoteOffer(t *testing.T) {
|
||||||
|
|
||||||
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
<-conn.remoteOffersCh
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
accepted := conn.OnRemoteOffer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{
|
||||||
|
UFrag: "test",
|
||||||
|
Pwd: "test",
|
||||||
|
},
|
||||||
|
WgListenPort: 0,
|
||||||
|
Version: "",
|
||||||
|
})
|
||||||
|
if accepted {
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConn_OnRemoteAnswer(t *testing.T) {
|
||||||
|
|
||||||
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(2)
|
||||||
|
go func() {
|
||||||
|
<-conn.remoteAnswerCh
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
accepted := conn.OnRemoteAnswer(OfferAnswer{
|
||||||
|
IceCredentials: IceCredentials{
|
||||||
|
UFrag: "test",
|
||||||
|
Pwd: "test",
|
||||||
|
},
|
||||||
|
WgListenPort: 0,
|
||||||
|
Version: "",
|
||||||
|
})
|
||||||
|
if accepted {
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
func TestConn_Status(t *testing.T) {
|
||||||
|
|
||||||
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tables := []struct {
|
||||||
|
name string
|
||||||
|
status ConnStatus
|
||||||
|
want ConnStatus
|
||||||
|
}{
|
||||||
|
{"StatusConnected", StatusConnected, StatusConnected},
|
||||||
|
{"StatusDisconnected", StatusDisconnected, StatusDisconnected},
|
||||||
|
{"StatusConnecting", StatusConnecting, StatusConnecting},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
t.Run(table.name, func(t *testing.T) {
|
||||||
|
conn.status = table.status
|
||||||
|
|
||||||
|
got := conn.Status()
|
||||||
|
assert.Equal(t, got, table.want, "they should be equal")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConn_Close(t *testing.T) {
|
||||||
|
|
||||||
|
conn, err := NewConn(connConf, nbstatus.NewRecorder())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
<-conn.closeCh
|
||||||
|
wg.Done()
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
err := conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
72
client/internal/peer/error.go
Normal file
72
client/internal/peer/error.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConnectionTimeoutError is an error indicating that a peer Conn has been timed out
|
||||||
|
type ConnectionTimeoutError struct {
|
||||||
|
peer string
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnectionTimeoutError) Error() string {
|
||||||
|
return fmt.Sprintf("connection to peer %s timed out after %s", e.peer, e.timeout.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnectionTimeoutError creates a new ConnectionTimeoutError error
|
||||||
|
func NewConnectionTimeoutError(peer string, timeout time.Duration) error {
|
||||||
|
return &ConnectionTimeoutError{
|
||||||
|
peer: peer,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionClosedError is an error indicating that a peer Conn has been forcefully closed
|
||||||
|
type ConnectionClosedError struct {
|
||||||
|
peer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnectionClosedError) Error() string {
|
||||||
|
return fmt.Sprintf("connection to peer %s has been closed", e.peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnectionClosedError creates a new ConnectionClosedError error
|
||||||
|
func NewConnectionClosedError(peer string) error {
|
||||||
|
return &ConnectionClosedError{
|
||||||
|
peer: peer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionDisconnectedError is an error indicating that a peer Conn has ctx from the remote
|
||||||
|
type ConnectionDisconnectedError struct {
|
||||||
|
peer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnectionDisconnectedError) Error() string {
|
||||||
|
return fmt.Sprintf("disconnected from peer %s", e.peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnectionDisconnectedError creates a new ConnectionDisconnectedError error
|
||||||
|
func NewConnectionDisconnectedError(peer string) error {
|
||||||
|
return &ConnectionDisconnectedError{
|
||||||
|
peer: peer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectionAlreadyClosedError is an error indicating that a peer Conn has been already closed and the invocation of the Close() method has been performed over a closed connection
|
||||||
|
type ConnectionAlreadyClosedError struct {
|
||||||
|
peer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ConnectionAlreadyClosedError) Error() string {
|
||||||
|
return fmt.Sprintf("connection to peer %s has been already closed", e.peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConnectionAlreadyClosed creates a new ConnectionAlreadyClosedError error
|
||||||
|
func NewConnectionAlreadyClosed(peer string) error {
|
||||||
|
return &ConnectionAlreadyClosedError{
|
||||||
|
peer: peer,
|
||||||
|
}
|
||||||
|
}
|
||||||
27
client/internal/peer/error_test.go
Normal file
27
client/internal/peer/error_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewConnectionClosedError(t *testing.T) {
|
||||||
|
err := NewConnectionClosedError("X")
|
||||||
|
assert.Equal(t, &ConnectionClosedError{peer: "X"}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConnectionDisconnectedError(t *testing.T) {
|
||||||
|
err := NewConnectionDisconnectedError("X")
|
||||||
|
assert.Equal(t, &ConnectionDisconnectedError{peer: "X"}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConnectionTimeoutErrorC(t *testing.T) {
|
||||||
|
err := NewConnectionTimeoutError("X", time.Second)
|
||||||
|
assert.Equal(t, &ConnectionTimeoutError{peer: "X", timeout: time.Second}, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewConnectionAlreadyClosed(t *testing.T) {
|
||||||
|
err := NewConnectionAlreadyClosed("X")
|
||||||
|
assert.Equal(t, &ConnectionAlreadyClosedError{peer: "X"}, err)
|
||||||
|
}
|
||||||
25
client/internal/peer/status.go
Normal file
25
client/internal/peer/status.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
type ConnStatus int
|
||||||
|
|
||||||
|
func (s ConnStatus) String() string {
|
||||||
|
switch s {
|
||||||
|
case StatusConnecting:
|
||||||
|
return "Connecting"
|
||||||
|
case StatusConnected:
|
||||||
|
return "Connected"
|
||||||
|
case StatusDisconnected:
|
||||||
|
return "Disconnected"
|
||||||
|
default:
|
||||||
|
log.Errorf("unknown status: %d", s)
|
||||||
|
return "INVALID_PEER_CONNECTION_STATUS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusConnected ConnStatus = iota
|
||||||
|
StatusConnecting
|
||||||
|
StatusDisconnected
|
||||||
|
)
|
||||||
27
client/internal/peer/status_test.go
Normal file
27
client/internal/peer/status_test.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/magiconair/properties/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConnStatus_String(t *testing.T) {
|
||||||
|
|
||||||
|
tables := []struct {
|
||||||
|
name string
|
||||||
|
status ConnStatus
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"StatusConnected", StatusConnected, "Connected"},
|
||||||
|
{"StatusDisconnected", StatusDisconnected, "Disconnected"},
|
||||||
|
{"StatusConnecting", StatusConnecting, "Connecting"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
t.Run(table.name, func(t *testing.T) {
|
||||||
|
got := table.status.String()
|
||||||
|
assert.Equal(t, got, table.want, "they should be equal")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
72
client/internal/proxy/dummy.go
Normal file
72
client/internal/proxy/dummy.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DummyProxy just sends pings to the RemoteKey peer and reads responses
|
||||||
|
type DummyProxy struct {
|
||||||
|
conn net.Conn
|
||||||
|
remote string
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDummyProxy(remote string) *DummyProxy {
|
||||||
|
p := &DummyProxy{remote: remote}
|
||||||
|
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DummyProxy) Close() error {
|
||||||
|
p.cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DummyProxy) Start(remoteConn net.Conn) error {
|
||||||
|
p.conn = remoteConn
|
||||||
|
go func() {
|
||||||
|
buf := make([]byte, 1500)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, err := p.conn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while reading RemoteKey %s proxy %v", p.remote, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//log.Debugf("received %s from %s", string(buf[:n]), p.remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
_, err := p.conn.Write([]byte("hello"))
|
||||||
|
//log.Debugf("sent ping to %s", p.remote)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while writing to RemoteKey %s proxy %v", p.remote, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *DummyProxy) Type() Type {
|
||||||
|
return TypeDummy
|
||||||
|
}
|
||||||
55
client/internal/proxy/noproxy.go
Normal file
55
client/internal/proxy/noproxy.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NoProxy 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)
|
||||||
|
// NoProxy will just update remote peer with a remote host and fixed Wireguard port (r.g. 51820).
|
||||||
|
// In order NoProxy to work, Wireguard port has to be fixed for the time being.
|
||||||
|
type NoProxy struct {
|
||||||
|
config Config
|
||||||
|
// RemoteWgListenPort is a WireGuard port of a remote peer.
|
||||||
|
// It is used instead of the hardcoded 51820 port.
|
||||||
|
RemoteWgListenPort int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNoProxy creates a new NoProxy with a provided config and remote peer's WireGuard listen port
|
||||||
|
func NewNoProxy(config Config, remoteWgPort int) *NoProxy {
|
||||||
|
return &NoProxy{config: config, RemoteWgListenPort: remoteWgPort}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NoProxy) Close() error {
|
||||||
|
err := p.config.WgInterface.RemovePeer(p.config.RemoteKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start just updates Wireguard peer with the remote IP and default Wireguard port
|
||||||
|
func (p *NoProxy) Start(remoteConn net.Conn) error {
|
||||||
|
|
||||||
|
log.Debugf("using NoProxy while connecting to peer %s", p.config.RemoteKey)
|
||||||
|
addr, err := net.ResolveUDPAddr("udp", remoteConn.RemoteAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
addr.Port = p.RemoteWgListenPort
|
||||||
|
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
|
||||||
|
addr, p.config.PreSharedKey)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *NoProxy) Type() Type {
|
||||||
|
return TypeNoProxy
|
||||||
|
}
|
||||||
34
client/internal/proxy/proxy.go
Normal file
34
client/internal/proxy/proxy.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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 (
|
||||||
|
TypeNoProxy Type = "NoProxy"
|
||||||
|
TypeWireguard Type = "Wireguard"
|
||||||
|
TypeDummy Type = "Dummy"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
Start(remoteConn net.Conn) error
|
||||||
|
Type() Type
|
||||||
|
}
|
||||||
128
client/internal/proxy/wireguard.go
Normal file
128
client/internal/proxy/wireguard.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WireguardProxy proxies
|
||||||
|
type WireguardProxy struct {
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
config Config
|
||||||
|
|
||||||
|
remoteConn net.Conn
|
||||||
|
localConn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWireguardProxy(config Config) *WireguardProxy {
|
||||||
|
p := &WireguardProxy{config: config}
|
||||||
|
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WireguardProxy) updateEndpoint() error {
|
||||||
|
udpAddr, err := net.ResolveUDPAddr(p.localConn.LocalAddr().Network(), p.localConn.LocalAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// add local proxy connection as a Wireguard peer
|
||||||
|
err = p.config.WgInterface.UpdatePeer(p.config.RemoteKey, p.config.AllowedIps, DefaultWgKeepAlive,
|
||||||
|
udpAddr, p.config.PreSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WireguardProxy) Start(remoteConn net.Conn) error {
|
||||||
|
p.remoteConn = remoteConn
|
||||||
|
|
||||||
|
var err error
|
||||||
|
p.localConn, err = net.Dial("udp", p.config.WgListenAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed dialing to local Wireguard port %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.updateEndpoint()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while updating Wireguard peer endpoint [%s] %v", p.config.RemoteKey, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go p.proxyToRemote()
|
||||||
|
go p.proxyToLocal()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WireguardProxy) Close() error {
|
||||||
|
p.cancel()
|
||||||
|
if c := p.localConn; c != nil {
|
||||||
|
err := p.localConn.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := p.config.WgInterface.RemovePeer(p.config.RemoteKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyToRemote proxies everything from Wireguard to the RemoteKey peer
|
||||||
|
// blocks
|
||||||
|
func (p *WireguardProxy) proxyToRemote() {
|
||||||
|
|
||||||
|
buf := make([]byte, 1500)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
log.Debugf("stopped proxying to remote peer %s due to closed connection", p.config.RemoteKey)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
n, err := p.localConn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.remoteConn.Write(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxyToLocal proxies everything from the RemoteKey peer to local Wireguard
|
||||||
|
// blocks
|
||||||
|
func (p *WireguardProxy) proxyToLocal() {
|
||||||
|
|
||||||
|
buf := make([]byte, 1500)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-p.ctx.Done():
|
||||||
|
log.Debugf("stopped proxying from remote peer %s due to closed connection", p.config.RemoteKey)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
n, err := p.remoteConn.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.localConn.Write(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *WireguardProxy) Type() Type {
|
||||||
|
return TypeWireguard
|
||||||
|
}
|
||||||
285
client/internal/routemanager/client.go
Normal file
285
client/internal/routemanager/client.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
|
"github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
type routerPeerStatus struct {
|
||||||
|
connected bool
|
||||||
|
relayed bool
|
||||||
|
direct bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type routesUpdate struct {
|
||||||
|
updateSerial uint64
|
||||||
|
routes []*route.Route
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientNetwork struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
statusRecorder *status.Status
|
||||||
|
wgInterface *iface.WGIface
|
||||||
|
routes map[string]*route.Route
|
||||||
|
routeUpdate chan routesUpdate
|
||||||
|
peerStateUpdate chan struct{}
|
||||||
|
routePeersNotifiers map[string]chan struct{}
|
||||||
|
chosenRoute *route.Route
|
||||||
|
network netip.Prefix
|
||||||
|
updateSerial uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientNetworkWatcher(ctx context.Context, wgInterface *iface.WGIface, statusRecorder *status.Status, network netip.Prefix) *clientNetwork {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
client := &clientNetwork{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
wgInterface: wgInterface,
|
||||||
|
routes: make(map[string]*route.Route),
|
||||||
|
routePeersNotifiers: make(map[string]chan struct{}),
|
||||||
|
routeUpdate: make(chan routesUpdate),
|
||||||
|
peerStateUpdate: make(chan struct{}),
|
||||||
|
network: network,
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientNetworkID(input *route.Route) string {
|
||||||
|
return input.NetID + "-" + input.Network.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) getRouterPeerStatuses() map[string]routerPeerStatus {
|
||||||
|
routePeerStatuses := make(map[string]routerPeerStatus)
|
||||||
|
for _, r := range c.routes {
|
||||||
|
peerStatus, err := c.statusRecorder.GetPeer(r.Peer)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("couldn't fetch peer state: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
routePeerStatuses[r.ID] = routerPeerStatus{
|
||||||
|
connected: peerStatus.ConnStatus == peer.StatusConnected.String(),
|
||||||
|
relayed: peerStatus.Relayed,
|
||||||
|
direct: peerStatus.Direct,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routePeerStatuses
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) getBestRouteFromStatuses(routePeerStatuses map[string]routerPeerStatus) string {
|
||||||
|
var chosen string
|
||||||
|
chosenScore := 0
|
||||||
|
|
||||||
|
currID := ""
|
||||||
|
if c.chosenRoute != nil {
|
||||||
|
currID = c.chosenRoute.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range c.routes {
|
||||||
|
tempScore := 0
|
||||||
|
peerStatus, found := routePeerStatuses[r.ID]
|
||||||
|
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 {
|
||||||
|
tempScore++
|
||||||
|
}
|
||||||
|
if tempScore > chosenScore || (tempScore == chosenScore && currID == r.ID) {
|
||||||
|
chosen = r.ID
|
||||||
|
chosenScore = tempScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosen == "" {
|
||||||
|
var peers []string
|
||||||
|
for _, r := range c.routes {
|
||||||
|
peers = append(peers, r.Peer)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chosen
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) watchPeerStatusChanges(ctx context.Context, peerKey string, peerStateUpdate chan struct{}, closer chan struct{}) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-closer:
|
||||||
|
return
|
||||||
|
case <-c.statusRecorder.GetPeerStateChangeNotifier(peerKey):
|
||||||
|
state, err := c.statusRecorder.GetPeer(peerKey)
|
||||||
|
if err != nil || state.ConnStatus == peer.StatusConnecting.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
peerStateUpdate <- struct{}{}
|
||||||
|
log.Debugf("triggered route state update for Peer %s, state: %s", peerKey, state.ConnStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) startPeersStatusChangeWatcher() {
|
||||||
|
for _, r := range c.routes {
|
||||||
|
_, found := c.routePeersNotifiers[r.Peer]
|
||||||
|
if !found {
|
||||||
|
c.routePeersNotifiers[r.Peer] = make(chan struct{})
|
||||||
|
go c.watchPeerStatusChanges(c.ctx, r.Peer, c.peerStateUpdate, c.routePeersNotifiers[r.Peer])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) removeRouteFromWireguardPeer(peerKey string) error {
|
||||||
|
state, err := c.statusRecorder.GetPeer(peerKey)
|
||||||
|
if err != nil || state.ConnStatus != peer.StatusConnected.String() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.wgInterface.RemoveAllowedIP(peerKey, c.network.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't remove allowed IP %s removed for peer %s, err: %v",
|
||||||
|
c.network, c.chosenRoute.Peer, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) removeRouteFromPeerAndSystem() error {
|
||||||
|
if c.chosenRoute != nil {
|
||||||
|
err := c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = removeFromRouteTableIfNonSystem(c.network, c.wgInterface.GetAddress().IP.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't remove route %s from system, err: %v",
|
||||||
|
c.network, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) recalculateRouteAndUpdatePeerAndSystem() error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
routerPeerStatuses := c.getRouterPeerStatuses()
|
||||||
|
|
||||||
|
chosen := c.getBestRouteFromStatuses(routerPeerStatuses)
|
||||||
|
if chosen == "" {
|
||||||
|
err = c.removeRouteFromPeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.chosenRoute = nil
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.chosenRoute != nil && c.chosenRoute.ID == chosen {
|
||||||
|
if c.chosenRoute.IsEqual(c.routes[chosen]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.chosenRoute != nil {
|
||||||
|
err = c.removeRouteFromWireguardPeer(c.chosenRoute.Peer)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = addToRouteTableIfNoExists(c.network, c.wgInterface.GetAddress().IP.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("route %s couldn't be added for peer %s, err: %v",
|
||||||
|
c.network.String(), c.wgInterface.GetAddress().IP.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.chosenRoute = c.routes[chosen]
|
||||||
|
err = c.wgInterface.AddAllowedIP(c.chosenRoute.Peer, c.network.String())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("couldn't add allowed IP %s added for peer %s, err: %v",
|
||||||
|
c.network, c.chosenRoute.Peer, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) sendUpdateToClientNetworkWatcher(update routesUpdate) {
|
||||||
|
go func() {
|
||||||
|
c.routeUpdate <- update
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientNetwork) handleUpdate(update routesUpdate) {
|
||||||
|
updateMap := make(map[string]*route.Route)
|
||||||
|
|
||||||
|
for _, r := range update.routes {
|
||||||
|
updateMap[r.ID] = r
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, r := range c.routes {
|
||||||
|
_, found := updateMap[id]
|
||||||
|
if !found {
|
||||||
|
close(c.routePeersNotifiers[r.Peer])
|
||||||
|
delete(c.routePeersNotifiers, r.Peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.routes = updateMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// peersStateAndUpdateWatcher is the main point of reacting on client network routing events.
|
||||||
|
// All the processing related to the client network should be done here. Thread-safe.
|
||||||
|
func (c *clientNetwork) peersStateAndUpdateWatcher() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
log.Debugf("stopping watcher for network %s", c.network)
|
||||||
|
err := c.removeRouteFromPeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case <-c.peerStateUpdate:
|
||||||
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
case update := <-c.routeUpdate:
|
||||||
|
if update.updateSerial < c.updateSerial {
|
||||||
|
log.Warnf("received a routes update with smaller serial number, ignoring it")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("received a new client network route update for %s", c.network)
|
||||||
|
|
||||||
|
c.handleUpdate(update)
|
||||||
|
|
||||||
|
c.updateSerial = update.updateSerial
|
||||||
|
|
||||||
|
err := c.recalculateRouteAndUpdatePeerAndSystem()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.startPeersStatusChangeWatcher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
client/internal/routemanager/common_linux_test.go
Normal file
75
client/internal/routemanager/common_linux_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
var insertRuleTestCases = []struct {
|
||||||
|
name string
|
||||||
|
inputPair routerPair
|
||||||
|
ipVersion string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding IPV4 Rule",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.200.0/24",
|
||||||
|
masquerade: false,
|
||||||
|
},
|
||||||
|
ipVersion: ipv4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding And Nat IPV4 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.200.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding IPV6 Rule",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc12::/64",
|
||||||
|
masquerade: false,
|
||||||
|
},
|
||||||
|
ipVersion: ipv6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Insert Forwarding And Nat IPV6 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc12::/64",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv6,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeRuleTestCases = []struct {
|
||||||
|
name string
|
||||||
|
inputPair routerPair
|
||||||
|
ipVersion string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Remove Forwarding And Nat IPV4 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.200.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove Forwarding And Nat IPV6 Rules",
|
||||||
|
inputPair: routerPair{
|
||||||
|
ID: "zxa",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc12::/64",
|
||||||
|
masquerade: true,
|
||||||
|
},
|
||||||
|
ipVersion: ipv6,
|
||||||
|
},
|
||||||
|
}
|
||||||
12
client/internal/routemanager/firewall.go
Normal file
12
client/internal/routemanager/firewall.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
type firewallManager interface {
|
||||||
|
// RestoreOrCreateContainers restores or creates a firewall container set of rules, tables and default rules
|
||||||
|
RestoreOrCreateContainers() error
|
||||||
|
// InsertRoutingRules inserts a routing firewall rule
|
||||||
|
InsertRoutingRules(pair routerPair) error
|
||||||
|
// RemoveRoutingRules removes a routing firewall rule
|
||||||
|
RemoveRoutingRules(pair routerPair) error
|
||||||
|
// CleanRoutingRules cleans a firewall set of containers
|
||||||
|
CleanRoutingRules()
|
||||||
|
}
|
||||||
67
client/internal/routemanager/firewall_linux.go
Normal file
67
client/internal/routemanager/firewall_linux.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
import "github.com/google/nftables"
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipv6Forwarding = "netbird-rt-ipv6-forwarding"
|
||||||
|
ipv4Forwarding = "netbird-rt-ipv4-forwarding"
|
||||||
|
ipv6Nat = "netbird-rt-ipv6-nat"
|
||||||
|
ipv4Nat = "netbird-rt-ipv4-nat"
|
||||||
|
natFormat = "netbird-nat-%s"
|
||||||
|
forwardingFormat = "netbird-fwd-%s"
|
||||||
|
inNatFormat = "netbird-nat-in-%s"
|
||||||
|
inForwardingFormat = "netbird-fwd-in-%s"
|
||||||
|
ipv6 = "ipv6"
|
||||||
|
ipv4 = "ipv4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func genKey(format string, input string) string {
|
||||||
|
return fmt.Sprintf(format, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFirewall if supported, returns an iptables manager, otherwise returns a nftables manager
|
||||||
|
func NewFirewall(parentCTX context.Context) firewallManager {
|
||||||
|
ctx, cancel := context.WithCancel(parentCTX)
|
||||||
|
|
||||||
|
if isIptablesSupported() {
|
||||||
|
log.Debugf("iptables is supported")
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
|
||||||
|
return &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("iptables is not supported, using nftables")
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInPair(pair routerPair) routerPair {
|
||||||
|
return routerPair{
|
||||||
|
ID: pair.ID,
|
||||||
|
// invert source/destination
|
||||||
|
source: pair.destination,
|
||||||
|
destination: pair.source,
|
||||||
|
masquerade: pair.masquerade,
|
||||||
|
}
|
||||||
|
}
|
||||||
27
client/internal/routemanager/firewall_nonlinux.go
Normal file
27
client/internal/routemanager/firewall_nonlinux.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package routemanager
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type unimplementedFirewall struct{}
|
||||||
|
|
||||||
|
func (unimplementedFirewall) RestoreOrCreateContainers() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (unimplementedFirewall) InsertRoutingRules(pair routerPair) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (unimplementedFirewall) RemoveRoutingRules(pair routerPair) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (unimplementedFirewall) CleanRoutingRules() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFirewall returns an unimplemented Firewall manager
|
||||||
|
func NewFirewall(parentCtx context.Context) firewallManager {
|
||||||
|
return unimplementedFirewall{}
|
||||||
|
}
|
||||||
436
client/internal/routemanager/iptables_linux.go
Normal file
436
client/internal/routemanager/iptables_linux.go
Normal file
@@ -0,0 +1,436 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isIptablesSupported() bool {
|
||||||
|
_, err4 := exec.LookPath("iptables")
|
||||||
|
_, err6 := exec.LookPath("ip6tables")
|
||||||
|
return err4 == nil && err6 == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// constants needed to manage and create iptable rules
|
||||||
|
const (
|
||||||
|
iptablesFilterTable = "filter"
|
||||||
|
iptablesNatTable = "nat"
|
||||||
|
iptablesForwardChain = "FORWARD"
|
||||||
|
iptablesPostRoutingChain = "POSTROUTING"
|
||||||
|
iptablesRoutingNatChain = "NETBIRD-RT-NAT"
|
||||||
|
iptablesRoutingForwardingChain = "NETBIRD-RT-FWD"
|
||||||
|
routingFinalForwardJump = "ACCEPT"
|
||||||
|
routingFinalNatJump = "MASQUERADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some presets for building nftable rules
|
||||||
|
var (
|
||||||
|
iptablesDefaultForwardingRule = []string{"-j", iptablesRoutingForwardingChain, "-m", "comment", "--comment"}
|
||||||
|
iptablesDefaultNetbirdForwardingRule = []string{"-j", "RETURN"}
|
||||||
|
iptablesDefaultNatRule = []string{"-j", iptablesRoutingNatChain, "-m", "comment", "--comment"}
|
||||||
|
iptablesDefaultNetbirdNatRule = []string{"-j", "RETURN"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type iptablesManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
ipv4Client *iptables.IPTables
|
||||||
|
ipv6Client *iptables.IPTables
|
||||||
|
rules map[string]map[string][]string
|
||||||
|
mux sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanRoutingRules cleans existing iptables resources that we created by the agent
|
||||||
|
func (i *iptablesManager) CleanRoutingRules() {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.cleanJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("flushing tables")
|
||||||
|
errMSGFormat := "iptables: failed cleaning %s chain %s,error: %v"
|
||||||
|
err = i.ipv4Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.ipv4Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.ipv6Client.ClearAndDeleteChain(iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.ipv6Client.ClearAndDeleteChain(iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("done cleaning up iptables rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreOrCreateContainers restores existing iptables containers (chains and rules)
|
||||||
|
// if they don't exist, we create them
|
||||||
|
func (i *iptablesManager) RestoreOrCreateContainers() error {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
if i.rules[ipv4][ipv4Forwarding] != nil && i.rules[ipv6][ipv6Forwarding] != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errMSGFormat := "iptables: failed creating %s chain %s,error: %v"
|
||||||
|
|
||||||
|
err := createChain(i.ipv4Client, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createChain(i.ipv4Client, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createChain(i.ipv6Client, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingForwardingChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = createChain(i.ipv6Client, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesRoutingNatChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.restoreRules(i.ipv4Client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while restoring ipv4 rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.restoreRules(i.ipv6Client)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while restoring ipv6 rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.addJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while creating jump rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addJumpRules create jump rules to send packets to NetBird chains
|
||||||
|
func (i *iptablesManager) addJumpRules() error {
|
||||||
|
err := i.cleanJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rule := append(iptablesDefaultForwardingRule, ipv4Forwarding)
|
||||||
|
err = i.ipv4Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.rules[ipv4][ipv4Forwarding] = rule
|
||||||
|
|
||||||
|
rule = append(iptablesDefaultNatRule, ipv4Nat)
|
||||||
|
err = i.ipv4Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[ipv4][ipv4Nat] = rule
|
||||||
|
|
||||||
|
rule = append(iptablesDefaultForwardingRule, ipv6Forwarding)
|
||||||
|
err = i.ipv6Client.Insert(iptablesFilterTable, iptablesForwardChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[ipv6][ipv6Forwarding] = rule
|
||||||
|
|
||||||
|
rule = append(iptablesDefaultNatRule, ipv6Nat)
|
||||||
|
err = i.ipv6Client.Insert(iptablesNatTable, iptablesPostRoutingChain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[ipv6][ipv6Nat] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanJumpRules cleans jump rules that was sending packets to NetBird chains
|
||||||
|
func (i *iptablesManager) cleanJumpRules() error {
|
||||||
|
var err error
|
||||||
|
errMSGFormat := "iptables: failed cleaning rule from %s chain %s,err: %v"
|
||||||
|
rule, found := i.rules[ipv4][ipv4Forwarding]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Forwarding)
|
||||||
|
err = i.ipv4Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesForwardChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule, found = i.rules[ipv4][ipv4Nat]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv4, ipv4Nat)
|
||||||
|
err = i.ipv4Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv4, iptablesPostRoutingChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule, found = i.rules[ipv6][ipv6Forwarding]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Forwarding)
|
||||||
|
err = i.ipv6Client.DeleteIfExists(iptablesFilterTable, iptablesForwardChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesForwardChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule, found = i.rules[ipv6][ipv6Nat]
|
||||||
|
if found {
|
||||||
|
log.Debugf("iptables: removing %s rule: %s ", ipv6, ipv6Nat)
|
||||||
|
err = i.ipv6Client.DeleteIfExists(iptablesNatTable, iptablesPostRoutingChain, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, ipv6, iptablesPostRoutingChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func iptablesProtoToString(proto iptables.Protocol) string {
|
||||||
|
if proto == iptables.ProtocolIPv6 {
|
||||||
|
return ipv6
|
||||||
|
}
|
||||||
|
return ipv4
|
||||||
|
}
|
||||||
|
|
||||||
|
// restoreRules restores existing NetBird rules
|
||||||
|
func (i *iptablesManager) restoreRules(iptablesClient *iptables.IPTables) error {
|
||||||
|
ipVersion := iptablesProtoToString(iptablesClient.Proto())
|
||||||
|
|
||||||
|
if i.rules[ipVersion] == nil {
|
||||||
|
i.rules[ipVersion] = make(map[string][]string)
|
||||||
|
}
|
||||||
|
table := iptablesFilterTable
|
||||||
|
for _, chain := range []string{iptablesForwardChain, iptablesRoutingForwardingChain} {
|
||||||
|
rules, err := iptablesClient.List(table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, ruleString := range rules {
|
||||||
|
rule := strings.Fields(ruleString)
|
||||||
|
id := getRuleRouteID(rule)
|
||||||
|
if id != "" {
|
||||||
|
i.rules[ipVersion][id] = rule[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table = iptablesNatTable
|
||||||
|
for _, chain := range []string{iptablesPostRoutingChain, iptablesRoutingNatChain} {
|
||||||
|
rules, err := iptablesClient.List(table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, ruleString := range rules {
|
||||||
|
rule := strings.Fields(ruleString)
|
||||||
|
id := getRuleRouteID(rule)
|
||||||
|
if id != "" {
|
||||||
|
i.rules[ipVersion][id] = rule[2:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createChain create NetBird chains
|
||||||
|
func createChain(iptables *iptables.IPTables, table, newChain string) error {
|
||||||
|
chains, err := iptables.ListChains(table)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't get %s %s table chains, error: %v", iptablesProtoToString(iptables.Proto()), table, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCreateChain := true
|
||||||
|
for _, chain := range chains {
|
||||||
|
if chain == newChain {
|
||||||
|
shouldCreateChain = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCreateChain {
|
||||||
|
err = iptables.NewChain(table, newChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create %s chain %s in %s table, error: %v", iptablesProtoToString(iptables.Proto()), newChain, table, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if table == iptablesNatTable {
|
||||||
|
err = iptables.Append(table, newChain, iptablesDefaultNetbirdNatRule...)
|
||||||
|
} else {
|
||||||
|
err = iptables.Append(table, newChain, iptablesDefaultNetbirdForwardingRule...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create %s chain %s default rule, error: %v", iptablesProtoToString(iptables.Proto()), newChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRuleSpec generates rule specification with comment identifier
|
||||||
|
func genRuleSpec(jump, id, source, destination string) []string {
|
||||||
|
return []string{"-s", source, "-d", destination, "-j", jump, "-m", "comment", "--comment", id}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRuleRouteID returns the rule ID if matches our prefix
|
||||||
|
func getRuleRouteID(rule []string) string {
|
||||||
|
for i, flag := range rule {
|
||||||
|
if flag == "--comment" {
|
||||||
|
id := rule[i+1]
|
||||||
|
if strings.HasPrefix(id, "netbird-") {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertRoutingRules inserts an iptables rule pair to the forwarding chain and if enabled, to the nat chain
|
||||||
|
func (i *iptablesManager) InsertRoutingRules(pair routerPair) error {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.insertRoutingRule(forwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, routingFinalForwardJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(inForwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, routingFinalForwardJump, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(natFormat, iptablesNatTable, iptablesRoutingNatChain, routingFinalNatJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(inNatFormat, iptablesNatTable, iptablesRoutingNatChain, routingFinalNatJump, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertRoutingRule inserts an iptable rule
|
||||||
|
func (i *iptablesManager) insertRoutingRule(keyFormat, table, chain, jump string, pair routerPair) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
|
ipVersion := ipv4
|
||||||
|
iptablesClient := i.ipv4Client
|
||||||
|
if prefix.Addr().Unmap().Is6() {
|
||||||
|
iptablesClient = i.ipv6Client
|
||||||
|
ipVersion = ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
|
rule := genRuleSpec(jump, ruleKey, pair.source, pair.destination)
|
||||||
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
|
if found {
|
||||||
|
err = iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
|
}
|
||||||
|
delete(i.rules[ipVersion], ruleKey)
|
||||||
|
}
|
||||||
|
err = iptablesClient.Insert(table, chain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while adding new %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.rules[ipVersion][ruleKey] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes an iptables rule pair from forwarding and nat chains
|
||||||
|
func (i *iptablesManager) RemoveRoutingRules(pair routerPair) error {
|
||||||
|
i.mux.Lock()
|
||||||
|
defer i.mux.Unlock()
|
||||||
|
|
||||||
|
err := i.removeRoutingRule(forwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(inForwardingFormat, iptablesFilterTable, iptablesRoutingForwardingChain, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(natFormat, iptablesNatTable, iptablesRoutingNatChain, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(inNatFormat, iptablesNatTable, iptablesRoutingNatChain, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeRoutingRule removes an iptables rule
|
||||||
|
func (i *iptablesManager) removeRoutingRule(keyFormat, table, chain string, pair routerPair) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
|
ipVersion := ipv4
|
||||||
|
iptablesClient := i.ipv4Client
|
||||||
|
if prefix.Addr().Unmap().Is6() {
|
||||||
|
iptablesClient = i.ipv6Client
|
||||||
|
ipVersion = ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleKey := genKey(keyFormat, pair.ID)
|
||||||
|
existingRule, found := i.rules[ipVersion][ruleKey]
|
||||||
|
if found {
|
||||||
|
err = iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("iptables: error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.destination, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(i.rules[ipVersion], ruleKey)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIptablesRuleType(table string) string {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if table == iptablesNatTable {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
return ruleType
|
||||||
|
}
|
||||||
300
client/internal/routemanager/iptables_linux_test.go
Normal file
300
client/internal/routemanager/iptables_linux_test.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
||||||
|
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
|
||||||
|
manager := &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules, 2, "should have created maps for ipv4 and ipv6")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv4], 2, "should have created minimal rules for ipv4")
|
||||||
|
|
||||||
|
exists, err := ipv4Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv4][ipv4Forwarding]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv4, iptablesFilterTable, iptablesForwardChain)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
exists, err = ipv4Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv4][ipv4Nat]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv4, iptablesNatTable, iptablesPostRoutingChain)
|
||||||
|
require.True(t, exists, "postrouting rule should exist")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv6], 2, "should have created minimal rules for ipv6")
|
||||||
|
|
||||||
|
exists, err = ipv6Client.Exists(iptablesFilterTable, iptablesForwardChain, manager.rules[ipv6][ipv6Forwarding]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv6, iptablesFilterTable, iptablesForwardChain)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
exists, err = ipv6Client.Exists(iptablesNatTable, iptablesPostRoutingChain, manager.rules[ipv6][ipv6Nat]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", ipv6, iptablesNatTable, iptablesPostRoutingChain)
|
||||||
|
require.True(t, exists, "postrouting rule should exist")
|
||||||
|
|
||||||
|
pair := routerPair{
|
||||||
|
ID: "abc",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.100.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
forward4RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
forward4Rule := genRuleSpec(routingFinalForwardJump, forward4RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv4Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward4Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
nat4RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
nat4Rule := genRuleSpec(routingFinalNatJump, nat4RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv4Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat4Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
pair = routerPair{
|
||||||
|
ID: "abc",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc11::/64",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
forward6RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
forward6Rule := genRuleSpec(routingFinalForwardJump, forward6RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv6Client.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forward6Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
nat6RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
nat6Rule := genRuleSpec(routingFinalNatJump, nat6RuleKey, pair.source, pair.destination)
|
||||||
|
|
||||||
|
err = ipv6Client.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, nat6Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
delete(manager.rules, ipv4)
|
||||||
|
delete(manager.rules, ipv6)
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv4], 4, "should have restored all rules for ipv4")
|
||||||
|
|
||||||
|
foundRule, found := manager.rules[ipv4][forward4RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
require.Equal(t, forward4Rule[:4], foundRule[:4], "stored forwarding rule should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[ipv4][nat4RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
require.Equal(t, nat4Rule[:4], foundRule[:4], "stored nat rule should match")
|
||||||
|
|
||||||
|
require.Len(t, manager.rules[ipv6], 4, "should have restored all rules for ipv6")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[ipv6][forward6RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
require.Equal(t, forward6Rule[:4], foundRule[:4], "stored forward rule should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[ipv6][nat6RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
require.Equal(t, nat6Rule[:4], foundRule[:4], "stored nat rule should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range insertRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
iptablesClient := ipv4Client
|
||||||
|
if testCase.ipVersion == ipv6 {
|
||||||
|
iptablesClient = ipv6Client
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.InsertRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "forwarding pair should be inserted")
|
||||||
|
|
||||||
|
forwardRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
forwardRule := genRuleSpec(routingFinalForwardJump, forwardRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
exists, err := iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, forwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
foundRule, found := manager.rules[testCase.ipVersion][forwardRuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the manager map")
|
||||||
|
require.Equal(t, forwardRule[:4], foundRule[:4], "stored forwarding rule should match")
|
||||||
|
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.True(t, exists, "income forwarding rule should exist")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[testCase.ipVersion][inForwardRuleKey]
|
||||||
|
require.True(t, found, "income forwarding rule should exist in the manager map")
|
||||||
|
require.Equal(t, inForwardRule[:4], foundRule[:4], "stored income forwarding rule should match")
|
||||||
|
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, natRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
require.True(t, exists, "nat rule should be created")
|
||||||
|
foundNatRule, foundNat := manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
|
require.True(t, foundNat, "nat rule should exist in the map")
|
||||||
|
require.Equal(t, natRule[:4], foundNatRule[:4], "stored nat rule should match")
|
||||||
|
} else {
|
||||||
|
require.False(t, exists, "nat rule should not be created")
|
||||||
|
_, foundNat := manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
|
require.False(t, foundNat, "nat rule should not exist in the map")
|
||||||
|
}
|
||||||
|
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
require.True(t, exists, "income nat rule should be created")
|
||||||
|
foundNatRule, foundNat := manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.True(t, foundNat, "income nat rule should exist in the map")
|
||||||
|
require.Equal(t, inNatRule[:4], foundNatRule[:4], "stored income nat rule should match")
|
||||||
|
} else {
|
||||||
|
require.False(t, exists, "nat rule should not be created")
|
||||||
|
_, foundNat := manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.False(t, foundNat, "income nat rule should not exist in the map")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptablesManager_RemoveRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range removeRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
ipv4Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
ipv6Client, _ := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||||
|
iptablesClient := ipv4Client
|
||||||
|
if testCase.ipVersion == ipv6 {
|
||||||
|
iptablesClient = ipv6Client
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := &iptablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
ipv4Client: ipv4Client,
|
||||||
|
ipv6Client: ipv6Client,
|
||||||
|
rules: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
forwardRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
forwardRule := genRuleSpec(routingFinalForwardJump, forwardRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, forwardRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, inForwardRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesFilterTable, iptablesRoutingForwardingChain, 1, inForwardRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
natRule := genRuleSpec(routingFinalNatJump, natRuleKey, testCase.inputPair.source, testCase.inputPair.destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, natRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, inNatRuleKey, getInPair(testCase.inputPair).source, getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(iptablesNatTable, iptablesRoutingNatChain, 1, inNatRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
delete(manager.rules, ipv4)
|
||||||
|
delete(manager.rules, ipv6)
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.RemoveRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
exists, err := iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, forwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.False(t, exists, "forwarding rule should not exist")
|
||||||
|
|
||||||
|
_, found := manager.rules[testCase.ipVersion][forwardRuleKey]
|
||||||
|
require.False(t, found, "forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesFilterTable, iptablesRoutingForwardingChain, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesFilterTable, iptablesRoutingForwardingChain)
|
||||||
|
require.False(t, exists, "income forwarding rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][inForwardRuleKey]
|
||||||
|
require.False(t, found, "income forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, natRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
require.False(t, exists, "nat rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][natRuleKey]
|
||||||
|
require.False(t, found, "nat rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(iptablesNatTable, iptablesRoutingNatChain, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s %s table and %s chain", testCase.ipVersion, iptablesNatTable, iptablesRoutingNatChain)
|
||||||
|
require.False(t, exists, "income nat rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[testCase.ipVersion][inNatRuleKey]
|
||||||
|
require.False(t, found, "income nat rule should exist in the manager map")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
181
client/internal/routemanager/manager.go
Normal file
181
client/internal/routemanager/manager.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager is a route manager interface
|
||||||
|
type Manager interface {
|
||||||
|
UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error
|
||||||
|
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
|
||||||
|
serverRoutes map[string]*route.Route
|
||||||
|
serverRouter *serverRouter
|
||||||
|
statusRecorder *status.Status
|
||||||
|
wgInterface *iface.WGIface
|
||||||
|
pubKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager returns a new route manager
|
||||||
|
func NewManager(ctx context.Context, pubKey string, wgInterface *iface.WGIface, statusRecorder *status.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)
|
||||||
|
|
||||||
|
for _, newRoute := range newRoutes {
|
||||||
|
// only linux is supported for now
|
||||||
|
if newRoute.Peer == m.pubKey {
|
||||||
|
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
|
||||||
|
} else {
|
||||||
|
// 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",
|
||||||
|
system.NetbirdVersion(), newRoute.Network)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
clientNetworkID := getClientNetworkID(newRoute)
|
||||||
|
newClientRoutesIDMap[clientNetworkID] = append(newClientRoutesIDMap[clientNetworkID], newRoute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.updateClientNetworks(updateSerial, newClientRoutesIDMap)
|
||||||
|
|
||||||
|
err := m.updateServerRoutes(newServerRoutesMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
370
client/internal/routemanager/manager_test.go
Normal file
370
client/internal/routemanager/manager_test.go
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// send 5 routes, one for server and 4 for clients, one normal and 2 HA and one small
|
||||||
|
// if linux host, should have one for server in map
|
||||||
|
// we should have 2 client manager
|
||||||
|
// 2 ranges in our routing table
|
||||||
|
|
||||||
|
const localPeerKey = "local"
|
||||||
|
const remotePeerKey1 = "remote1"
|
||||||
|
const remotePeerKey2 = "remote1"
|
||||||
|
|
||||||
|
func TestManagerUpdateRoutes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
inputInitRoutes []*route.Route
|
||||||
|
inputRoutes []*route.Route
|
||||||
|
inputSerial uint64
|
||||||
|
shouldCheckServerRoutes bool
|
||||||
|
serverRoutesExpected int
|
||||||
|
clientNetworkWatchersExpected int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should create 2 client networks",
|
||||||
|
inputInitRoutes: []*route.Route{},
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Create 2 Server Routes",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("100.64.252.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.9/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: runtime.GOOS == "linux",
|
||||||
|
serverRoutesExpected: 2,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Create 1 Route For Client And Server",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("100.64.30.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.9.9/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: runtime.GOOS == "linux",
|
||||||
|
serverRoutesExpected: 1,
|
||||||
|
clientNetworkWatchersExpected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Create 1 HA Route and 1 Standalone",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.20.0/24"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey2,
|
||||||
|
Network: netip.MustParsePrefix("8.8.20.0/24"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "c",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.9.9/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No Small Client Route Should Be Added",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No Server Routes Should Be Added To Non Linux",
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("1.2.3.4/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: runtime.GOOS != "linux",
|
||||||
|
serverRoutesExpected: 0,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove 1 Client Route",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update Route to HA",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey2,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove Client Routes",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{},
|
||||||
|
inputSerial: 1,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove All Routes",
|
||||||
|
inputInitRoutes: []*route.Route{
|
||||||
|
{
|
||||||
|
ID: "a",
|
||||||
|
NetID: "routeA",
|
||||||
|
Peer: localPeerKey,
|
||||||
|
Network: netip.MustParsePrefix("100.64.251.250/30"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ID: "b",
|
||||||
|
NetID: "routeB",
|
||||||
|
Peer: remotePeerKey1,
|
||||||
|
Network: netip.MustParsePrefix("8.8.8.8/32"),
|
||||||
|
NetworkType: route.IPv4Network,
|
||||||
|
Metric: 9999,
|
||||||
|
Masquerade: false,
|
||||||
|
Enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inputRoutes: []*route.Route{},
|
||||||
|
inputSerial: 1,
|
||||||
|
shouldCheckServerRoutes: true,
|
||||||
|
serverRoutesExpected: 0,
|
||||||
|
clientNetworkWatchersExpected: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun43%d", n), "100.65.65.2/24", iface.DefaultMTU)
|
||||||
|
require.NoError(t, err, "should create testing WGIface interface")
|
||||||
|
defer wgInterface.Close()
|
||||||
|
|
||||||
|
err = wgInterface.Create()
|
||||||
|
require.NoError(t, err, "should create testing wireguard interface")
|
||||||
|
|
||||||
|
statusRecorder := status.NewRecorder()
|
||||||
|
ctx := context.TODO()
|
||||||
|
routeManager := NewManager(ctx, localPeerKey, wgInterface, statusRecorder)
|
||||||
|
defer routeManager.Stop()
|
||||||
|
|
||||||
|
if len(testCase.inputInitRoutes) > 0 {
|
||||||
|
err = routeManager.UpdateRoutes(testCase.inputSerial, testCase.inputRoutes)
|
||||||
|
require.NoError(t, err, "should update routes with init routes")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = routeManager.UpdateRoutes(testCase.inputSerial+uint64(len(testCase.inputInitRoutes)), testCase.inputRoutes)
|
||||||
|
require.NoError(t, err, "should update routes")
|
||||||
|
|
||||||
|
require.Len(t, routeManager.clientNetworks, testCase.clientNetworkWatchersExpected, "client networks size should match")
|
||||||
|
|
||||||
|
if testCase.shouldCheckServerRoutes {
|
||||||
|
require.Len(t, routeManager.serverRoutes, testCase.serverRoutesExpected, "server networks size should match")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
27
client/internal/routemanager/mock.go
Normal file
27
client/internal/routemanager/mock.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/route"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockManager is the mock instance of a route manager
|
||||||
|
type MockManager struct {
|
||||||
|
UpdateRoutesFunc func(updateSerial uint64, newRoutes []*route.Route) error
|
||||||
|
StopFunc func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateRoutes mock implementation of UpdateRoutes from Manager interface
|
||||||
|
func (m *MockManager) UpdateRoutes(updateSerial uint64, newRoutes []*route.Route) error {
|
||||||
|
if m.UpdateRoutesFunc != nil {
|
||||||
|
return m.UpdateRoutesFunc(updateSerial, newRoutes)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("method UpdateRoutes is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop mock implementation of Stop from Manager interface
|
||||||
|
func (m *MockManager) Stop() {
|
||||||
|
if m.StopFunc != nil {
|
||||||
|
m.StopFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
432
client/internal/routemanager/nftables_linux.go
Normal file
432
client/internal/routemanager/nftables_linux.go
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"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"
|
||||||
|
nftablesRoutingForwardingChain = "netbird-rt-fwd"
|
||||||
|
nftablesRoutingNatChain = "netbird-rt-nat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// constants needed to create nftable rules
|
||||||
|
const (
|
||||||
|
ipv4Len = 4
|
||||||
|
ipv4SrcOffset = 12
|
||||||
|
ipv4DestOffset = 16
|
||||||
|
ipv6Len = 16
|
||||||
|
ipv6SrcOffset = 8
|
||||||
|
ipv6DestOffset = 24
|
||||||
|
exprDirectionSource = "source"
|
||||||
|
exprDirectionDestination = "destination"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some presets for building nftable rules
|
||||||
|
var (
|
||||||
|
zeroXor = binaryutil.NativeEndian.PutUint32(0)
|
||||||
|
|
||||||
|
zeroXor6 = append(binaryutil.NativeEndian.PutUint64(0), binaryutil.NativeEndian.PutUint64(0)...)
|
||||||
|
|
||||||
|
exprAllowRelatedEstablished = []expr.Any{
|
||||||
|
&expr.Ct{
|
||||||
|
Register: 1,
|
||||||
|
SourceRegister: false,
|
||||||
|
Key: 0,
|
||||||
|
},
|
||||||
|
&expr.Bitwise{
|
||||||
|
DestRegister: 1,
|
||||||
|
SourceRegister: 1,
|
||||||
|
Len: 4,
|
||||||
|
Mask: []uint8{0x6, 0x0, 0x0, 0x0},
|
||||||
|
Xor: zeroXor,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Register: 1,
|
||||||
|
Data: binaryutil.NativeEndian.PutUint32(0),
|
||||||
|
},
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exprCounterAccept = []expr.Any{
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type nftablesManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
conn *nftables.Conn
|
||||||
|
tableIPv4 *nftables.Table
|
||||||
|
tableIPv6 *nftables.Table
|
||||||
|
chains map[string]map[string]*nftables.Chain
|
||||||
|
rules map[string]*nftables.Rule
|
||||||
|
mux sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanRoutingRules cleans existing nftables rules from the system
|
||||||
|
func (n *nftablesManager) CleanRoutingRules() {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
log.Debug("flushing tables")
|
||||||
|
if n.tableIPv4 != nil && n.tableIPv6 != nil {
|
||||||
|
n.conn.FlushTable(n.tableIPv6)
|
||||||
|
n.conn.FlushTable(n.tableIPv4)
|
||||||
|
}
|
||||||
|
log.Debugf("flushing tables result in: %v error", n.conn.Flush())
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreOrCreateContainers restores existing nftables containers (tables and chains)
|
||||||
|
// if they don't exist, we create them
|
||||||
|
func (n *nftablesManager) RestoreOrCreateContainers() error {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
|
||||||
|
if n.tableIPv6 != nil && n.tableIPv4 != nil {
|
||||||
|
log.Debugf("nftables: containers already restored, skipping")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tables, err := n.conn.ListTables()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if table.Name == nftablesTable {
|
||||||
|
if table.Family == nftables.TableFamilyIPv4 {
|
||||||
|
n.tableIPv4 = table
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n.tableIPv6 = table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.tableIPv4 == nil {
|
||||||
|
n.tableIPv4 = n.conn.AddTable(&nftables.Table{
|
||||||
|
Name: nftablesTable,
|
||||||
|
Family: nftables.TableFamilyIPv4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.tableIPv6 == nil {
|
||||||
|
n.tableIPv6 = n.conn.AddTable(&nftables.Table{
|
||||||
|
Name: nftablesTable,
|
||||||
|
Family: nftables.TableFamilyIPv6,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
chains, err := n.conn.ListChains()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to list chains: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.chains[ipv4] = make(map[string]*nftables.Chain)
|
||||||
|
n.chains[ipv6] = make(map[string]*nftables.Chain)
|
||||||
|
|
||||||
|
for _, chain := range chains {
|
||||||
|
switch {
|
||||||
|
case chain.Table.Name == nftablesTable && chain.Table.Family == nftables.TableFamilyIPv4:
|
||||||
|
n.chains[ipv4][chain.Name] = chain
|
||||||
|
case chain.Table.Name == nftablesTable && chain.Table.Family == nftables.TableFamilyIPv6:
|
||||||
|
n.chains[ipv6][chain.Name] = chain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv4][nftablesRoutingForwardingChain]; !found {
|
||||||
|
n.chains[ipv4][nftablesRoutingForwardingChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingForwardingChain,
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Hooknum: nftables.ChainHookForward,
|
||||||
|
Priority: nftables.ChainPriorityNATDest + 1,
|
||||||
|
Type: nftables.ChainTypeFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv4][nftablesRoutingNatChain]; !found {
|
||||||
|
n.chains[ipv4][nftablesRoutingNatChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingNatChain,
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Hooknum: nftables.ChainHookPostrouting,
|
||||||
|
Priority: nftables.ChainPriorityNATSource - 1,
|
||||||
|
Type: nftables.ChainTypeNAT,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv6][nftablesRoutingForwardingChain]; !found {
|
||||||
|
n.chains[ipv6][nftablesRoutingForwardingChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingForwardingChain,
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Hooknum: nftables.ChainHookForward,
|
||||||
|
Priority: nftables.ChainPriorityNATDest + 1,
|
||||||
|
Type: nftables.ChainTypeFilter,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := n.chains[ipv6][nftablesRoutingNatChain]; !found {
|
||||||
|
n.chains[ipv6][nftablesRoutingNatChain] = n.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: nftablesRoutingNatChain,
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Hooknum: nftables.ChainHookPostrouting,
|
||||||
|
Priority: nftables.ChainPriorityNATSource - 1,
|
||||||
|
Type: nftables.ChainTypeNAT,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.checkOrCreateDefaultForwardingRules()
|
||||||
|
err = n.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to initialize table: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshRulesMap refreshes the rule map with the latest rules. this is useful to avoid
|
||||||
|
// duplicates and to get missing attributes that we don't have when adding new rules
|
||||||
|
func (n *nftablesManager) refreshRulesMap() error {
|
||||||
|
for _, registeredChains := range n.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := n.conn.GetRules(chain.Table, chain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to list rules: %v", err)
|
||||||
|
}
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 {
|
||||||
|
n.rules[string(rule.UserData)] = rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOrCreateDefaultForwardingRules checks if the default forwarding rules are enabled
|
||||||
|
func (n *nftablesManager) checkOrCreateDefaultForwardingRules() {
|
||||||
|
_, foundIPv4 := n.rules[ipv4Forwarding]
|
||||||
|
if !foundIPv4 {
|
||||||
|
n.rules[ipv4Forwarding] = n.conn.AddRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Chain: n.chains[ipv4][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: exprAllowRelatedEstablished,
|
||||||
|
UserData: []byte(ipv4Forwarding),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, foundIPv6 := n.rules[ipv6Forwarding]
|
||||||
|
if !foundIPv6 {
|
||||||
|
n.rules[ipv6Forwarding] = n.conn.AddRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Chain: n.chains[ipv6][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: exprAllowRelatedEstablished,
|
||||||
|
UserData: []byte(ipv6Forwarding),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertRoutingRules inserts a nftable rule pair to the forwarding chain and if enabled, to the nat chain
|
||||||
|
func (n *nftablesManager) InsertRoutingRules(pair routerPair) error {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
|
||||||
|
err := n.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.insertRoutingRule(forwardingFormat, nftablesRoutingForwardingChain, pair, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = n.insertRoutingRule(inForwardingFormat, nftablesRoutingForwardingChain, getInPair(pair), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.masquerade {
|
||||||
|
err = n.insertRoutingRule(natFormat, nftablesRoutingNatChain, pair, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = n.insertRoutingRule(inNatFormat, nftablesRoutingNatChain, getInPair(pair), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to insert rules for %s: %v", pair.destination, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertRoutingRule inserts a nftable rule to the conn client flush queue
|
||||||
|
func (n *nftablesManager) insertRoutingRule(format, chain string, pair routerPair, isNat bool) error {
|
||||||
|
|
||||||
|
prefix := netip.MustParsePrefix(pair.source)
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", pair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", pair.destination)
|
||||||
|
|
||||||
|
var expression []expr.Any
|
||||||
|
if isNat {
|
||||||
|
expression = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
} else {
|
||||||
|
expression = append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleKey := genKey(format, pair.ID)
|
||||||
|
|
||||||
|
_, exists := n.rules[ruleKey]
|
||||||
|
if exists {
|
||||||
|
err := n.removeRoutingRule(format, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix.Addr().Unmap().Is4() {
|
||||||
|
n.rules[ruleKey] = n.conn.InsertRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv4,
|
||||||
|
Chain: n.chains[ipv4][chain],
|
||||||
|
Exprs: expression,
|
||||||
|
UserData: []byte(ruleKey),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
n.rules[ruleKey] = n.conn.InsertRule(&nftables.Rule{
|
||||||
|
Table: n.tableIPv6,
|
||||||
|
Chain: n.chains[ipv6][chain],
|
||||||
|
Exprs: expression,
|
||||||
|
UserData: []byte(ruleKey),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes a nftable rule pair from forwarding and nat chains
|
||||||
|
func (n *nftablesManager) RemoveRoutingRules(pair routerPair) error {
|
||||||
|
n.mux.Lock()
|
||||||
|
defer n.mux.Unlock()
|
||||||
|
|
||||||
|
err := n.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(forwardingFormat, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(inForwardingFormat, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(natFormat, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.removeRoutingRule(inNatFormat, getInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = n.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: received error while applying rule removal for %s: %v", pair.destination, err)
|
||||||
|
}
|
||||||
|
log.Debugf("nftables: removed rules for %s", pair.destination)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeRoutingRule add a nftable rule to the removal queue and delete from rules map
|
||||||
|
func (n *nftablesManager) removeRoutingRule(format string, pair routerPair) error {
|
||||||
|
ruleKey := genKey(format, pair.ID)
|
||||||
|
|
||||||
|
rule, found := n.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if rule.Chain.Type == nftables.ChainTypeNAT {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := n.conn.DelRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to remove %s rule for %s: %v", ruleType, pair.destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("nftables: removing %s rule for %s", ruleType, pair.destination)
|
||||||
|
|
||||||
|
delete(n.rules, ruleKey)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPayloadDirectives get expression directives based on ip version and direction
|
||||||
|
func getPayloadDirectives(direction string, isIPv4 bool, isIPv6 bool) (uint32, uint32, []byte) {
|
||||||
|
switch {
|
||||||
|
case direction == exprDirectionSource && isIPv4:
|
||||||
|
return ipv4SrcOffset, ipv4Len, zeroXor
|
||||||
|
case direction == exprDirectionDestination && isIPv4:
|
||||||
|
return ipv4DestOffset, ipv4Len, zeroXor
|
||||||
|
case direction == exprDirectionSource && isIPv6:
|
||||||
|
return ipv6SrcOffset, ipv6Len, zeroXor6
|
||||||
|
case direction == exprDirectionDestination && isIPv6:
|
||||||
|
return ipv6DestOffset, ipv6Len, zeroXor6
|
||||||
|
default:
|
||||||
|
panic("no matched payload directive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCIDRMatcherExpressions generates nftables expressions that matches a CIDR
|
||||||
|
func generateCIDRMatcherExpressions(direction string, cidr string) []expr.Any {
|
||||||
|
ip, network, _ := net.ParseCIDR(cidr)
|
||||||
|
ipToAdd, _ := netip.AddrFromSlice(ip)
|
||||||
|
add := ipToAdd.Unmap()
|
||||||
|
|
||||||
|
offSet, packetLen, zeroXor := getPayloadDirectives(direction, add.Is4(), add.Is6())
|
||||||
|
|
||||||
|
return []expr.Any{
|
||||||
|
// fetch src add
|
||||||
|
&expr.Payload{
|
||||||
|
DestRegister: 1,
|
||||||
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
|
Offset: offSet,
|
||||||
|
Len: packetLen,
|
||||||
|
},
|
||||||
|
// net mask
|
||||||
|
&expr.Bitwise{
|
||||||
|
DestRegister: 1,
|
||||||
|
SourceRegister: 1,
|
||||||
|
Len: packetLen,
|
||||||
|
Mask: network.Mask,
|
||||||
|
Xor: zeroXor,
|
||||||
|
},
|
||||||
|
// net address
|
||||||
|
&expr.Cmp{
|
||||||
|
Register: 1,
|
||||||
|
Data: add.AsSlice(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
333
client/internal/routemanager/nftables_linux_test.go
Normal file
333
client/internal/routemanager/nftables_linux_test.go
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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) {
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.chains, 2, "should have created chains for ipv4 and ipv6")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv4")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv6")
|
||||||
|
require.Len(t, manager.rules, 2, "should have created rules for ipv4 and ipv6")
|
||||||
|
|
||||||
|
pair := routerPair{
|
||||||
|
ID: "abc",
|
||||||
|
source: "100.100.100.1/32",
|
||||||
|
destination: "100.100.100.0/24",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", pair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", pair.destination)
|
||||||
|
|
||||||
|
forward4Exp := append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
forward4RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
inserted4Forwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv4,
|
||||||
|
Chain: manager.chains[ipv4][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forward4Exp,
|
||||||
|
UserData: []byte(forward4RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
nat4Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
nat4RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
|
||||||
|
inserted4Nat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv4,
|
||||||
|
Chain: manager.chains[ipv4][nftablesRoutingNatChain],
|
||||||
|
Exprs: nat4Exp,
|
||||||
|
UserData: []byte(nat4RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
err = nftablesTestingClient.Flush()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
pair = routerPair{
|
||||||
|
ID: "xyz",
|
||||||
|
source: "fc00::1/128",
|
||||||
|
destination: "fc11::/64",
|
||||||
|
masquerade: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", pair.source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", pair.destination)
|
||||||
|
|
||||||
|
forward6Exp := append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
forward6RuleKey := genKey(forwardingFormat, pair.ID)
|
||||||
|
inserted6Forwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv6,
|
||||||
|
Chain: manager.chains[ipv6][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forward6Exp,
|
||||||
|
UserData: []byte(forward6RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
nat6Exp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
nat6RuleKey := genKey(natFormat, pair.ID)
|
||||||
|
|
||||||
|
inserted6Nat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.tableIPv6,
|
||||||
|
Chain: manager.chains[ipv6][nftablesRoutingNatChain],
|
||||||
|
Exprs: nat6Exp,
|
||||||
|
UserData: []byte(nat6RuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
err = nftablesTestingClient.Flush()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
manager.tableIPv4 = nil
|
||||||
|
manager.tableIPv6 = nil
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
require.Len(t, manager.chains, 2, "should have created chains for ipv4 and ipv6")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv4")
|
||||||
|
require.Len(t, manager.chains[ipv4], 2, "should have created chains for ipv6")
|
||||||
|
require.Len(t, manager.rules, 6, "should have restored all rules for ipv4 and ipv6")
|
||||||
|
|
||||||
|
foundRule, found := manager.rules[forward4RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
assert.Equal(t, inserted4Forwarding.Exprs, foundRule.Exprs, "stored forwarding rule expressions should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[nat4RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
// match len of output as nftables client doesn't return expressions with masquerade expression
|
||||||
|
assert.ElementsMatch(t, inserted4Nat.Exprs[:len(foundRule.Exprs)], foundRule.Exprs, "stored nat rule expressions should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[forward6RuleKey]
|
||||||
|
require.True(t, found, "forwarding rule should exist in the map")
|
||||||
|
assert.Equal(t, inserted6Forwarding.Exprs, foundRule.Exprs, "stored forward rule should match")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[nat6RuleKey]
|
||||||
|
require.True(t, found, "nat rule should exist in the map")
|
||||||
|
// match len of output as nftables client doesn't return expressions with masquerade expression
|
||||||
|
assert.ElementsMatch(t, inserted6Nat.Exprs[:len(foundRule.Exprs)], foundRule.Exprs, "stored nat rule should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
for _, testCase := range insertRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.InsertRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "forwarding pair should be inserted")
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", testCase.inputPair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", testCase.inputPair.destination)
|
||||||
|
testingExpression := append(sourceExp, destExp...)
|
||||||
|
fwdRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
found := 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == fwdRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "forwarding rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
found := 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == natRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "nat rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination)
|
||||||
|
testingExpression = append(sourceExp, destExp...)
|
||||||
|
inFwdRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == inFwdRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "income forwarding rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
|
||||||
|
if testCase.inputPair.masquerade {
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
found := 0
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 && string(rule.UserData) == inNatRuleKey {
|
||||||
|
require.ElementsMatchf(t, rule.Exprs[:len(testingExpression)], testingExpression, "income nat rule elements should match")
|
||||||
|
found = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.Equal(t, 1, found, "should find at least 1 rule to test")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNftablesManager_RemoveRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
for _, testCase := range removeRuleTestCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
|
|
||||||
|
manager := &nftablesManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
chains: make(map[string]map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.CleanRoutingRules()
|
||||||
|
|
||||||
|
err := manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
table := manager.tableIPv4
|
||||||
|
if testCase.ipVersion == ipv6 {
|
||||||
|
table = manager.tableIPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions("source", testCase.inputPair.source)
|
||||||
|
destExp := generateCIDRMatcherExpressions("destination", testCase.inputPair.destination)
|
||||||
|
|
||||||
|
forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
forwardRuleKey := genKey(forwardingFormat, testCase.inputPair.ID)
|
||||||
|
insertedForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forwardExp,
|
||||||
|
UserData: []byte(forwardRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
natExp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
natRuleKey := genKey(natFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingNatChain],
|
||||||
|
Exprs: natExp,
|
||||||
|
UserData: []byte(natRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions("source", getInPair(testCase.inputPair).source)
|
||||||
|
destExp = generateCIDRMatcherExpressions("destination", getInPair(testCase.inputPair).destination)
|
||||||
|
|
||||||
|
forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...)
|
||||||
|
inForwardRuleKey := genKey(inForwardingFormat, testCase.inputPair.ID)
|
||||||
|
insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingForwardingChain],
|
||||||
|
Exprs: forwardExp,
|
||||||
|
UserData: []byte(inForwardRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
natExp = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...)
|
||||||
|
inNatRuleKey := genKey(inNatFormat, testCase.inputPair.ID)
|
||||||
|
|
||||||
|
insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: table,
|
||||||
|
Chain: manager.chains[testCase.ipVersion][nftablesRoutingNatChain],
|
||||||
|
Exprs: natExp,
|
||||||
|
UserData: []byte(inNatRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
err = nftablesTestingClient.Flush()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
manager.tableIPv4 = nil
|
||||||
|
manager.tableIPv6 = nil
|
||||||
|
|
||||||
|
err = manager.RestoreOrCreateContainers()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.RemoveRoutingRules(testCase.inputPair)
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
for _, registeredChains := range manager.chains {
|
||||||
|
for _, chain := range registeredChains {
|
||||||
|
rules, err := nftablesTestingClient.GetRules(chain.Table, chain)
|
||||||
|
require.NoError(t, err, "should list rules for %s table and %s chain", chain.Table.Name, chain.Name)
|
||||||
|
for _, rule := range rules {
|
||||||
|
if len(rule.UserData) > 0 {
|
||||||
|
require.NotEqual(t, insertedForwarding.UserData, rule.UserData, "forwarding rule should not exist")
|
||||||
|
require.NotEqual(t, insertedNat.UserData, rule.UserData, "nat rule should not exist")
|
||||||
|
require.NotEqual(t, insertedInForwarding.UserData, rule.UserData, "income forwarding rule should not exist")
|
||||||
|
require.NotEqual(t, insertedInNat.UserData, rule.UserData, "income nat rule should not exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
67
client/internal/routemanager/server.go
Normal file
67
client/internal/routemanager/server.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
55
client/internal/routemanager/systemops.go
Normal file
55
client/internal/routemanager/systemops.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefixGateway != nil && !prefixGateway.Equal(gateway) {
|
||||||
|
log.Warnf("route for network %s already exist and is pointing to the gateway: %s, won't add another one", prefix, prefixGateway)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return addToRouteTable(prefix, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromRouteTableIfNonSystem(prefix netip.Prefix, addr string) error {
|
||||||
|
addrIP := net.ParseIP(addr)
|
||||||
|
prefixGateway, err := getExistingRIBRouteGateway(prefix)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if prefixGateway != nil && !prefixGateway.Equal(addrIP) {
|
||||||
|
log.Warnf("route for network %s is pointing to a different gateway: %s, should be pointing to: %s, not removing", prefix, prefixGateway, addrIP)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return removeFromRouteTable(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExistingRIBRouteGateway(prefix netip.Prefix) (net.IP, error) {
|
||||||
|
r, err := netroute.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, _, localGatewayAddress, err := r.Route(prefix.Addr().AsSlice())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getting routes returned an error: %v", err)
|
||||||
|
return nil, errRouteNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return localGatewayAddress, nil
|
||||||
|
}
|
||||||
73
client/internal/routemanager/systemops_linux.go
Normal file
73
client/internal/routemanager/systemops_linux.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ipv4ForwardingPath = "/proc/sys/net/ipv4/ip_forward"
|
||||||
|
|
||||||
|
func addToRouteTable(prefix netip.Prefix, addr string) error {
|
||||||
|
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrMask := "/32"
|
||||||
|
if prefix.Addr().Unmap().Is6() {
|
||||||
|
addrMask = "/128"
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, _, err := net.ParseCIDR(addr + addrMask)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
route := &netlink.Route{
|
||||||
|
Scope: netlink.SCOPE_UNIVERSE,
|
||||||
|
Dst: ipNet,
|
||||||
|
Gw: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netlink.RouteAdd(route)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromRouteTable(prefix netip.Prefix) error {
|
||||||
|
_, ipNet, err := net.ParseCIDR(prefix.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
route := &netlink.Route{
|
||||||
|
Scope: netlink.SCOPE_UNIVERSE,
|
||||||
|
Dst: ipNet,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = netlink.RouteDel(route)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableIPForwarding() error {
|
||||||
|
err := ioutil.WriteFile(ipv4ForwardingPath, []byte("1"), 0644)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetForwardHistoryEnabled() bool {
|
||||||
|
out, err := ioutil.ReadFile(ipv4ForwardingPath)
|
||||||
|
if err != nil {
|
||||||
|
// todo
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(out) == "1"
|
||||||
|
}
|
||||||
41
client/internal/routemanager/systemops_nonlinux.go
Normal file
41
client/internal/routemanager/systemops_nonlinux.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addToRouteTable(prefix netip.Prefix, addr string) error {
|
||||||
|
cmd := exec.Command("route", "add", prefix.String(), addr)
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf(string(out))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFromRouteTable(prefix netip.Prefix) error {
|
||||||
|
cmd := exec.Command("route", "delete", prefix.String())
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf(string(out))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableIPForwarding() error {
|
||||||
|
log.Infof("enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNetForwardHistoryEnabled() bool {
|
||||||
|
log.Infof("check netforwad history is not implemented on %s", runtime.GOOS)
|
||||||
|
return false
|
||||||
|
}
|
||||||
68
client/internal/routemanager/systemops_test.go
Normal file
68
client/internal/routemanager/systemops_test.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package routemanager
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddRemoveRoutes(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
prefix netip.Prefix
|
||||||
|
shouldRouteToWireguard bool
|
||||||
|
shouldBeRemoved bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Should Add And Remove Route",
|
||||||
|
prefix: netip.MustParsePrefix("100.66.120.0/24"),
|
||||||
|
shouldRouteToWireguard: true,
|
||||||
|
shouldBeRemoved: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Should Not Add Or Remove Route",
|
||||||
|
prefix: netip.MustParsePrefix("127.0.0.1/32"),
|
||||||
|
shouldRouteToWireguard: false,
|
||||||
|
shouldBeRemoved: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for n, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
wgInterface, err := iface.NewWGIFace(fmt.Sprintf("utun53%d", n), "100.65.75.2/24", iface.DefaultMTU)
|
||||||
|
require.NoError(t, err, "should create testing WGIface interface")
|
||||||
|
defer wgInterface.Close()
|
||||||
|
|
||||||
|
err = wgInterface.Create()
|
||||||
|
require.NoError(t, err, "should create testing wireguard interface")
|
||||||
|
|
||||||
|
err = addToRouteTableIfNoExists(testCase.prefix, wgInterface.GetAddress().IP.String())
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
|
||||||
|
prefixGateway, err := getExistingRIBRouteGateway(testCase.prefix)
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
if testCase.shouldRouteToWireguard {
|
||||||
|
require.Equal(t, wgInterface.GetAddress().IP.String(), prefixGateway.String(), "route should point to wireguard interface IP")
|
||||||
|
} else {
|
||||||
|
require.NotEqual(t, wgInterface.GetAddress().IP.String(), prefixGateway.String(), "route should point to a different interface")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = removeFromRouteTableIfNonSystem(testCase.prefix, wgInterface.GetAddress().IP.String())
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
|
||||||
|
prefixGateway, err = getExistingRIBRouteGateway(testCase.prefix)
|
||||||
|
require.NoError(t, err, "should not return err")
|
||||||
|
|
||||||
|
internetGateway, err := getExistingRIBRouteGateway(netip.MustParsePrefix("0.0.0.0/0"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if testCase.shouldBeRemoved {
|
||||||
|
require.Equal(t, internetGateway, prefixGateway, "route should be pointing to default internet gateway")
|
||||||
|
} else {
|
||||||
|
require.NotEqual(t, internetGateway, prefixGateway, "route should be pointing to a different gateway than the internet gateway")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
69
client/internal/state.go
Normal file
69
client/internal/state.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusIdle StatusType = "Idle"
|
||||||
|
|
||||||
|
StatusConnecting StatusType = "Connecting"
|
||||||
|
StatusConnected StatusType = "Connected"
|
||||||
|
StatusNeedsLogin StatusType = "NeedsLogin"
|
||||||
|
StatusLoginFailed StatusType = "LoginFailed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CtxInitState setup context state into the context tree.
|
||||||
|
//
|
||||||
|
// This function should be used to initialize context before
|
||||||
|
// CtxGetState will be executed.
|
||||||
|
func CtxInitState(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, stateCtx, &contextState{
|
||||||
|
status: StatusIdle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CtxGetState object to get/update state/errors of process.
|
||||||
|
func CtxGetState(ctx context.Context) *contextState {
|
||||||
|
return ctx.Value(stateCtx).(*contextState)
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextState struct {
|
||||||
|
err error
|
||||||
|
status StatusType
|
||||||
|
mutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *contextState) Set(update StatusType) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.status = update
|
||||||
|
c.err = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *contextState) Status() (StatusType, error) {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
if c.err != nil {
|
||||||
|
return "", c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *contextState) Wrap(err error) error {
|
||||||
|
c.mutex.Lock()
|
||||||
|
defer c.mutex.Unlock()
|
||||||
|
|
||||||
|
c.err = err
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type stateKey int
|
||||||
|
|
||||||
|
var stateCtx stateKey
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/wiretrustee/wiretrustee/cmd"
|
"github.com/netbirdio/netbird/client/cmd"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
17
client/manifest.xml
Normal file
17
client/manifest.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<assemblyIdentity
|
||||||
|
version="0.0.0.1"
|
||||||
|
processorArchitecture="*"
|
||||||
|
name="netbird.exe"
|
||||||
|
type="win32"
|
||||||
|
/>
|
||||||
|
<description>Netbird application</description>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges>
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
</assembly>
|
||||||
1421
client/proto/daemon.pb.go
Normal file
1421
client/proto/daemon.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
134
client/proto/daemon.proto
Normal file
134
client/proto/daemon.proto
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
import "google/protobuf/descriptor.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
option go_package = "/proto";
|
||||||
|
|
||||||
|
package daemon;
|
||||||
|
|
||||||
|
service DaemonService {
|
||||||
|
// Login uses setup key to prepare configuration for the daemon.
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse) {}
|
||||||
|
|
||||||
|
// WaitSSOLogin uses the userCode to validate the TokenInfo and
|
||||||
|
// waits for the user to continue with the login on a browser
|
||||||
|
rpc WaitSSOLogin(WaitSSOLoginRequest) returns (WaitSSOLoginResponse) {}
|
||||||
|
|
||||||
|
// Up starts engine work in the daemon.
|
||||||
|
rpc Up(UpRequest) returns (UpResponse) {}
|
||||||
|
|
||||||
|
// Status of the service.
|
||||||
|
rpc Status(StatusRequest) returns (StatusResponse) {}
|
||||||
|
|
||||||
|
// Down engine work in the daemon.
|
||||||
|
rpc Down(DownRequest) returns (DownResponse) {}
|
||||||
|
|
||||||
|
// GetConfig of the daemon.
|
||||||
|
rpc GetConfig(GetConfigRequest) returns (GetConfigResponse) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
// setupKey wiretrustee setup key.
|
||||||
|
string setupKey = 1;
|
||||||
|
|
||||||
|
// preSharedKey for wireguard setup.
|
||||||
|
string preSharedKey = 2;
|
||||||
|
|
||||||
|
// managementUrl to authenticate.
|
||||||
|
string managementUrl = 3;
|
||||||
|
|
||||||
|
// adminUrl to manage keys.
|
||||||
|
string adminURL = 4;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
bool needsSSOLogin = 1;
|
||||||
|
string userCode = 2;
|
||||||
|
string verificationURI = 3;
|
||||||
|
string verificationURIComplete = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WaitSSOLoginRequest {
|
||||||
|
string userCode = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WaitSSOLoginResponse {}
|
||||||
|
|
||||||
|
message UpRequest {}
|
||||||
|
|
||||||
|
message UpResponse {}
|
||||||
|
|
||||||
|
message StatusRequest{
|
||||||
|
bool getFullPeerStatus = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message StatusResponse{
|
||||||
|
// status of the server.
|
||||||
|
string status = 1;
|
||||||
|
FullStatus fullStatus = 2;
|
||||||
|
// NetBird daemon version
|
||||||
|
string daemonVersion = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DownRequest {}
|
||||||
|
|
||||||
|
message DownResponse {}
|
||||||
|
|
||||||
|
message GetConfigRequest {}
|
||||||
|
|
||||||
|
message GetConfigResponse {
|
||||||
|
// managementUrl settings value.
|
||||||
|
string managementUrl = 1;
|
||||||
|
|
||||||
|
// configFile settings value.
|
||||||
|
string configFile = 2;
|
||||||
|
|
||||||
|
// logFile settings value.
|
||||||
|
string logFile = 3;
|
||||||
|
|
||||||
|
// preSharedKey settings value.
|
||||||
|
string preSharedKey = 4;
|
||||||
|
|
||||||
|
// adminURL settings value.
|
||||||
|
string adminURL = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerState contains the latest state of a peer
|
||||||
|
message PeerState {
|
||||||
|
string IP = 1;
|
||||||
|
string pubKey = 2;
|
||||||
|
string connStatus = 3;
|
||||||
|
google.protobuf.Timestamp connStatusUpdate = 4;
|
||||||
|
bool relayed = 5;
|
||||||
|
bool direct = 6;
|
||||||
|
string localIceCandidateType = 7;
|
||||||
|
string remoteIceCandidateType =8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPeerState contains the latest state of the local peer
|
||||||
|
message LocalPeerState {
|
||||||
|
string IP = 1;
|
||||||
|
string pubKey = 2;
|
||||||
|
bool kernelInterface =3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalState contains the latest state of a signal connection
|
||||||
|
message SignalState {
|
||||||
|
string URL = 1;
|
||||||
|
bool connected = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagementState contains the latest state of a management connection
|
||||||
|
message ManagementState {
|
||||||
|
string URL = 1;
|
||||||
|
bool connected = 2;
|
||||||
|
}
|
||||||
|
// FullStatus contains the full state held by the Status instance
|
||||||
|
message FullStatus {
|
||||||
|
ManagementState managementState = 1;
|
||||||
|
SignalState signalState = 2;
|
||||||
|
LocalPeerState localPeerState = 3;
|
||||||
|
repeated PeerState peers = 4;
|
||||||
|
}
|
||||||
295
client/proto/daemon_grpc.pb.go
Normal file
295
client/proto/daemon_grpc.pb.go
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
// DaemonServiceClient is the client API for DaemonService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type DaemonServiceClient interface {
|
||||||
|
// Login uses setup key to prepare configuration for the daemon.
|
||||||
|
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
|
||||||
|
// WaitSSOLogin uses the userCode to validate the TokenInfo and
|
||||||
|
// waits for the user to continue with the login on a browser
|
||||||
|
WaitSSOLogin(ctx context.Context, in *WaitSSOLoginRequest, opts ...grpc.CallOption) (*WaitSSOLoginResponse, error)
|
||||||
|
// Up starts engine work in the daemon.
|
||||||
|
Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error)
|
||||||
|
// Status of the service.
|
||||||
|
Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
|
||||||
|
// Down engine work in the daemon.
|
||||||
|
Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error)
|
||||||
|
// GetConfig of the daemon.
|
||||||
|
GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type daemonServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDaemonServiceClient(cc grpc.ClientConnInterface) DaemonServiceClient {
|
||||||
|
return &daemonServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
|
||||||
|
out := new(LoginResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Login", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) WaitSSOLogin(ctx context.Context, in *WaitSSOLoginRequest, opts ...grpc.CallOption) (*WaitSSOLoginResponse, error) {
|
||||||
|
out := new(WaitSSOLoginResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/WaitSSOLogin", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) Up(ctx context.Context, in *UpRequest, opts ...grpc.CallOption) (*UpResponse, error) {
|
||||||
|
out := new(UpResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Up", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error) {
|
||||||
|
out := new(StatusResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Status", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) Down(ctx context.Context, in *DownRequest, opts ...grpc.CallOption) (*DownResponse, error) {
|
||||||
|
out := new(DownResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/Down", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *daemonServiceClient) GetConfig(ctx context.Context, in *GetConfigRequest, opts ...grpc.CallOption) (*GetConfigResponse, error) {
|
||||||
|
out := new(GetConfigResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/daemon.DaemonService/GetConfig", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaemonServiceServer is the server API for DaemonService service.
|
||||||
|
// All implementations must embed UnimplementedDaemonServiceServer
|
||||||
|
// for forward compatibility
|
||||||
|
type DaemonServiceServer interface {
|
||||||
|
// Login uses setup key to prepare configuration for the daemon.
|
||||||
|
Login(context.Context, *LoginRequest) (*LoginResponse, error)
|
||||||
|
// WaitSSOLogin uses the userCode to validate the TokenInfo and
|
||||||
|
// waits for the user to continue with the login on a browser
|
||||||
|
WaitSSOLogin(context.Context, *WaitSSOLoginRequest) (*WaitSSOLoginResponse, error)
|
||||||
|
// Up starts engine work in the daemon.
|
||||||
|
Up(context.Context, *UpRequest) (*UpResponse, error)
|
||||||
|
// Status of the service.
|
||||||
|
Status(context.Context, *StatusRequest) (*StatusResponse, error)
|
||||||
|
// Down engine work in the daemon.
|
||||||
|
Down(context.Context, *DownRequest) (*DownResponse, error)
|
||||||
|
// GetConfig of the daemon.
|
||||||
|
GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error)
|
||||||
|
mustEmbedUnimplementedDaemonServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedDaemonServiceServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedDaemonServiceServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedDaemonServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) WaitSSOLogin(context.Context, *WaitSSOLoginRequest) (*WaitSSOLoginResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method WaitSSOLogin not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) Up(context.Context, *UpRequest) (*UpResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Up not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) Status(context.Context, *StatusRequest) (*StatusResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Status not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) Down(context.Context, *DownRequest) (*DownResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Down not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) GetConfig(context.Context, *GetConfigRequest) (*GetConfigResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method GetConfig not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedDaemonServiceServer) mustEmbedUnimplementedDaemonServiceServer() {}
|
||||||
|
|
||||||
|
// UnsafeDaemonServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to DaemonServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeDaemonServiceServer interface {
|
||||||
|
mustEmbedUnimplementedDaemonServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterDaemonServiceServer(s grpc.ServiceRegistrar, srv DaemonServiceServer) {
|
||||||
|
s.RegisterService(&DaemonService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(LoginRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).Login(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/Login",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).Login(ctx, req.(*LoginRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_WaitSSOLogin_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(WaitSSOLoginRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).WaitSSOLogin(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/WaitSSOLogin",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).WaitSSOLogin(ctx, req.(*WaitSSOLoginRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_Up_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(UpRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).Up(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/Up",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).Up(ctx, req.(*UpRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(StatusRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).Status(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/Status",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).Status(ctx, req.(*StatusRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_Down_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(DownRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).Down(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/Down",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).Down(ctx, req.(*DownRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _DaemonService_GetConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(GetConfigRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(DaemonServiceServer).GetConfig(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/daemon.DaemonService/GetConfig",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(DaemonServiceServer).GetConfig(ctx, req.(*GetConfigRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaemonService_ServiceDesc is the grpc.ServiceDesc for DaemonService service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var DaemonService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "daemon.DaemonService",
|
||||||
|
HandlerType: (*DaemonServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "Login",
|
||||||
|
Handler: _DaemonService_Login_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "WaitSSOLogin",
|
||||||
|
Handler: _DaemonService_WaitSSOLogin_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Up",
|
||||||
|
Handler: _DaemonService_Up_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Status",
|
||||||
|
Handler: _DaemonService_Status_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "Down",
|
||||||
|
Handler: _DaemonService_Down_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "GetConfig",
|
||||||
|
Handler: _DaemonService_GetConfig_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "daemon.proto",
|
||||||
|
}
|
||||||
4
client/proto/generate.sh
Executable file
4
client/proto/generate.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.26
|
||||||
|
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.1
|
||||||
|
protoc -I proto/ proto/daemon.proto --go_out=. --go-grpc_out=.
|
||||||
9
client/resources.rc
Normal file
9
client/resources.rc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#pragma code_page(65001) // UTF-8
|
||||||
|
|
||||||
|
#define STRINGIZE(x) #x
|
||||||
|
#define EXPAND(x) STRINGIZE(x)
|
||||||
|
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml
|
||||||
|
7 ICON ui/netbird.ico
|
||||||
|
wireguard.dll RCDATA wireguard.dll
|
||||||
493
client/server/server.go
Normal file
493
client/server/server.go
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
nbStatus "github.com/netbirdio/netbird/client/status"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Server for service control.
|
||||||
|
type Server struct {
|
||||||
|
rootCtx context.Context
|
||||||
|
actCancel context.CancelFunc
|
||||||
|
|
||||||
|
managementURL string
|
||||||
|
adminURL string
|
||||||
|
configPath string
|
||||||
|
logFile string
|
||||||
|
|
||||||
|
oauthAuthFlow oauthAuthFlow
|
||||||
|
|
||||||
|
mutex sync.Mutex
|
||||||
|
config *internal.Config
|
||||||
|
proto.UnimplementedDaemonServiceServer
|
||||||
|
|
||||||
|
statusRecorder *nbStatus.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
type oauthAuthFlow struct {
|
||||||
|
expiresAt time.Time
|
||||||
|
client internal.OAuthClient
|
||||||
|
info internal.DeviceAuthInfo
|
||||||
|
waitCancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// New server instance constructor.
|
||||||
|
func New(ctx context.Context, managementURL, adminURL, configPath, logFile string) *Server {
|
||||||
|
return &Server{
|
||||||
|
rootCtx: ctx,
|
||||||
|
managementURL: managementURL,
|
||||||
|
adminURL: adminURL,
|
||||||
|
configPath: configPath,
|
||||||
|
logFile: logFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
state := internal.CtxGetState(s.rootCtx)
|
||||||
|
|
||||||
|
// if current state contains any error, return it
|
||||||
|
// in all other cases we can continue execution only if status is idle and up command was
|
||||||
|
// not in the progress or already successfully established connection.
|
||||||
|
status, err := state.Status()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status != internal.StatusIdle {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(s.rootCtx)
|
||||||
|
s.actCancel = cancel
|
||||||
|
|
||||||
|
// if configuration exists, we just start connections. if is new config we skip and set status NeedsLogin
|
||||||
|
// on failure we return error to retry
|
||||||
|
config, err := internal.ReadConfig(s.managementURL, s.adminURL, s.configPath, nil)
|
||||||
|
if errorStatus, ok := gstatus.FromError(err); ok && errorStatus.Code() == codes.NotFound {
|
||||||
|
config, err = internal.GetConfig(s.managementURL, s.adminURL, s.configPath, "")
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to create configuration file: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
state.Set(internal.StatusNeedsLogin)
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
log.Warnf("unable to create configuration file: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if configuration exists, we just start connections.
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, s.configPath)
|
||||||
|
|
||||||
|
s.config = config
|
||||||
|
|
||||||
|
if s.statusRecorder == nil {
|
||||||
|
s.statusRecorder = nbStatus.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := internal.RunClient(ctx, config, s.statusRecorder); err != nil {
|
||||||
|
log.Errorf("init connections: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loginAttempt attempts to login using the provided information. it returns a status in case something fails
|
||||||
|
func (s *Server) loginAttempt(ctx context.Context, setupKey, jwtToken string) (internal.StatusType, error) {
|
||||||
|
var status internal.StatusType
|
||||||
|
err := internal.Login(ctx, s.config, setupKey, jwtToken)
|
||||||
|
if err != nil {
|
||||||
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||||
|
log.Warnf("failed login: %v", err)
|
||||||
|
status = internal.StatusNeedsLogin
|
||||||
|
} else {
|
||||||
|
log.Errorf("failed login: %v", err)
|
||||||
|
status = internal.StatusLoginFailed
|
||||||
|
}
|
||||||
|
return status, err
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login uses setup key to prepare configuration for the daemon.
|
||||||
|
func (s *Server) Login(callerCtx context.Context, msg *proto.LoginRequest) (*proto.LoginResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
if s.actCancel != nil {
|
||||||
|
s.actCancel()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(s.rootCtx)
|
||||||
|
|
||||||
|
md, ok := metadata.FromIncomingContext(callerCtx)
|
||||||
|
if ok {
|
||||||
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.actCancel = cancel
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
state := internal.CtxGetState(ctx)
|
||||||
|
defer func() {
|
||||||
|
status, err := state.Status()
|
||||||
|
if err != nil || (status != internal.StatusNeedsLogin && status != internal.StatusLoginFailed) {
|
||||||
|
state.Set(internal.StatusIdle)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
managementURL := s.managementURL
|
||||||
|
if msg.ManagementUrl != "" {
|
||||||
|
managementURL = msg.ManagementUrl
|
||||||
|
s.managementURL = msg.ManagementUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
adminURL := s.adminURL
|
||||||
|
if msg.AdminURL != "" {
|
||||||
|
adminURL = msg.AdminURL
|
||||||
|
s.adminURL = msg.AdminURL
|
||||||
|
}
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
config, err := internal.GetConfig(managementURL, adminURL, s.configPath, msg.PreSharedKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.ManagementUrl == "" {
|
||||||
|
config, _ = internal.UpdateOldManagementPort(ctx, config, s.configPath)
|
||||||
|
s.config = config
|
||||||
|
s.managementURL = config.ManagementURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.config = config
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
if _, err := s.loginAttempt(ctx, "", ""); err == nil {
|
||||||
|
state.Set(internal.StatusIdle)
|
||||||
|
return &proto.LoginResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Set(internal.StatusConnecting)
|
||||||
|
|
||||||
|
if msg.SetupKey == "" {
|
||||||
|
providerConfig, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
state.Set(internal.StatusLoginFailed)
|
||||||
|
s, ok := gstatus.FromError(err)
|
||||||
|
if ok && s.Code() == codes.NotFound {
|
||||||
|
return nil, gstatus.Errorf(codes.NotFound, "no SSO provider returned from management. "+
|
||||||
|
"If you are using hosting Netbird see documentation at "+
|
||||||
|
"https://github.com/netbirdio/netbird/tree/main/management for details")
|
||||||
|
} else if ok && s.Code() == codes.Unimplemented {
|
||||||
|
return nil, gstatus.Errorf(codes.Unimplemented, "the management server, %s, does not support SSO providers, "+
|
||||||
|
"please update your server or use Setup Keys to login", config.ManagementURL)
|
||||||
|
} else {
|
||||||
|
log.Errorf("getting device authorization flow info failed with error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostedClient := internal.NewHostedDeviceFlow(
|
||||||
|
providerConfig.ProviderConfig.Audience,
|
||||||
|
providerConfig.ProviderConfig.ClientID,
|
||||||
|
providerConfig.ProviderConfig.TokenEndpoint,
|
||||||
|
providerConfig.ProviderConfig.DeviceAuthEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
if s.oauthAuthFlow.client != nil && s.oauthAuthFlow.client.GetClientID(ctx) == hostedClient.GetClientID(context.TODO()) {
|
||||||
|
if s.oauthAuthFlow.expiresAt.After(time.Now().Add(90 * time.Second)) {
|
||||||
|
log.Debugf("using previous device flow info")
|
||||||
|
return &proto.LoginResponse{
|
||||||
|
NeedsSSOLogin: true,
|
||||||
|
VerificationURI: s.oauthAuthFlow.info.VerificationURI,
|
||||||
|
VerificationURIComplete: s.oauthAuthFlow.info.VerificationURIComplete,
|
||||||
|
UserCode: s.oauthAuthFlow.info.UserCode,
|
||||||
|
}, nil
|
||||||
|
} else {
|
||||||
|
log.Warnf("canceling previous waiting execution")
|
||||||
|
s.oauthAuthFlow.waitCancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceAuthInfo, err := hostedClient.RequestDeviceCode(context.TODO())
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("getting a request device code failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.oauthAuthFlow.client = hostedClient
|
||||||
|
s.oauthAuthFlow.info = deviceAuthInfo
|
||||||
|
s.oauthAuthFlow.expiresAt = time.Now().Add(time.Duration(deviceAuthInfo.ExpiresIn) * time.Second)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
state.Set(internal.StatusNeedsLogin)
|
||||||
|
|
||||||
|
return &proto.LoginResponse{
|
||||||
|
NeedsSSOLogin: true,
|
||||||
|
VerificationURI: deviceAuthInfo.VerificationURI,
|
||||||
|
VerificationURIComplete: deviceAuthInfo.VerificationURIComplete,
|
||||||
|
UserCode: deviceAuthInfo.UserCode,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginStatus, err := s.loginAttempt(ctx, msg.SetupKey, ""); err != nil {
|
||||||
|
state.Set(loginStatus)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.LoginResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitSSOLogin uses the userCode to validate the TokenInfo and
|
||||||
|
// waits for the user to continue with the login on a browser
|
||||||
|
func (s *Server) WaitSSOLogin(callerCtx context.Context, msg *proto.WaitSSOLoginRequest) (*proto.WaitSSOLoginResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
if s.actCancel != nil {
|
||||||
|
s.actCancel()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(s.rootCtx)
|
||||||
|
|
||||||
|
md, ok := metadata.FromIncomingContext(callerCtx)
|
||||||
|
if ok {
|
||||||
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.actCancel = cancel
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
if s.oauthAuthFlow.client == nil {
|
||||||
|
return nil, gstatus.Errorf(codes.Internal, "oauth client is not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
state := internal.CtxGetState(ctx)
|
||||||
|
defer func() {
|
||||||
|
s, err := state.Status()
|
||||||
|
if err != nil || (s != internal.StatusNeedsLogin && s != internal.StatusLoginFailed) {
|
||||||
|
state.Set(internal.StatusIdle)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
state.Set(internal.StatusConnecting)
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
deviceAuthInfo := s.oauthAuthFlow.info
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
if deviceAuthInfo.UserCode != msg.UserCode {
|
||||||
|
state.Set(internal.StatusLoginFailed)
|
||||||
|
return nil, gstatus.Errorf(codes.InvalidArgument, "sso user code is invalid")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.oauthAuthFlow.waitCancel != nil {
|
||||||
|
s.oauthAuthFlow.waitCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitTimeout := time.Until(s.oauthAuthFlow.expiresAt)
|
||||||
|
waitCTX, cancel := context.WithTimeout(ctx, waitTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.oauthAuthFlow.waitCancel = cancel
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
tokenInfo, err := s.oauthAuthFlow.client.WaitToken(waitCTX, deviceAuthInfo)
|
||||||
|
if err != nil {
|
||||||
|
if err == context.Canceled {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.oauthAuthFlow.expiresAt = time.Now()
|
||||||
|
s.mutex.Unlock()
|
||||||
|
state.Set(internal.StatusLoginFailed)
|
||||||
|
log.Errorf("waiting for browser login failed: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
s.oauthAuthFlow.expiresAt = time.Now()
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
if loginStatus, err := s.loginAttempt(ctx, "", tokenInfo.AccessToken); err != nil {
|
||||||
|
state.Set(loginStatus)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.WaitSSOLoginResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Up starts engine work in the daemon.
|
||||||
|
func (s *Server) Up(callerCtx context.Context, msg *proto.UpRequest) (*proto.UpResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
state := internal.CtxGetState(s.rootCtx)
|
||||||
|
|
||||||
|
// if current state contains any error, return it
|
||||||
|
// in all other cases we can continue execution only if status is idle and up command was
|
||||||
|
// not in the progress or already successfully established connection.
|
||||||
|
status, err := state.Status()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if status != internal.StatusIdle {
|
||||||
|
return nil, fmt.Errorf("up already in progress: current status %s", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// it should be nil here, but .
|
||||||
|
if s.actCancel != nil {
|
||||||
|
s.actCancel()
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithCancel(s.rootCtx)
|
||||||
|
|
||||||
|
md, ok := metadata.FromIncomingContext(callerCtx)
|
||||||
|
if ok {
|
||||||
|
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.actCancel = cancel
|
||||||
|
|
||||||
|
if s.config == nil {
|
||||||
|
return nil, fmt.Errorf("config is not defined, please call login command first")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.statusRecorder == nil {
|
||||||
|
s.statusRecorder = nbStatus.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := internal.RunClient(ctx, s.config, s.statusRecorder); err != nil {
|
||||||
|
log.Errorf("run client connection: %v", state.Wrap(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return &proto.UpResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Down engine work in the daemon.
|
||||||
|
func (s *Server) Down(ctx context.Context, msg *proto.DownRequest) (*proto.DownResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if s.actCancel == nil {
|
||||||
|
return nil, fmt.Errorf("service is not up")
|
||||||
|
}
|
||||||
|
s.actCancel()
|
||||||
|
|
||||||
|
return &proto.DownResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status starts engine work in the daemon.
|
||||||
|
func (s *Server) Status(
|
||||||
|
_ context.Context,
|
||||||
|
msg *proto.StatusRequest,
|
||||||
|
) (*proto.StatusResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
status, err := internal.CtxGetState(s.rootCtx).Status()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statusResponse := proto.StatusResponse{Status: string(status), DaemonVersion: system.NetbirdVersion()}
|
||||||
|
|
||||||
|
if s.statusRecorder == nil {
|
||||||
|
s.statusRecorder = nbStatus.NewRecorder()
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.GetFullPeerStatus {
|
||||||
|
fullStatus := s.statusRecorder.GetFullStatus()
|
||||||
|
pbFullStatus := toProtoFullStatus(fullStatus)
|
||||||
|
statusResponse.FullStatus = pbFullStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
return &statusResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig of the daemon.
|
||||||
|
func (s *Server) GetConfig(ctx context.Context, msg *proto.GetConfigRequest) (*proto.GetConfigResponse, error) {
|
||||||
|
s.mutex.Lock()
|
||||||
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
managementURL := s.managementURL
|
||||||
|
adminURL := s.adminURL
|
||||||
|
preSharedKey := ""
|
||||||
|
|
||||||
|
if s.config != nil {
|
||||||
|
if managementURL == "" && s.config.ManagementURL != nil {
|
||||||
|
managementURL = s.config.ManagementURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.config.AdminURL != nil {
|
||||||
|
adminURL = s.config.AdminURL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
preSharedKey = s.config.PreSharedKey
|
||||||
|
if preSharedKey != "" {
|
||||||
|
preSharedKey = "**********"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return &proto.GetConfigResponse{
|
||||||
|
ManagementUrl: managementURL,
|
||||||
|
AdminURL: adminURL,
|
||||||
|
ConfigFile: s.configPath,
|
||||||
|
LogFile: s.logFile,
|
||||||
|
PreSharedKey: preSharedKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProtoFullStatus(fullStatus nbStatus.FullStatus) *proto.FullStatus {
|
||||||
|
pbFullStatus := proto.FullStatus{
|
||||||
|
ManagementState: &proto.ManagementState{},
|
||||||
|
SignalState: &proto.SignalState{},
|
||||||
|
LocalPeerState: &proto.LocalPeerState{},
|
||||||
|
Peers: []*proto.PeerState{},
|
||||||
|
}
|
||||||
|
|
||||||
|
pbFullStatus.ManagementState.URL = fullStatus.ManagementState.URL
|
||||||
|
pbFullStatus.ManagementState.Connected = fullStatus.ManagementState.Connected
|
||||||
|
|
||||||
|
pbFullStatus.SignalState.URL = fullStatus.SignalState.URL
|
||||||
|
pbFullStatus.SignalState.Connected = fullStatus.SignalState.Connected
|
||||||
|
|
||||||
|
pbFullStatus.LocalPeerState.IP = fullStatus.LocalPeerState.IP
|
||||||
|
pbFullStatus.LocalPeerState.PubKey = fullStatus.LocalPeerState.PubKey
|
||||||
|
pbFullStatus.LocalPeerState.KernelInterface = fullStatus.LocalPeerState.KernelInterface
|
||||||
|
|
||||||
|
for _, peerState := range fullStatus.Peers {
|
||||||
|
pbPeerState := &proto.PeerState{
|
||||||
|
IP: peerState.IP,
|
||||||
|
PubKey: peerState.PubKey,
|
||||||
|
ConnStatus: peerState.ConnStatus,
|
||||||
|
ConnStatusUpdate: timestamppb.New(peerState.ConnStatusUpdate),
|
||||||
|
Relayed: peerState.Relayed,
|
||||||
|
Direct: peerState.Direct,
|
||||||
|
LocalIceCandidateType: peerState.LocalIceCandidateType,
|
||||||
|
RemoteIceCandidateType: peerState.RemoteIceCandidateType,
|
||||||
|
}
|
||||||
|
pbFullStatus.Peers = append(pbFullStatus.Peers, pbPeerState)
|
||||||
|
}
|
||||||
|
return &pbFullStatus
|
||||||
|
}
|
||||||
116
client/ssh/client.go
Normal file
116
client/ssh/client.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"golang.org/x/term"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client wraps crypto/ssh Client to simplify usage
|
||||||
|
type Client struct {
|
||||||
|
client *ssh.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the wrapped SSH Client
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
return c.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenTerminal starts an interactive terminal session with the remote SSH server
|
||||||
|
func (c *Client) OpenTerminal() error {
|
||||||
|
session, err := c.client.NewSession()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open new session: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := session.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
fd := int(os.Stdout.Fd())
|
||||||
|
state, err := term.MakeRaw(fd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run raw terminal: %s", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := term.Restore(fd, state)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
w, h, err := term.GetSize(fd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("terminal get size: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
modes := ssh.TerminalModes{
|
||||||
|
ssh.ECHO: 1,
|
||||||
|
ssh.TTY_OP_ISPEED: 14400,
|
||||||
|
ssh.TTY_OP_OSPEED: 14400,
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal := os.Getenv("TERM")
|
||||||
|
if terminal == "" {
|
||||||
|
terminal = "xterm-256color"
|
||||||
|
}
|
||||||
|
if err := session.RequestPty(terminal, h, w, modes); err != nil {
|
||||||
|
return fmt.Errorf("failed requesting pty session with xterm: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Stdout = os.Stdout
|
||||||
|
session.Stderr = os.Stderr
|
||||||
|
session.Stdin = os.Stdin
|
||||||
|
|
||||||
|
if err := session.Shell(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start login shell on the remote host: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := session.Wait(); err != nil {
|
||||||
|
if e, ok := err.(*ssh.ExitError); ok {
|
||||||
|
switch e.ExitStatus() {
|
||||||
|
case 130:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed running SSH session: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialWithKey connects to the remote SSH server with a provided private key file (PEM).
|
||||||
|
func DialWithKey(addr, user string, privateKey []byte) (*Client, error) {
|
||||||
|
|
||||||
|
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &ssh.ClientConfig{
|
||||||
|
User: user,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
Auth: []ssh.AuthMethod{
|
||||||
|
ssh.PublicKeys(signer),
|
||||||
|
},
|
||||||
|
HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error { return nil }),
|
||||||
|
}
|
||||||
|
|
||||||
|
return Dial("tcp", addr, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to the remote SSH server.
|
||||||
|
func Dial(network, addr string, config *ssh.ClientConfig) (*Client, error) {
|
||||||
|
client, err := ssh.Dial(network, addr, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
36
client/ssh/login.go
Normal file
36
client/ssh/login.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getLoginCmd(user string, remoteAddr net.Addr) (loginPath string, args []string, err error) {
|
||||||
|
loginPath, err = exec.LookPath("login")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
addrPort, err := netip.ParseAddrPort(remoteAddr.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
|
||||||
|
if util.FileExists("/etc/arch-release") && !util.FileExists("/etc/pam.d/remote") {
|
||||||
|
// detect if Arch Linux
|
||||||
|
return loginPath, []string{"-f", user, "-p"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginPath, []string{"-f", user, "-h", addrPort.Addr().String(), "-p"}, nil
|
||||||
|
} else if runtime.GOOS == "darwin" {
|
||||||
|
return loginPath, []string{"-fp", "-h", addrPort.Addr().String(), user}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil, fmt.Errorf("unsupported platform")
|
||||||
|
}
|
||||||
10
client/ssh/lookup.go
Normal file
10
client/ssh/lookup.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build !darwin
|
||||||
|
// +build !darwin
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import "os/user"
|
||||||
|
|
||||||
|
func userNameLookup(username string) (*user.User, error) {
|
||||||
|
return user.Lookup(username)
|
||||||
|
}
|
||||||
47
client/ssh/lookup_darwin.go
Normal file
47
client/ssh/lookup_darwin.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func userNameLookup(username string) (*user.User, error) {
|
||||||
|
var userObject *user.User
|
||||||
|
userObject, err := user.Lookup(username)
|
||||||
|
if err != nil && err.Error() == user.UnknownUserError(username).Error() {
|
||||||
|
return idUserNameLookup(username)
|
||||||
|
} else if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return userObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func idUserNameLookup(username string) (*user.User, error) {
|
||||||
|
cmd := exec.Command("id", "-P", username)
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error while retrieving user with id -P command, error: %v", err)
|
||||||
|
}
|
||||||
|
colon := ":"
|
||||||
|
|
||||||
|
if !bytes.Contains(out, []byte(username+colon)) {
|
||||||
|
return nil, fmt.Errorf("unable to find user in returned string")
|
||||||
|
}
|
||||||
|
// netbird:********:501:20::0:0:netbird:/Users/netbird:/bin/zsh
|
||||||
|
parts := strings.SplitN(string(out), colon, 10)
|
||||||
|
userObject := &user.User{
|
||||||
|
Username: parts[0],
|
||||||
|
Uid: parts[2],
|
||||||
|
Gid: parts[3],
|
||||||
|
Name: parts[7],
|
||||||
|
HomeDir: parts[8],
|
||||||
|
}
|
||||||
|
return userObject, nil
|
||||||
|
}
|
||||||
250
client/ssh/server.go
Normal file
250
client/ssh/server.go
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/creack/pty"
|
||||||
|
"github.com/gliderlabs/ssh"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultSSHPort is the default SSH port of the NetBird's embedded SSH server
|
||||||
|
const DefaultSSHPort = 44338
|
||||||
|
|
||||||
|
// DefaultSSHServer is a function that creates DefaultServer
|
||||||
|
func DefaultSSHServer(hostKeyPEM []byte, addr string) (Server, error) {
|
||||||
|
return newDefaultServer(hostKeyPEM, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server is an interface of SSH server
|
||||||
|
type Server interface {
|
||||||
|
// Stop stops SSH server.
|
||||||
|
Stop() error
|
||||||
|
// Start starts SSH server. Blocking
|
||||||
|
Start() error
|
||||||
|
// RemoveAuthorizedKey removes SSH key of a given peer from the authorized keys
|
||||||
|
RemoveAuthorizedKey(peer string)
|
||||||
|
// AddAuthorizedKey add a given peer key to server authorized keys
|
||||||
|
AddAuthorizedKey(peer, newKey string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultServer is the embedded NetBird SSH server
|
||||||
|
type DefaultServer struct {
|
||||||
|
listener net.Listener
|
||||||
|
// authorizedKeys is ssh pub key indexed by peer WireGuard public key
|
||||||
|
authorizedKeys map[string]ssh.PublicKey
|
||||||
|
mu sync.Mutex
|
||||||
|
hostKeyPEM []byte
|
||||||
|
sessions []ssh.Session
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDefaultServer creates new server with provided host key
|
||||||
|
func newDefaultServer(hostKeyPEM []byte, addr string) (*DefaultServer, error) {
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allowedKeys := make(map[string]ssh.PublicKey)
|
||||||
|
return &DefaultServer{listener: ln, mu: sync.Mutex{}, hostKeyPEM: hostKeyPEM, authorizedKeys: allowedKeys, sessions: make([]ssh.Session, 0)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAuthorizedKey removes SSH key of a given peer from the authorized keys
|
||||||
|
func (srv *DefaultServer) RemoveAuthorizedKey(peer string) {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
|
||||||
|
delete(srv.authorizedKeys, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAuthorizedKey add a given peer key to server authorized keys
|
||||||
|
func (srv *DefaultServer) AddAuthorizedKey(peer, newKey string) error {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
|
||||||
|
parsedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(newKey))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.authorizedKeys[peer] = parsedKey
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops SSH server.
|
||||||
|
func (srv *DefaultServer) Stop() error {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
err := srv.listener.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, session := range srv.sessions {
|
||||||
|
err := session.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed closing SSH session from %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *DefaultServer) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||||
|
srv.mu.Lock()
|
||||||
|
defer srv.mu.Unlock()
|
||||||
|
|
||||||
|
for _, allowed := range srv.authorizedKeys {
|
||||||
|
if ssh.KeysEqual(allowed, key) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUserEnv(user *user.User, shell string) []string {
|
||||||
|
return []string{
|
||||||
|
fmt.Sprintf("SHELL=" + shell),
|
||||||
|
fmt.Sprintf("USER=" + user.Username),
|
||||||
|
fmt.Sprintf("HOME=" + user.HomeDir),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func acceptEnv(s string) bool {
|
||||||
|
split := strings.Split(s, "=")
|
||||||
|
if len(split) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return split[0] == "TERM" || split[0] == "LANG" || strings.HasPrefix(split[0], "LC_")
|
||||||
|
}
|
||||||
|
|
||||||
|
// sessionHandler handles SSH session post auth
|
||||||
|
func (srv *DefaultServer) sessionHandler(session ssh.Session) {
|
||||||
|
srv.mu.Lock()
|
||||||
|
srv.sessions = append(srv.sessions, session)
|
||||||
|
srv.mu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := session.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
localUser, err := userNameLookup(session.User())
|
||||||
|
if err != nil {
|
||||||
|
_, err = fmt.Fprintf(session, "remote SSH server couldn't find local user %s\n", session.User()) //nolint
|
||||||
|
err = session.Exit(1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Warnf("failed SSH session from %v, user %s", session.RemoteAddr(), session.User())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ptyReq, winCh, isPty := session.Pty()
|
||||||
|
if isPty {
|
||||||
|
loginCmd, loginArgs, err := getLoginCmd(localUser.Username, session.RemoteAddr())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed logging-in user %s from remote IP %s", localUser.Username, session.RemoteAddr().String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd := exec.Command(loginCmd, loginArgs...)
|
||||||
|
go func() {
|
||||||
|
<-session.Context().Done()
|
||||||
|
err := cmd.Process.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
cmd.Dir = localUser.HomeDir
|
||||||
|
cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
|
||||||
|
cmd.Env = append(cmd.Env, prepareUserEnv(localUser, getUserShell(localUser.Uid))...)
|
||||||
|
for _, v := range session.Environ() {
|
||||||
|
if acceptEnv(v) {
|
||||||
|
cmd.Env = append(cmd.Env, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := pty.Start(cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed starting SSH server %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for win := range winCh {
|
||||||
|
setWinSize(file, win.Width, win.Height)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
srv.stdInOut(file, session)
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := io.WriteString(session, "only PTY is supported.\n")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = session.Exit(1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *DefaultServer) stdInOut(file *os.File, session ssh.Session) {
|
||||||
|
go func() {
|
||||||
|
// stdin
|
||||||
|
_, err := io.Copy(file, session)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// stdout
|
||||||
|
_, err := io.Copy(session, file)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts SSH server. Blocking
|
||||||
|
func (srv *DefaultServer) Start() error {
|
||||||
|
log.Infof("starting SSH server on addr: %s", srv.listener.Addr().String())
|
||||||
|
|
||||||
|
publicKeyOption := ssh.PublicKeyAuth(srv.publicKeyHandler)
|
||||||
|
hostKeyPEM := ssh.HostKeyPEM(srv.hostKeyPEM)
|
||||||
|
err := ssh.Serve(srv.listener, srv.sessionHandler, publicKeyOption, hostKeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserShell(userID string) string {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
output, _ := exec.Command("getent", "passwd", userID).Output()
|
||||||
|
line := strings.SplitN(string(output), ":", 10)
|
||||||
|
if len(line) > 6 {
|
||||||
|
return strings.TrimSpace(line[6])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
if shell == "" {
|
||||||
|
shell = "/bin/sh"
|
||||||
|
}
|
||||||
|
return shell
|
||||||
|
}
|
||||||
44
client/ssh/server_mock.go
Normal file
44
client/ssh/server_mock.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// MockServer mocks ssh.Server
|
||||||
|
type MockServer struct {
|
||||||
|
Ctx context.Context
|
||||||
|
StopFunc func() error
|
||||||
|
StartFunc func() error
|
||||||
|
AddAuthorizedKeyFunc func(peer, newKey string) error
|
||||||
|
RemoveAuthorizedKeyFunc func(peer string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAuthorizedKey removes SSH key of a given peer from the authorized keys
|
||||||
|
func (srv *MockServer) RemoveAuthorizedKey(peer string) {
|
||||||
|
if srv.RemoveAuthorizedKeyFunc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srv.RemoveAuthorizedKeyFunc(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAuthorizedKey add a given peer key to server authorized keys
|
||||||
|
func (srv *MockServer) AddAuthorizedKey(peer, newKey string) error {
|
||||||
|
if srv.AddAuthorizedKeyFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return srv.AddAuthorizedKeyFunc(peer, newKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops SSH server.
|
||||||
|
func (srv *MockServer) Stop() error {
|
||||||
|
if srv.StopFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return srv.StopFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts SSH server. Blocking
|
||||||
|
func (srv *MockServer) Start() error {
|
||||||
|
if srv.StartFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return srv.StartFunc()
|
||||||
|
}
|
||||||
121
client/ssh/server_test.go
Normal file
121
client/ssh/server_test.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServer_AddAuthorizedKey(t *testing.T) {
|
||||||
|
key, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server, err := newDefaultServer(key, "localhost:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add multiple keys
|
||||||
|
keys := map[string][]byte{}
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
peer := fmt.Sprintf("%s-%d", "remotePeer", i)
|
||||||
|
remotePrivKey, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
remotePubKey, err := GeneratePublicKey(remotePrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.AddAuthorizedKey(peer, string(remotePubKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
keys[peer] = remotePubKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that all keys have been added
|
||||||
|
for peer, remotePubKey := range keys {
|
||||||
|
k, ok := server.authorizedKeys[peer]
|
||||||
|
assert.True(t, ok, "expecting remotePeer key to be found in authorizedKeys")
|
||||||
|
|
||||||
|
assert.Equal(t, string(remotePubKey), strings.TrimSpace(string(ssh.MarshalAuthorizedKey(k))))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_RemoveAuthorizedKey(t *testing.T) {
|
||||||
|
key, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server, err := newDefaultServer(key, "localhost:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remotePrivKey, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
remotePubKey, err := GeneratePublicKey(remotePrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.AddAuthorizedKey("remotePeer", string(remotePubKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.RemoveAuthorizedKey("remotePeer")
|
||||||
|
|
||||||
|
_, ok := server.authorizedKeys["remotePeer"]
|
||||||
|
assert.False(t, ok, "expecting remotePeer's SSH key to be removed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_PubKeyHandler(t *testing.T) {
|
||||||
|
key, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
server, err := newDefaultServer(key, "localhost:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys []ssh.PublicKey
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
peer := fmt.Sprintf("%s-%d", "remotePeer", i)
|
||||||
|
remotePrivKey, err := GeneratePrivateKey(ED25519)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
remotePubKey, err := GeneratePublicKey(remotePrivKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteParsedPubKey, _, _, _, err := ssh.ParseAuthorizedKey(remotePubKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.AddAuthorizedKey(peer, string(remotePubKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
keys = append(keys, remoteParsedPubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
accepted := server.publicKeyHandler(nil, key)
|
||||||
|
|
||||||
|
assert.Truef(t, accepted, "expecting SSH connection to be accepted for a given SSH key %s", string(ssh.MarshalAuthorizedKey(key)))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
86
client/ssh/util.go
Normal file
86
client/ssh/util.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/crypto/ed25519"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyType is a type of SSH key
|
||||||
|
type KeyType string
|
||||||
|
|
||||||
|
// ED25519 is key of type ed25519
|
||||||
|
const ED25519 KeyType = "ed25519"
|
||||||
|
|
||||||
|
// ECDSA is key of type ecdsa
|
||||||
|
const ECDSA KeyType = "ecdsa"
|
||||||
|
|
||||||
|
// RSA is key of type rsa
|
||||||
|
const RSA KeyType = "rsa"
|
||||||
|
|
||||||
|
// RSAKeySize is a size of newly generated RSA key
|
||||||
|
const RSAKeySize = 2048
|
||||||
|
|
||||||
|
// GeneratePrivateKey creates RSA Private Key of specified byte size
|
||||||
|
func GeneratePrivateKey(keyType KeyType) ([]byte, error) {
|
||||||
|
|
||||||
|
var key crypto.Signer
|
||||||
|
var err error
|
||||||
|
switch keyType {
|
||||||
|
case ED25519:
|
||||||
|
_, key, err = ed25519.GenerateKey(rand.Reader)
|
||||||
|
case ECDSA:
|
||||||
|
key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
case RSA:
|
||||||
|
key, err = rsa.GenerateKey(rand.Reader, RSAKeySize)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported ket type %s", keyType)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pemBytes, err := EncodePrivateKeyToPEM(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pemBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GeneratePublicKey returns the public part of the private key
|
||||||
|
func GeneratePublicKey(key []byte) ([]byte, error) {
|
||||||
|
signer, err := gossh.ParsePrivateKey(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
strKey := strings.TrimSpace(string(gossh.MarshalAuthorizedKey(signer.PublicKey())))
|
||||||
|
return []byte(strKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodePrivateKeyToPEM encodes Private Key from RSA to PEM format
|
||||||
|
func EncodePrivateKeyToPEM(privateKey crypto.Signer) ([]byte, error) {
|
||||||
|
mk, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// pem.Block
|
||||||
|
privBlock := pem.Block{
|
||||||
|
Type: "PRIVATE KEY",
|
||||||
|
Bytes: mk,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private key in PEM format
|
||||||
|
privatePEM := pem.EncodeToMemory(&privBlock)
|
||||||
|
return privatePEM, nil
|
||||||
|
}
|
||||||
14
client/ssh/window_unix.go
Normal file
14
client/ssh/window_unix.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build linux || darwin
|
||||||
|
|
||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setWinSize(file *os.File, width, height int) {
|
||||||
|
syscall.Syscall(syscall.SYS_IOCTL, file.Fd(), uintptr(syscall.TIOCSWINSZ), //nolint
|
||||||
|
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(height), uint16(width), 0, 0})))
|
||||||
|
}
|
||||||
9
client/ssh/window_windows.go
Normal file
9
client/ssh/window_windows.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package ssh
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setWinSize(file *os.File, width, height int) {
|
||||||
|
|
||||||
|
}
|
||||||
223
client/status/status.go
Normal file
223
client/status/status.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PeerState contains the latest state of a peer
|
||||||
|
type PeerState struct {
|
||||||
|
IP string
|
||||||
|
PubKey string
|
||||||
|
ConnStatus string
|
||||||
|
ConnStatusUpdate time.Time
|
||||||
|
Relayed bool
|
||||||
|
Direct bool
|
||||||
|
LocalIceCandidateType string
|
||||||
|
RemoteIceCandidateType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalPeerState contains the latest state of the local peer
|
||||||
|
type LocalPeerState struct {
|
||||||
|
IP string
|
||||||
|
PubKey string
|
||||||
|
KernelInterface bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignalState contains the latest state of a signal connection
|
||||||
|
type SignalState struct {
|
||||||
|
URL string
|
||||||
|
Connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagementState contains the latest state of a management connection
|
||||||
|
type ManagementState struct {
|
||||||
|
URL string
|
||||||
|
Connected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FullStatus contains the full state held by the Status instance
|
||||||
|
type FullStatus struct {
|
||||||
|
Peers []PeerState
|
||||||
|
ManagementState ManagementState
|
||||||
|
SignalState SignalState
|
||||||
|
LocalPeerState LocalPeerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status holds a state of peers, signal and management connections
|
||||||
|
type Status struct {
|
||||||
|
mux sync.Mutex
|
||||||
|
peers map[string]PeerState
|
||||||
|
changeNotify map[string]chan struct{}
|
||||||
|
signal SignalState
|
||||||
|
management ManagementState
|
||||||
|
localPeer LocalPeerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRecorder returns a new Status instance
|
||||||
|
func NewRecorder() *Status {
|
||||||
|
return &Status{
|
||||||
|
peers: make(map[string]PeerState),
|
||||||
|
changeNotify: make(map[string]chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPeer adds peer to Daemon status map
|
||||||
|
func (d *Status) AddPeer(peerPubKey string) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
_, ok := d.peers[peerPubKey]
|
||||||
|
if ok {
|
||||||
|
return errors.New("peer already exist")
|
||||||
|
}
|
||||||
|
d.peers[peerPubKey] = PeerState{PubKey: peerPubKey}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeer adds peer to Daemon status map
|
||||||
|
func (d *Status) GetPeer(peerPubKey string) (PeerState, error) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
state, ok := d.peers[peerPubKey]
|
||||||
|
if !ok {
|
||||||
|
return PeerState{}, errors.New("peer not found")
|
||||||
|
}
|
||||||
|
return state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemovePeer removes peer from Daemon status map
|
||||||
|
func (d *Status) RemovePeer(peerPubKey string) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
_, ok := d.peers[peerPubKey]
|
||||||
|
if ok {
|
||||||
|
delete(d.peers, peerPubKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("no peer with to remove")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePeerState updates peer status
|
||||||
|
func (d *Status) UpdatePeerState(receivedState PeerState) error {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
peerState, ok := d.peers[receivedState.PubKey]
|
||||||
|
if !ok {
|
||||||
|
return errors.New("peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedState.IP != "" {
|
||||||
|
peerState.IP = receivedState.IP
|
||||||
|
}
|
||||||
|
|
||||||
|
if receivedState.ConnStatus != peerState.ConnStatus {
|
||||||
|
peerState.ConnStatus = receivedState.ConnStatus
|
||||||
|
peerState.ConnStatusUpdate = receivedState.ConnStatusUpdate
|
||||||
|
peerState.Direct = receivedState.Direct
|
||||||
|
peerState.Relayed = receivedState.Relayed
|
||||||
|
peerState.LocalIceCandidateType = receivedState.LocalIceCandidateType
|
||||||
|
peerState.RemoteIceCandidateType = receivedState.RemoteIceCandidateType
|
||||||
|
}
|
||||||
|
|
||||||
|
d.peers[receivedState.PubKey] = peerState
|
||||||
|
|
||||||
|
ch, found := d.changeNotify[receivedState.PubKey]
|
||||||
|
if found && ch != nil {
|
||||||
|
close(ch)
|
||||||
|
d.changeNotify[receivedState.PubKey] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPeerStateChangeNotifier returns a change notifier channel for a peer
|
||||||
|
func (d *Status) GetPeerStateChangeNotifier(peer string) <-chan struct{} {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
ch, found := d.changeNotify[peer]
|
||||||
|
if !found || ch == nil {
|
||||||
|
ch = make(chan struct{})
|
||||||
|
d.changeNotify[peer] = ch
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLocalPeerState updates local peer status
|
||||||
|
func (d *Status) UpdateLocalPeerState(localPeerState LocalPeerState) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
d.localPeer = localPeerState
|
||||||
|
}
|
||||||
|
|
||||||
|
// CleanLocalPeerState cleans local peer status
|
||||||
|
func (d *Status) CleanLocalPeerState() {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
d.localPeer = LocalPeerState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkManagementDisconnected sets ManagementState to disconnected
|
||||||
|
func (d *Status) MarkManagementDisconnected(managementURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.management = ManagementState{
|
||||||
|
URL: managementURL,
|
||||||
|
Connected: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkManagementConnected sets ManagementState to connected
|
||||||
|
func (d *Status) MarkManagementConnected(managementURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.management = ManagementState{
|
||||||
|
URL: managementURL,
|
||||||
|
Connected: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkSignalDisconnected sets SignalState to disconnected
|
||||||
|
func (d *Status) MarkSignalDisconnected(signalURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.signal = SignalState{
|
||||||
|
signalURL,
|
||||||
|
false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarkSignalConnected sets SignalState to connected
|
||||||
|
func (d *Status) MarkSignalConnected(signalURL string) {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
d.signal = SignalState{
|
||||||
|
signalURL,
|
||||||
|
true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFullStatus gets full status
|
||||||
|
func (d *Status) GetFullStatus() FullStatus {
|
||||||
|
d.mux.Lock()
|
||||||
|
defer d.mux.Unlock()
|
||||||
|
|
||||||
|
fullStatus := FullStatus{
|
||||||
|
ManagementState: d.management,
|
||||||
|
SignalState: d.signal,
|
||||||
|
LocalPeerState: d.localPeer,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, status := range d.peers {
|
||||||
|
fullStatus.Peers = append(fullStatus.Peers, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullStatus
|
||||||
|
}
|
||||||
225
client/status/status_test.go
Normal file
225
client/status/status_test.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddPeer(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
status := NewRecorder()
|
||||||
|
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)
|
||||||
|
|
||||||
|
assert.Error(t, err, "should return error on duplicate")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPeer(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
status := NewRecorder()
|
||||||
|
err := status.AddPeer(key)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
peerStatus, err := status.GetPeer(key)
|
||||||
|
assert.NoError(t, err, "shouldn't return error on getting peer")
|
||||||
|
|
||||||
|
assert.Equal(t, key, peerStatus.PubKey, "retrieved public key should match")
|
||||||
|
|
||||||
|
_, err = status.GetPeer("non_existing_key")
|
||||||
|
assert.Error(t, err, "should return error when peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdatePeerState(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
ip := "10.10.10.10"
|
||||||
|
status := NewRecorder()
|
||||||
|
peerState := PeerState{
|
||||||
|
PubKey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
status.peers[key] = peerState
|
||||||
|
|
||||||
|
peerState.IP = ip
|
||||||
|
|
||||||
|
err := status.UpdatePeerState(peerState)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
state, exists := status.peers[key]
|
||||||
|
assert.True(t, exists, "state should be found")
|
||||||
|
assert.Equal(t, ip, state.IP, "ip should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPeerStateChangeNotifierLogic(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
ip := "10.10.10.10"
|
||||||
|
status := NewRecorder()
|
||||||
|
peerState := PeerState{
|
||||||
|
PubKey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
status.peers[key] = peerState
|
||||||
|
|
||||||
|
ch := status.GetPeerStateChangeNotifier(key)
|
||||||
|
assert.NotNil(t, ch, "channel shouldn't be nil")
|
||||||
|
|
||||||
|
peerState.IP = ip
|
||||||
|
|
||||||
|
err := status.UpdatePeerState(peerState)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
default:
|
||||||
|
t.Errorf("channel wasn't closed after update")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemovePeer(t *testing.T) {
|
||||||
|
key := "abc"
|
||||||
|
status := NewRecorder()
|
||||||
|
peerState := PeerState{
|
||||||
|
PubKey: key,
|
||||||
|
}
|
||||||
|
|
||||||
|
status.peers[key] = peerState
|
||||||
|
|
||||||
|
err := status.RemovePeer(key)
|
||||||
|
assert.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
_, exists := status.peers[key]
|
||||||
|
assert.False(t, exists, "state value shouldn't be found")
|
||||||
|
|
||||||
|
err = status.RemovePeer("not existing")
|
||||||
|
assert.Error(t, err, "should return error when peer doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateLocalPeerState(t *testing.T) {
|
||||||
|
localPeerState := LocalPeerState{
|
||||||
|
IP: "10.10.10.10",
|
||||||
|
PubKey: "abc",
|
||||||
|
KernelInterface: false,
|
||||||
|
}
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
status.UpdateLocalPeerState(localPeerState)
|
||||||
|
|
||||||
|
assert.Equal(t, localPeerState, status.localPeer, "local peer status should be equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCleanLocalPeerState(t *testing.T) {
|
||||||
|
emptyLocalPeerState := LocalPeerState{}
|
||||||
|
localPeerState := LocalPeerState{
|
||||||
|
IP: "10.10.10.10",
|
||||||
|
PubKey: "abc",
|
||||||
|
KernelInterface: false,
|
||||||
|
}
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
status.localPeer = localPeerState
|
||||||
|
|
||||||
|
status.CleanLocalPeerState()
|
||||||
|
|
||||||
|
assert.Equal(t, emptyLocalPeerState, status.localPeer, "local peer status should be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateSignalState(t *testing.T) {
|
||||||
|
url := "https://signal"
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
connected bool
|
||||||
|
want SignalState
|
||||||
|
}{
|
||||||
|
{"should mark as connected", true, SignalState{
|
||||||
|
|
||||||
|
URL: url,
|
||||||
|
Connected: true,
|
||||||
|
}},
|
||||||
|
{"should mark as disconnected", false, SignalState{
|
||||||
|
URL: url,
|
||||||
|
Connected: false,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if test.connected {
|
||||||
|
status.MarkSignalConnected(url)
|
||||||
|
} else {
|
||||||
|
status.MarkSignalDisconnected(url)
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.want, status.signal, "signal status should be equal")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateManagementState(t *testing.T) {
|
||||||
|
url := "https://management"
|
||||||
|
var tests = []struct {
|
||||||
|
name string
|
||||||
|
connected bool
|
||||||
|
want ManagementState
|
||||||
|
}{
|
||||||
|
{"should mark as connected", true, ManagementState{
|
||||||
|
|
||||||
|
URL: url,
|
||||||
|
Connected: true,
|
||||||
|
}},
|
||||||
|
{"should mark as disconnected", false, ManagementState{
|
||||||
|
URL: url,
|
||||||
|
Connected: false,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if test.connected {
|
||||||
|
status.MarkManagementConnected(url)
|
||||||
|
} else {
|
||||||
|
status.MarkManagementDisconnected(url)
|
||||||
|
}
|
||||||
|
assert.Equal(t, test.want, status.management, "signal status should be equal")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFullStatus(t *testing.T) {
|
||||||
|
key1 := "abc"
|
||||||
|
key2 := "def"
|
||||||
|
managementState := ManagementState{
|
||||||
|
URL: "https://signal",
|
||||||
|
Connected: true,
|
||||||
|
}
|
||||||
|
signalState := SignalState{
|
||||||
|
URL: "https://signal",
|
||||||
|
Connected: true,
|
||||||
|
}
|
||||||
|
peerState1 := PeerState{
|
||||||
|
PubKey: key1,
|
||||||
|
}
|
||||||
|
|
||||||
|
peerState2 := PeerState{
|
||||||
|
PubKey: key2,
|
||||||
|
}
|
||||||
|
|
||||||
|
status := NewRecorder()
|
||||||
|
|
||||||
|
status.management = managementState
|
||||||
|
status.signal = signalState
|
||||||
|
status.peers[key1] = peerState1
|
||||||
|
status.peers[key2] = peerState2
|
||||||
|
|
||||||
|
fullStatus := status.GetFullStatus()
|
||||||
|
|
||||||
|
assert.Equal(t, managementState, fullStatus.ManagementState, "management status should be equal")
|
||||||
|
assert.Equal(t, signalState, fullStatus.SignalState, "signal status should be equal")
|
||||||
|
assert.ElementsMatch(t, []PeerState{peerState1, peerState2}, fullStatus.Peers, "peers states should match")
|
||||||
|
}
|
||||||
52
client/system/info.go
Normal file
52
client/system/info.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// this is the wiretrustee version
|
||||||
|
// will be replaced with the release version when using goreleaser
|
||||||
|
var version = "development"
|
||||||
|
|
||||||
|
//Info is an object that contains machine information
|
||||||
|
// Most of the code is taken from https://github.com/matishsiao/goInfo
|
||||||
|
type Info struct {
|
||||||
|
GoOS string
|
||||||
|
Kernel string
|
||||||
|
Core string
|
||||||
|
Platform string
|
||||||
|
OS string
|
||||||
|
OSVersion string
|
||||||
|
Hostname string
|
||||||
|
CPUs int
|
||||||
|
WiretrusteeVersion string
|
||||||
|
UIVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetbirdVersion returns the Netbird version
|
||||||
|
func NetbirdVersion() string {
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractUserAgent extracts Netbird's agent (client) name and version from the outgoing context
|
||||||
|
func extractUserAgent(ctx context.Context) string {
|
||||||
|
md, hasMeta := metadata.FromOutgoingContext(ctx)
|
||||||
|
if hasMeta {
|
||||||
|
agent, ok := md["user-agent"]
|
||||||
|
if ok {
|
||||||
|
nbAgent := strings.Split(agent[0], " ")[0]
|
||||||
|
if strings.HasPrefix(nbAgent, "netbird") {
|
||||||
|
return nbAgent
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDesktopUIUserAgent returns the Desktop ui user agent
|
||||||
|
func GetDesktopUIUserAgent() string {
|
||||||
|
return "netbird-desktop-ui/" + NetbirdVersion()
|
||||||
|
}
|
||||||
28
client/system/info_darwin.go
Normal file
28
client/system/info_darwin.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetInfo retrieves and parses the system information
|
||||||
|
func GetInfo(ctx context.Context) *Info {
|
||||||
|
utsname := unix.Utsname{}
|
||||||
|
err := unix.Uname(&utsname)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("getInfo:", err)
|
||||||
|
}
|
||||||
|
sysName := string(bytes.Split(utsname.Sysname[:], []byte{0})[0])
|
||||||
|
machine := string(bytes.Split(utsname.Machine[:], []byte{0})[0])
|
||||||
|
release := string(bytes.Split(utsname.Release[:], []byte{0})[0])
|
||||||
|
gio := &Info{Kernel: sysName, OSVersion: release, Core: release, Platform: machine, OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||||
|
gio.Hostname, _ = os.Hostname()
|
||||||
|
gio.WiretrusteeVersion = NetbirdVersion()
|
||||||
|
gio.UIVersion = extractUserAgent(ctx)
|
||||||
|
|
||||||
|
return gio
|
||||||
|
}
|
||||||
44
client/system/info_freebsd.go
Normal file
44
client/system/info_freebsd.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetInfo retrieves and parses the system information
|
||||||
|
func GetInfo(ctx context.Context) *Info {
|
||||||
|
out := _getInfo()
|
||||||
|
for strings.Contains(out, "broken pipe") {
|
||||||
|
out = _getInfo()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
osStr := strings.Replace(out, "\n", "", -1)
|
||||||
|
osStr = strings.Replace(osStr, "\r\n", "", -1)
|
||||||
|
osInfo := strings.Split(osStr, " ")
|
||||||
|
gio := &Info{Kernel: osInfo[0], Core: osInfo[1], Platform: runtime.GOARCH, OS: osInfo[2], GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||||
|
gio.Hostname, _ = os.Hostname()
|
||||||
|
gio.WiretrusteeVersion = NetbirdVersion()
|
||||||
|
gio.UIVersion = extractUserAgent(ctx)
|
||||||
|
|
||||||
|
return gio
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getInfo() string {
|
||||||
|
cmd := exec.Command("uname", "-sri")
|
||||||
|
cmd.Stdin = strings.NewReader("some input")
|
||||||
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("getInfo:", err)
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
81
client/system/info_linux.go
Normal file
81
client/system/info_linux.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetInfo retrieves and parses the system information
|
||||||
|
func GetInfo(ctx context.Context) *Info {
|
||||||
|
info := _getInfo()
|
||||||
|
for strings.Contains(info, "broken pipe") {
|
||||||
|
info = _getInfo()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseInfo := _getReleaseInfo()
|
||||||
|
for strings.Contains(info, "broken pipe") {
|
||||||
|
releaseInfo = _getReleaseInfo()
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
osRelease := strings.Split(releaseInfo, "\n")
|
||||||
|
var osName string
|
||||||
|
var osVer string
|
||||||
|
for _, s := range osRelease {
|
||||||
|
if strings.HasPrefix(s, "NAME=") {
|
||||||
|
osName = strings.Split(s, "=")[1]
|
||||||
|
osName = strings.ReplaceAll(osName, "\"", "")
|
||||||
|
} else if strings.HasPrefix(s, "VERSION_ID=") {
|
||||||
|
osVer = strings.Split(s, "=")[1]
|
||||||
|
osVer = strings.ReplaceAll(osVer, "\"", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
osStr := strings.Replace(info, "\n", "", -1)
|
||||||
|
osStr = strings.Replace(osStr, "\r\n", "", -1)
|
||||||
|
osInfo := strings.Split(osStr, " ")
|
||||||
|
if osName == "" {
|
||||||
|
osName = osInfo[3]
|
||||||
|
}
|
||||||
|
gio := &Info{Kernel: osInfo[0], Core: osInfo[1], Platform: osInfo[2], OS: osName, OSVersion: osVer, GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||||
|
gio.Hostname, _ = os.Hostname()
|
||||||
|
gio.WiretrusteeVersion = NetbirdVersion()
|
||||||
|
gio.UIVersion = extractUserAgent(ctx)
|
||||||
|
|
||||||
|
return gio
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getInfo() string {
|
||||||
|
cmd := exec.Command("uname", "-srio")
|
||||||
|
cmd.Stdin = strings.NewReader("some")
|
||||||
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("getInfo:", err)
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func _getReleaseInfo() string {
|
||||||
|
cmd := exec.Command("cat", "/etc/os-release")
|
||||||
|
cmd.Stdin = strings.NewReader("some")
|
||||||
|
var out bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("getReleaseInfo:", err)
|
||||||
|
}
|
||||||
|
return out.String()
|
||||||
|
}
|
||||||
26
client/system/info_test.go
Normal file
26
client/system/info_test.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_LocalWTVersion(t *testing.T) {
|
||||||
|
got := GetInfo(context.TODO())
|
||||||
|
want := "development"
|
||||||
|
assert.Equal(t, want, got.WiretrusteeVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_UIVersion(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
want := "netbird-desktop-ui/development"
|
||||||
|
ctx = metadata.NewOutgoingContext(ctx, map[string][]string{
|
||||||
|
"user-agent": {want},
|
||||||
|
})
|
||||||
|
|
||||||
|
got := GetInfo(ctx)
|
||||||
|
assert.Equal(t, want, got.UIVersion)
|
||||||
|
}
|
||||||
55
client/system/info_windows.go
Normal file
55
client/system/info_windows.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package system
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetInfo retrieves and parses the system information
|
||||||
|
func GetInfo(ctx context.Context) *Info {
|
||||||
|
ver := getOSVersion()
|
||||||
|
gio := &Info{Kernel: "windows", OSVersion: ver, Core: ver, Platform: "unknown", OS: "windows", GoOS: runtime.GOOS, CPUs: runtime.NumCPU()}
|
||||||
|
gio.Hostname, _ = os.Hostname()
|
||||||
|
gio.WiretrusteeVersion = NetbirdVersion()
|
||||||
|
gio.UIVersion = extractUserAgent(ctx)
|
||||||
|
|
||||||
|
return gio
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOSVersion() string {
|
||||||
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
deferErr := k.Close()
|
||||||
|
if deferErr != nil {
|
||||||
|
log.Error(deferErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
major, _, err := k.GetIntegerValue("CurrentMajorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
minor, _, err := k.GetIntegerValue("CurrentMinorVersionNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
build, _, err := k.GetStringValue("CurrentBuildNumber")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
// Update Build Revision
|
||||||
|
ubr, _, err := k.GetIntegerValue("UBR")
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
ver := fmt.Sprintf("%d.%d.%s.%d", major, minor, build, ubr)
|
||||||
|
return ver
|
||||||
|
}
|
||||||
37
client/testdata/management.json
vendored
Normal file
37
client/testdata/management.json
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"Stuns": [
|
||||||
|
{
|
||||||
|
"Proto": "udp",
|
||||||
|
"URI": "stun:stun.wiretrustee.com:3468",
|
||||||
|
"Username": "",
|
||||||
|
"Password": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"TURNConfig": {
|
||||||
|
"Turns": [
|
||||||
|
{
|
||||||
|
"Proto": "udp",
|
||||||
|
"URI": "turn:stun.wiretrustee.com:3468",
|
||||||
|
"Username": "some_user",
|
||||||
|
"Password": "c29tZV9wYXNzd29yZA=="
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"CredentialsTTL": "1h",
|
||||||
|
"Secret": "c29tZV9wYXNzd29yZA==",
|
||||||
|
"TimeBasedCredentials": true
|
||||||
|
},
|
||||||
|
"Signal": {
|
||||||
|
"Proto": "http",
|
||||||
|
"URI": "signal.wiretrustee.com:10000",
|
||||||
|
"Username": "",
|
||||||
|
"Password": null
|
||||||
|
},
|
||||||
|
"DataDir": "",
|
||||||
|
"HttpConfig": {
|
||||||
|
"LetsEncryptDomain": "<PASTE YOUR LET'S ENCRYPT DOMAIN HERE>",
|
||||||
|
"Address": "0.0.0.0:33071",
|
||||||
|
"AuthIssuer": "<PASTE YOUR AUTH0 ISSUER HERE>,",
|
||||||
|
"AuthAudience": "<PASTE YOUR AUTH0 AUDIENCE HERE>",
|
||||||
|
"AuthKeysLocation": "<PASTE YOUR AUTH0 PUBLIC JWT KEYS LOCATION HERE>"
|
||||||
|
}
|
||||||
|
}
|
||||||
38
client/testdata/store.json
vendored
Normal file
38
client/testdata/store.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"Accounts": {
|
||||||
|
"bf1c8084-ba50-4ce7-9439-34653001fc3b": {
|
||||||
|
"Id": "bf1c8084-ba50-4ce7-9439-34653001fc3b",
|
||||||
|
"SetupKeys": {
|
||||||
|
"A2C8E62B-38F5-4553-B31E-DD66C696CEBB": {
|
||||||
|
"Key": "A2C8E62B-38F5-4553-B31E-DD66C696CEBB",
|
||||||
|
"Name": "Default key",
|
||||||
|
"Type": "reusable",
|
||||||
|
"CreatedAt": "2021-08-19T20:46:20.005936822+02:00",
|
||||||
|
"ExpiresAt": "2321-09-18T20:46:20.005936822+02:00",
|
||||||
|
"Revoked": false,
|
||||||
|
"UsedTimes": 0
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Network": {
|
||||||
|
"Id": "af1c8024-ha40-4ce2-9418-34653101fc3c",
|
||||||
|
"Net": {
|
||||||
|
"IP": "100.64.0.0",
|
||||||
|
"Mask": "//8AAA=="
|
||||||
|
},
|
||||||
|
"Dns": null
|
||||||
|
},
|
||||||
|
"Peers": {},
|
||||||
|
"Users": {
|
||||||
|
"edafee4e-63fb-11ec-90d6-0242ac120003": {
|
||||||
|
"Id": "edafee4e-63fb-11ec-90d6-0242ac120003",
|
||||||
|
"Role": "admin"
|
||||||
|
},
|
||||||
|
"f4f6d672-63fb-11ec-90d6-0242ac120003": {
|
||||||
|
"Id": "f4f6d672-63fb-11ec-90d6-0242ac120003",
|
||||||
|
"Role": "user"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
client/ui/Info.plist
Normal file
12
client/ui/Info.plist
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>netbird-ui</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>Netbird</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
BIN
client/ui/Netbird.icns
Normal file
BIN
client/ui/Netbird.icns
Normal file
Binary file not shown.
BIN
client/ui/banner.bmp
Normal file
BIN
client/ui/banner.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
514
client/ui/client_ui.go
Normal file
514
client/ui/client_ui.go
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
//go:build !(linux && 386)
|
||||||
|
// +build !linux !386
|
||||||
|
|
||||||
|
// skipping linux 32 bits build and tests
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cenkalti/backoff/v4"
|
||||||
|
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
"github.com/getlantern/systray"
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/app"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultFailTimeout = 3 * time.Second
|
||||||
|
failFastTimeout = time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var daemonAddr string
|
||||||
|
|
||||||
|
defaultDaemonAddr := "unix:///var/run/netbird.sock"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.StringVar(
|
||||||
|
&daemonAddr, "daemon-addr",
|
||||||
|
defaultDaemonAddr,
|
||||||
|
"Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
||||||
|
|
||||||
|
var showSettings bool
|
||||||
|
flag.BoolVar(&showSettings, "settings", false, "run settings windows")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
a := app.New()
|
||||||
|
client := newServiceClient(daemonAddr, a, showSettings)
|
||||||
|
if showSettings {
|
||||||
|
a.Run()
|
||||||
|
} else {
|
||||||
|
if err := checkPIDFile(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
systray.Run(client.onTrayReady, client.onTrayExit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed connected.ico
|
||||||
|
var iconConnectedICO []byte
|
||||||
|
|
||||||
|
//go:embed connected.png
|
||||||
|
var iconConnectedPNG []byte
|
||||||
|
|
||||||
|
//go:embed disconnected.ico
|
||||||
|
var iconDisconnectedICO []byte
|
||||||
|
|
||||||
|
//go:embed disconnected.png
|
||||||
|
var iconDisconnectedPNG []byte
|
||||||
|
|
||||||
|
type serviceClient struct {
|
||||||
|
ctx context.Context
|
||||||
|
addr string
|
||||||
|
conn proto.DaemonServiceClient
|
||||||
|
|
||||||
|
icConnected []byte
|
||||||
|
icDisconnected []byte
|
||||||
|
|
||||||
|
// systray menu items
|
||||||
|
mStatus *systray.MenuItem
|
||||||
|
mUp *systray.MenuItem
|
||||||
|
mDown *systray.MenuItem
|
||||||
|
mAdminPanel *systray.MenuItem
|
||||||
|
mSettings *systray.MenuItem
|
||||||
|
mQuit *systray.MenuItem
|
||||||
|
|
||||||
|
// application with main windows.
|
||||||
|
app fyne.App
|
||||||
|
wSettings fyne.Window
|
||||||
|
showSettings bool
|
||||||
|
|
||||||
|
// input elements for settings form
|
||||||
|
iMngURL *widget.Entry
|
||||||
|
iAdminURL *widget.Entry
|
||||||
|
iConfigFile *widget.Entry
|
||||||
|
iLogFile *widget.Entry
|
||||||
|
iPreSharedKey *widget.Entry
|
||||||
|
|
||||||
|
// observable settings over correspondign iMngURL and iPreSharedKey values.
|
||||||
|
managementURL string
|
||||||
|
preSharedKey string
|
||||||
|
adminURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newServiceClient instance constructor
|
||||||
|
//
|
||||||
|
// This constructor olso build UI elements for settings window.
|
||||||
|
func newServiceClient(addr string, a fyne.App, showSettings bool) *serviceClient {
|
||||||
|
s := &serviceClient{
|
||||||
|
ctx: context.Background(),
|
||||||
|
addr: addr,
|
||||||
|
app: a,
|
||||||
|
|
||||||
|
showSettings: showSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
s.icConnected = iconConnectedICO
|
||||||
|
s.icDisconnected = iconDisconnectedICO
|
||||||
|
} else {
|
||||||
|
s.icConnected = iconConnectedPNG
|
||||||
|
s.icDisconnected = iconDisconnectedPNG
|
||||||
|
}
|
||||||
|
|
||||||
|
if showSettings {
|
||||||
|
s.showUIElements()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) showUIElements() {
|
||||||
|
// add settings window UI elements.
|
||||||
|
s.wSettings = s.app.NewWindow("Settings")
|
||||||
|
s.iMngURL = widget.NewEntry()
|
||||||
|
s.iAdminURL = widget.NewEntry()
|
||||||
|
s.iConfigFile = widget.NewEntry()
|
||||||
|
s.iConfigFile.Disable()
|
||||||
|
s.iLogFile = widget.NewEntry()
|
||||||
|
s.iLogFile.Disable()
|
||||||
|
s.iPreSharedKey = widget.NewPasswordEntry()
|
||||||
|
s.wSettings.SetContent(s.getSettingsForm())
|
||||||
|
s.wSettings.Resize(fyne.NewSize(600, 100))
|
||||||
|
|
||||||
|
s.getSrvConfig()
|
||||||
|
|
||||||
|
s.wSettings.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSettingsForm to embed it into settings window.
|
||||||
|
func (s *serviceClient) getSettingsForm() *widget.Form {
|
||||||
|
return &widget.Form{
|
||||||
|
Items: []*widget.FormItem{
|
||||||
|
{Text: "Management URL", Widget: s.iMngURL},
|
||||||
|
{Text: "Admin URL", Widget: s.iAdminURL},
|
||||||
|
{Text: "Pre-shared Key", Widget: s.iPreSharedKey},
|
||||||
|
{Text: "Config File", Widget: s.iConfigFile},
|
||||||
|
{Text: "Log File", Widget: s.iLogFile},
|
||||||
|
},
|
||||||
|
SubmitText: "Save",
|
||||||
|
OnSubmit: func() {
|
||||||
|
if s.iPreSharedKey.Text != "" && s.iPreSharedKey.Text != "**********" {
|
||||||
|
// validate preSharedKey if it added
|
||||||
|
if _, err := wgtypes.ParseKey(s.iPreSharedKey.Text); err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("Invalid Pre-shared Key Value"), s.wSettings)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defer s.wSettings.Close()
|
||||||
|
// if management URL or Pre-shared key changed, we try to re-login with new settings.
|
||||||
|
if s.managementURL != s.iMngURL.Text || s.preSharedKey != s.iPreSharedKey.Text ||
|
||||||
|
s.adminURL != s.iAdminURL.Text {
|
||||||
|
|
||||||
|
s.managementURL = s.iMngURL.Text
|
||||||
|
s.preSharedKey = s.iPreSharedKey.Text
|
||||||
|
s.adminURL = s.iAdminURL.Text
|
||||||
|
|
||||||
|
client, err := s.getSrvClient(failFastTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get daemon client: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Login(s.ctx, &proto.LoginRequest{
|
||||||
|
ManagementUrl: s.iMngURL.Text,
|
||||||
|
AdminURL: s.iAdminURL.Text,
|
||||||
|
PreSharedKey: s.iPreSharedKey.Text,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("login to management URL: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.Up(s.ctx, &proto.UpRequest{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("login to management URL: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
s.wSettings.Close()
|
||||||
|
},
|
||||||
|
OnCancel: func() {
|
||||||
|
s.wSettings.Close()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) login() error {
|
||||||
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get client: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loginResp, err := conn.Login(s.ctx, &proto.LoginRequest{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("login to management URL with: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginResp.NeedsSSOLogin {
|
||||||
|
err = open.Run(loginResp.VerificationURIComplete)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("opening the verification uri in the browser failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = conn.WaitSSOLogin(s.ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("waiting sso login failed with: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) menuUpClick() error {
|
||||||
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get client: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.login()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("login failed with: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get service status: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status == string(internal.StatusConnected) {
|
||||||
|
log.Warnf("already connected")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||||
|
log.Errorf("up service: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) menuDownClick() error {
|
||||||
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get client: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get service status: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status != string(internal.StatusConnected) {
|
||||||
|
log.Warnf("already down")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
||||||
|
log.Errorf("down service: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) updateStatus() error {
|
||||||
|
conn, err := s.getSrvClient(defaultFailTimeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = backoff.Retry(func() error {
|
||||||
|
status, err := conn.Status(s.ctx, &proto.StatusRequest{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get service status: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status == string(internal.StatusConnected) && !s.mUp.Disabled() {
|
||||||
|
systray.SetIcon(s.icConnected)
|
||||||
|
s.mStatus.SetTitle("Connected")
|
||||||
|
s.mUp.Disable()
|
||||||
|
s.mDown.Enable()
|
||||||
|
} else if status.Status != string(internal.StatusConnected) && s.mUp.Disabled() {
|
||||||
|
systray.SetIcon(s.icDisconnected)
|
||||||
|
s.mStatus.SetTitle("Disconnected")
|
||||||
|
s.mDown.Disable()
|
||||||
|
s.mUp.Enable()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, &backoff.ExponentialBackOff{
|
||||||
|
InitialInterval: time.Second,
|
||||||
|
RandomizationFactor: backoff.DefaultRandomizationFactor,
|
||||||
|
Multiplier: backoff.DefaultMultiplier,
|
||||||
|
MaxInterval: 300 * time.Millisecond,
|
||||||
|
MaxElapsedTime: 2 * time.Second,
|
||||||
|
Stop: backoff.Stop,
|
||||||
|
Clock: backoff.SystemClock,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) onTrayReady() {
|
||||||
|
systray.SetIcon(s.icDisconnected)
|
||||||
|
|
||||||
|
// setup systray menu items
|
||||||
|
s.mStatus = systray.AddMenuItem("Disconnected", "Disconnected")
|
||||||
|
s.mStatus.Disable()
|
||||||
|
systray.AddSeparator()
|
||||||
|
s.mUp = systray.AddMenuItem("Connect", "Connect")
|
||||||
|
s.mDown = systray.AddMenuItem("Disconnect", "Disconnect")
|
||||||
|
s.mDown.Disable()
|
||||||
|
s.mAdminPanel = systray.AddMenuItem("Admin Panel", "Wiretrustee Admin Panel")
|
||||||
|
systray.AddSeparator()
|
||||||
|
s.mSettings = systray.AddMenuItem("Settings", "Settings of the application")
|
||||||
|
systray.AddSeparator()
|
||||||
|
v := systray.AddMenuItem("v"+system.NetbirdVersion(), "Client Version: "+system.NetbirdVersion())
|
||||||
|
v.Disable()
|
||||||
|
systray.AddSeparator()
|
||||||
|
s.mQuit = systray.AddMenuItem("Quit", "Quit the client app")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.getSrvConfig()
|
||||||
|
for {
|
||||||
|
err := s.updateStatus()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while updating status: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.mAdminPanel.ClickedCh:
|
||||||
|
err = open.Run(s.adminURL)
|
||||||
|
case <-s.mUp.ClickedCh:
|
||||||
|
go func() {
|
||||||
|
err := s.menuUpClick()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case <-s.mDown.ClickedCh:
|
||||||
|
go func() {
|
||||||
|
err := s.menuDownClick()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
case <-s.mSettings.ClickedCh:
|
||||||
|
s.mSettings.Disable()
|
||||||
|
go func() {
|
||||||
|
defer s.mSettings.Enable()
|
||||||
|
proc, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("show settings: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(proc, "--settings=true")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
||||||
|
log.Errorf("start settings UI: %v, %s", err, string(out))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(out) != 0 {
|
||||||
|
log.Info("settings change:", string(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
// update config in systray when settings windows closed
|
||||||
|
s.getSrvConfig()
|
||||||
|
}()
|
||||||
|
case <-s.mQuit.ClickedCh:
|
||||||
|
systray.Quit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("process connection: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serviceClient) onTrayExit() {}
|
||||||
|
|
||||||
|
// getSrvClient connection to the service.
|
||||||
|
func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) {
|
||||||
|
if s.conn != nil {
|
||||||
|
return s.conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
strings.TrimPrefix(s.addr, "tcp://"),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithBlock(),
|
||||||
|
grpc.WithUserAgent(system.GetDesktopUIUserAgent()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dial service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conn = proto.NewDaemonServiceClient(conn)
|
||||||
|
return s.conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSrvConfig from the service to show it in the settings window.
|
||||||
|
func (s *serviceClient) getSrvConfig() {
|
||||||
|
s.managementURL = "https://api.wiretrustee.com:33073"
|
||||||
|
s.adminURL = "https://app.netbird.io"
|
||||||
|
|
||||||
|
conn, err := s.getSrvClient(failFastTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get client: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := conn.GetConfig(s.ctx, &proto.GetConfigRequest{})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get config settings from server: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.ManagementUrl != "" {
|
||||||
|
s.managementURL = cfg.ManagementUrl
|
||||||
|
}
|
||||||
|
if cfg.AdminURL != "" {
|
||||||
|
s.adminURL = cfg.AdminURL
|
||||||
|
}
|
||||||
|
s.preSharedKey = cfg.PreSharedKey
|
||||||
|
|
||||||
|
if s.showSettings {
|
||||||
|
s.iMngURL.SetText(s.managementURL)
|
||||||
|
s.iAdminURL.SetText(s.adminURL)
|
||||||
|
s.iConfigFile.SetText(cfg.ConfigFile)
|
||||||
|
s.iLogFile.SetText(cfg.LogFile)
|
||||||
|
s.iPreSharedKey.SetText(cfg.PreSharedKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPIDFile exists and return error, or write new.
|
||||||
|
func checkPIDFile() error {
|
||||||
|
pidFile := path.Join(os.TempDir(), "wiretrustee-ui.pid")
|
||||||
|
if piddata, err := os.ReadFile(pidFile); err == nil {
|
||||||
|
if pid, err := strconv.Atoi(string(piddata)); err == nil {
|
||||||
|
if process, err := os.FindProcess(pid); err == nil {
|
||||||
|
if err := process.Signal(syscall.Signal(0)); err == nil {
|
||||||
|
return fmt.Errorf("process already exists: %d", pid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0o664)
|
||||||
|
}
|
||||||
46
client/ui/config/config.go
Normal file
46
client/ui/config/config.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientConfig basic settings for the UI application.
|
||||||
|
type ClientConfig struct {
|
||||||
|
configPath string
|
||||||
|
logFile string
|
||||||
|
daemonAddr string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config object with default settings.
|
||||||
|
//
|
||||||
|
// We are creating this package to extract utility functions from the cmd package
|
||||||
|
// reading and parsing the configurations for the client should be done here
|
||||||
|
func Config() *ClientConfig {
|
||||||
|
defaultConfigPath := "/etc/wiretrustee/config.json"
|
||||||
|
defaultLogFile := "/var/log/wiretrustee/client.log"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
defaultConfigPath = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "config.json"
|
||||||
|
defaultLogFile = os.Getenv("PROGRAMDATA") + "\\Wiretrustee\\" + "client.log"
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultDaemonAddr := "unix:///var/run/wiretrustee.sock"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
||||||
|
}
|
||||||
|
return &ClientConfig{
|
||||||
|
configPath: defaultConfigPath,
|
||||||
|
logFile: defaultLogFile,
|
||||||
|
daemonAddr: defaultDaemonAddr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DaemonAddr of the gRPC API.
|
||||||
|
func (c *ClientConfig) DaemonAddr() string {
|
||||||
|
return c.daemonAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogFile path.
|
||||||
|
func (c *ClientConfig) LogFile() string {
|
||||||
|
return c.logFile
|
||||||
|
}
|
||||||
BIN
client/ui/connected.ico
Normal file
BIN
client/ui/connected.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user