mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-09 02:56:08 -04:00
Compare commits
496 Commits
update-get
...
netmap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840b07c784 | ||
|
|
85e991ff78 | ||
|
|
f9845e53a0 | ||
|
|
765aba2c1c | ||
|
|
7cb81f1d70 | ||
|
|
cea19de667 | ||
|
|
29e5eceb6b | ||
|
|
0f63737330 | ||
|
|
bf518c5fba | ||
|
|
eab6183a8e | ||
|
|
4517da8b3a | ||
|
|
9c0d923124 | ||
|
|
6857734c48 | ||
|
|
3b019800f8 | ||
|
|
4cd4f88666 | ||
|
|
d2157bda66 | ||
|
|
43a8ba97e3 | ||
|
|
17874771cc | ||
|
|
f6ccf6b97a | ||
|
|
6aae797baf | ||
|
|
aca054e51e | ||
|
|
10cee8f46e | ||
|
|
628673db20 | ||
|
|
eaa31c2dc6 | ||
|
|
25723e9b07 | ||
|
|
3cf4d5758f | ||
|
|
fc15ee6351 | ||
|
|
4a3e78fb0f | ||
|
|
f9462eea27 | ||
|
|
b075009ef7 | ||
|
|
c347a4c2ca | ||
|
|
61bc092458 | ||
|
|
b679404618 | ||
|
|
215fb257f7 | ||
|
|
381447b8d6 | ||
|
|
919c1cb3d4 | ||
|
|
85d17cbc89 | ||
|
|
c9f3854dde | ||
|
|
245b086646 | ||
|
|
1609b21b5b | ||
|
|
1f926d15b8 | ||
|
|
a432e8e23a | ||
|
|
4fec709bb1 | ||
|
|
95299be52d | ||
|
|
f51cae7103 | ||
|
|
f68d5e965f | ||
|
|
85b8f36ec1 | ||
|
|
94e505480b | ||
|
|
10d8617be6 | ||
|
|
deffe037aa | ||
|
|
983d7bafbe | ||
|
|
4da29451d0 | ||
|
|
9b3449753e | ||
|
|
456629811b | ||
|
|
c311d0d19e | ||
|
|
521f7dd39f | ||
|
|
f9ec0a9a2e | ||
|
|
012235ff12 | ||
|
|
f176807ebe | ||
|
|
d4c47eaf8a | ||
|
|
d35a79d3b5 | ||
|
|
6a2929011d | ||
|
|
e877c9d6c1 | ||
|
|
7a1c96ebf4 | ||
|
|
41fe9f84ec | ||
|
|
d13fb0e379 | ||
|
|
f3214527ea | ||
|
|
69048bfd34 | ||
|
|
29a2d93873 | ||
|
|
6b01b0020e | ||
|
|
9d3db68805 | ||
|
|
2e315311e0 | ||
|
|
67e2185964 | ||
|
|
89149dc6f4 | ||
|
|
5a1f8f13a2 | ||
|
|
e71059d245 | ||
|
|
91fa2e20a0 | ||
|
|
61034aaf4d | ||
|
|
b8717b8956 | ||
|
|
50201d63c2 | ||
|
|
d11b39282b | ||
|
|
bd58eea8ea | ||
|
|
a5811a2d7d | ||
|
|
a680f80ed9 | ||
|
|
10fbdc2c4a | ||
|
|
1444fbe104 | ||
|
|
650bca7ca8 | ||
|
|
570e28d227 | ||
|
|
272ade07a8 | ||
|
|
263abe4862 | ||
|
|
ceee421a05 | ||
|
|
0a75da6fb7 | ||
|
|
920877964f | ||
|
|
2e0047daea | ||
|
|
ce0718fcb5 | ||
|
|
c590518e0c | ||
|
|
f309b120cd | ||
|
|
7357a9954c | ||
|
|
13b63eebc1 | ||
|
|
735ed7ab34 | ||
|
|
961d9198ef | ||
|
|
df4ca01848 | ||
|
|
4e7c17756c | ||
|
|
6a4935139d | ||
|
|
35dd991776 | ||
|
|
3598418206 | ||
|
|
e435e39158 | ||
|
|
fd26e989e3 | ||
|
|
4424162bce | ||
|
|
54b045d9ca | ||
|
|
71c6437bab | ||
|
|
7b254cb966 | ||
|
|
8f3a0f2c38 | ||
|
|
1f33e2e003 | ||
|
|
1e6addaa65 | ||
|
|
f51dc13f8c | ||
|
|
3477108ce7 | ||
|
|
012e624296 | ||
|
|
4c5e987e02 | ||
|
|
a80c8b0176 | ||
|
|
9e01155d2e | ||
|
|
3c3111ad01 | ||
|
|
b74078fd95 | ||
|
|
77488ad11a | ||
|
|
e3b76448f3 | ||
|
|
e0de86d6c9 | ||
|
|
5204d07811 | ||
|
|
5ea24ba56e | ||
|
|
d30cf8706a | ||
|
|
15a2feb723 | ||
|
|
91b2f9fc51 | ||
|
|
76702c8a09 | ||
|
|
061f673a4f | ||
|
|
9505805313 | ||
|
|
704c67dec8 | ||
|
|
3ed2f08f3c | ||
|
|
4c83408f27 | ||
|
|
90bd39c740 | ||
|
|
dd0cf41147 | ||
|
|
22b2caffc6 | ||
|
|
c1f66d1354 | ||
|
|
ac0fe6025b | ||
|
|
c28657710a | ||
|
|
3875c29f6b | ||
|
|
9f32ccd453 | ||
|
|
1d1d057e7d | ||
|
|
3461b1bb90 | ||
|
|
3d2a2377c6 | ||
|
|
25f5f26527 | ||
|
|
bb0d5c5baf | ||
|
|
7938295190 | ||
|
|
9af532fe71 | ||
|
|
23a1473797 | ||
|
|
9c2dc05df1 | ||
|
|
40d56e5d29 | ||
|
|
fd23d0c28f | ||
|
|
4fff93a1f2 | ||
|
|
22beac1b1b | ||
|
|
bd7a65d798 | ||
|
|
2d76b058fc | ||
|
|
ea2d060f93 | ||
|
|
68b377a28c | ||
|
|
af50eb350f | ||
|
|
2475473227 | ||
|
|
846871913d | ||
|
|
6cba9c0818 | ||
|
|
f0672b87bc | ||
|
|
9b0fe2c8e5 | ||
|
|
abd57d1191 | ||
|
|
416f04c27a | ||
|
|
fc7c1e397f | ||
|
|
52a3ac6b06 | ||
|
|
0b3b50c705 | ||
|
|
042141db06 | ||
|
|
4a1aee1ae0 | ||
|
|
ba33572ec9 | ||
|
|
9d213e0b54 | ||
|
|
5dde044fa5 | ||
|
|
5a3d9e401f | ||
|
|
fde1a2196c | ||
|
|
0aeb87742a | ||
|
|
6d747b2f83 | ||
|
|
199bf73103 | ||
|
|
17f5abc653 | ||
|
|
aa935bdae3 | ||
|
|
452419c4c3 | ||
|
|
17b1099032 | ||
|
|
a4b9e93217 | ||
|
|
63d7957140 | ||
|
|
9a6814deff | ||
|
|
190698bcf2 | ||
|
|
468fa2940b | ||
|
|
79a0647a26 | ||
|
|
17ceb3bde8 | ||
|
|
5a8f1763a6 | ||
|
|
f64e73ca70 | ||
|
|
b085419ab8 | ||
|
|
d78b652ff7 | ||
|
|
7251150c1c | ||
|
|
b65c2f69b0 | ||
|
|
d8ce08d898 | ||
|
|
e1c50248d9 | ||
|
|
ce2d14c08e | ||
|
|
52fd9a575a | ||
|
|
9028c3c1f7 | ||
|
|
9357a587e9 | ||
|
|
a47c69c472 | ||
|
|
bbea4c3cc3 | ||
|
|
b7a6cbfaa5 | ||
|
|
e18bf565a2 | ||
|
|
51fa3c92c5 | ||
|
|
d65602f904 | ||
|
|
8d9e1fed5f | ||
|
|
e1eddd1cab | ||
|
|
0fbf72434e | ||
|
|
51f133fdc6 | ||
|
|
d5338c09dc | ||
|
|
8fd4166c53 | ||
|
|
9bc7b9e897 | ||
|
|
db3cba5e0f | ||
|
|
cb3408a10b | ||
|
|
0afd738509 | ||
|
|
cf87f1e702 | ||
|
|
e890fdae54 | ||
|
|
dd14db6478 | ||
|
|
88747e3e01 | ||
|
|
fb30931365 | ||
|
|
a7547b9990 | ||
|
|
62bacee8dc | ||
|
|
71cd2e3e03 | ||
|
|
bdf71ab7ff | ||
|
|
a2f2a6e21a | ||
|
|
f89332fcd2 | ||
|
|
8604add997 | ||
|
|
93cab49696 | ||
|
|
b6835d9467 | ||
|
|
846d486366 | ||
|
|
9c56f74235 | ||
|
|
25b3641be8 | ||
|
|
c41504b571 | ||
|
|
399493a954 | ||
|
|
4771fed64f | ||
|
|
88117f7d16 | ||
|
|
5ac9f9fe2f | ||
|
|
a7d6632298 | ||
|
|
d4194cba6a | ||
|
|
131d9f1bc7 | ||
|
|
f099e02b34 | ||
|
|
93646e6a13 | ||
|
|
67a2127fd7 | ||
|
|
dd7fcbd083 | ||
|
|
d5f330b9c0 | ||
|
|
9fa0fbda0d | ||
|
|
5a7aa461de | ||
|
|
e9c967b27c | ||
|
|
ace588758c | ||
|
|
8bb16e016c | ||
|
|
6a2a97f088 | ||
|
|
3591795a58 | ||
|
|
5311ce4e4a | ||
|
|
c61cb00f40 | ||
|
|
72a1e97304 | ||
|
|
5242851ecc | ||
|
|
cb69348a30 | ||
|
|
69dbcbd362 | ||
|
|
5de4acf2fe | ||
|
|
aa3b79d311 | ||
|
|
8b4ec96516 | ||
|
|
1f3a12d941 | ||
|
|
1de3bb5420 | ||
|
|
163933d429 | ||
|
|
875a2e2b63 | ||
|
|
fd8bba6aa3 | ||
|
|
86908eee58 | ||
|
|
c1caec3fcb | ||
|
|
b28b8fce50 | ||
|
|
f780f17f85 | ||
|
|
5903715a61 | ||
|
|
5469de53c5 | ||
|
|
bc3d647d6b | ||
|
|
7060b63838 | ||
|
|
3168b80ad0 | ||
|
|
818c6b885f | ||
|
|
01f28baec7 | ||
|
|
56896794b3 | ||
|
|
f73a2e2848 | ||
|
|
19fa071a93 | ||
|
|
cba3c549e9 | ||
|
|
65247de48d | ||
|
|
2d1dfa3ae7 | ||
|
|
5961c8330e | ||
|
|
d275d411aa | ||
|
|
5ecafef5d2 | ||
|
|
d073a250cc | ||
|
|
a1c48468ab | ||
|
|
dd1e730454 | ||
|
|
050f140245 | ||
|
|
006ba32086 | ||
|
|
b03343bc4d | ||
|
|
36d62f1844 | ||
|
|
08733ed8d5 | ||
|
|
27ed88f918 | ||
|
|
45fc89b2c9 | ||
|
|
f822a58326 | ||
|
|
d1f13025d1 | ||
|
|
3f8b500f0b | ||
|
|
0d2db4b172 | ||
|
|
7a18dea766 | ||
|
|
ae5f69562d | ||
|
|
755ffcfc73 | ||
|
|
dc8f55f23e | ||
|
|
89249b414f | ||
|
|
92adf57fea | ||
|
|
e37a337164 | ||
|
|
1cd5a66575 | ||
|
|
b9fc008542 | ||
|
|
d5bf79bc51 | ||
|
|
d7efea74b6 | ||
|
|
b8c46e2654 | ||
|
|
4bf574037f | ||
|
|
47c44d4b87 | ||
|
|
96f866fb68 | ||
|
|
141065f14e | ||
|
|
8e74fb1fa8 | ||
|
|
ba96e102b4 | ||
|
|
7a46a63a14 | ||
|
|
2129b23fe7 | ||
|
|
b6211ad020 | ||
|
|
efd05ca023 | ||
|
|
c829ad930c | ||
|
|
ad1f18a52a | ||
|
|
bab420ca77 | ||
|
|
c2eaf8a1c0 | ||
|
|
a729c83b06 | ||
|
|
dc05102b8f | ||
|
|
a7e55cc5e3 | ||
|
|
b7c0eba1e5 | ||
|
|
d1a323fa9d | ||
|
|
63d211c698 | ||
|
|
0ca06b566a | ||
|
|
cf9e447bf0 | ||
|
|
fdd23d4644 | ||
|
|
5a3ee4f9c4 | ||
|
|
5ffed796c0 | ||
|
|
ab895be4a3 | ||
|
|
96cdcf8e49 | ||
|
|
63f6514be5 | ||
|
|
afece95ae5 | ||
|
|
d78b7e5d93 | ||
|
|
67906f6da5 | ||
|
|
52b5a31058 | ||
|
|
b58094de0f | ||
|
|
456aaf2868 | ||
|
|
d379c25ff5 | ||
|
|
f86ed12cf5 | ||
|
|
5a45f79fec | ||
|
|
e7d063126d | ||
|
|
fb42fedb58 | ||
|
|
9eb1e90bbe | ||
|
|
53fb0a9754 | ||
|
|
70c7543e36 | ||
|
|
d1d01a0611 | ||
|
|
9e8725618e | ||
|
|
a40261ff7e | ||
|
|
89e8540531 | ||
|
|
9f7e13fc87 | ||
|
|
8be6e92563 | ||
|
|
b726b3262d | ||
|
|
125a7a9daf | ||
|
|
9b1a0c2df7 | ||
|
|
1568c8aa91 | ||
|
|
2f5ba96596 | ||
|
|
63568e5e0e | ||
|
|
9c4bf1e899 | ||
|
|
2c01514259 | ||
|
|
e2f27502e4 | ||
|
|
8cf2866a6a | ||
|
|
c99ae6f009 | ||
|
|
8843784312 | ||
|
|
c38d65ef4c | ||
|
|
6d4240a5ae | ||
|
|
52f5101715 | ||
|
|
e2eef4e3fd | ||
|
|
76318f3f06 | ||
|
|
db25ca21a8 | ||
|
|
a8d03d8c91 | ||
|
|
74ff2619d0 | ||
|
|
40bea645e9 | ||
|
|
e7d52beeab | ||
|
|
7a5c6b24ae | ||
|
|
90c2093018 | ||
|
|
06318a15e1 | ||
|
|
eeb38b7ecf | ||
|
|
e59d2317fe | ||
|
|
ee6be58a67 | ||
|
|
a9f5fad625 | ||
|
|
c979a4e9fb | ||
|
|
f2fc0df104 | ||
|
|
87cc53b743 | ||
|
|
7d8a69cc0c | ||
|
|
e4de1d75de | ||
|
|
73e57f17ea | ||
|
|
46f5f148da | ||
|
|
32880c56a4 | ||
|
|
2b90ff8c24 | ||
|
|
b8599f634c | ||
|
|
659110f0d5 | ||
|
|
4ad14cb46b | ||
|
|
3c485dc7a1 | ||
|
|
f7e6cdcbf0 | ||
|
|
af6fdd3af2 | ||
|
|
5781ec7a8e | ||
|
|
1219006a6e | ||
|
|
4791e41004 | ||
|
|
9131069d12 | ||
|
|
26bbc33e7a | ||
|
|
35bc493cc3 | ||
|
|
e26ec0b937 | ||
|
|
a952e7c72f | ||
|
|
22f69d7852 | ||
|
|
b23011fbe8 | ||
|
|
6ad3894a51 | ||
|
|
c81b83b346 | ||
|
|
8c5c6815e0 | ||
|
|
0c470e7838 | ||
|
|
8118d60ffb | ||
|
|
1956ca169e | ||
|
|
830dee1771 | ||
|
|
c08a96770e | ||
|
|
c6bf1c7f26 | ||
|
|
5f499d66b2 | ||
|
|
7c065bd9fc | ||
|
|
ab849f0942 | ||
|
|
aa1d31bde6 | ||
|
|
5b4dc4dd47 | ||
|
|
1324169ebb | ||
|
|
732afd8393 | ||
|
|
da7b6b11ad | ||
|
|
e260270825 | ||
|
|
d4b6d7646c | ||
|
|
8febab4076 | ||
|
|
34e2c6b943 | ||
|
|
0be8c72601 | ||
|
|
c34e53477f | ||
|
|
8d18190c94 | ||
|
|
06bec61be9 | ||
|
|
2135533f1d | ||
|
|
bb791d59f3 | ||
|
|
30f1c54ed1 | ||
|
|
5c8541ef42 | ||
|
|
fa4b8c1d42 | ||
|
|
7682fe2e45 | ||
|
|
c9b2ce08eb | ||
|
|
246abda46d | ||
|
|
e4bc76c4de | ||
|
|
bdb8383485 | ||
|
|
bb40325977 | ||
|
|
8524cc75d6 | ||
|
|
c1f164c9cb | ||
|
|
4e2d075413 | ||
|
|
f89c200ce9 | ||
|
|
d51dc4fd33 | ||
|
|
00dddb9458 | ||
|
|
1a9301b684 | ||
|
|
80d9b5fca5 | ||
|
|
ac0b7dc8cb | ||
|
|
e586eca16c | ||
|
|
892db25021 | ||
|
|
da75a76d41 | ||
|
|
3ac32fd78a | ||
|
|
3aa657599b | ||
|
|
d4e9087f94 | ||
|
|
da8447a67d | ||
|
|
8e3bcd57a2 | ||
|
|
4572c6c1f8 | ||
|
|
01f2b0ecb7 | ||
|
|
442ba7cbc8 | ||
|
|
6c2b364966 | ||
|
|
0f0c7ec2ed | ||
|
|
2dec016201 | ||
|
|
06125acb8d | ||
|
|
a9b9b3fa0a | ||
|
|
cdf57275b7 | ||
|
|
e5e69b1f75 | ||
|
|
8eca83f3cb | ||
|
|
973316d194 | ||
|
|
a0a6ced148 | ||
|
|
0fc6c477a9 | ||
|
|
401a462398 | ||
|
|
a3839a6ef7 | ||
|
|
8aa4f240c7 | ||
|
|
d9686bae92 | ||
|
|
24e19ae287 | ||
|
|
74fde0ea2c | ||
|
|
890e09b787 | ||
|
|
48098c994d |
15
.devcontainer/Dockerfile
Normal file
15
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM golang:1.21-bullseye
|
||||||
|
|
||||||
|
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||||
|
&& apt-get -y install --no-install-recommends\
|
||||||
|
gettext-base=0.21-4 \
|
||||||
|
iptables=1.8.7-1 \
|
||||||
|
libgl1-mesa-dev=20.3.5-1 \
|
||||||
|
xorg-dev=1:7.7+22 \
|
||||||
|
libayatana-appindicator3-dev=0.5.5-2+deb11u2 \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& go install -v golang.org/x/tools/gopls@latest
|
||||||
|
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
20
.devcontainer/devcontainer.json
Normal file
20
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "NetBird",
|
||||||
|
"build": {
|
||||||
|
"context": "..",
|
||||||
|
"dockerfile": "Dockerfile"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||||
|
"ghcr.io/devcontainers/features/go:1": {
|
||||||
|
"version": "1.21"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||||
|
"capAdd": [
|
||||||
|
"NET_ADMIN",
|
||||||
|
"SYS_ADMIN",
|
||||||
|
"SYS_RESOURCE"
|
||||||
|
],
|
||||||
|
"privileged": true
|
||||||
|
}
|
||||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.go text eol=lf
|
||||||
18
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
18
.github/ISSUE_TEMPLATE/bug-issue-report.md
vendored
@@ -2,15 +2,17 @@
|
|||||||
name: Bug/Issue report
|
name: Bug/Issue report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ['triage-needed']
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the problem**
|
**Describe the problem**
|
||||||
|
|
||||||
A clear and concise description of what the problem is.
|
A clear and concise description of what the problem is.
|
||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@@ -18,13 +20,25 @@ Steps to reproduce the behavior:
|
|||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
|
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Are you using NetBird Cloud?**
|
||||||
|
|
||||||
|
Please specify whether you use NetBird Cloud or self-host NetBird's control plane.
|
||||||
|
|
||||||
|
**NetBird version**
|
||||||
|
|
||||||
|
`netbird version`
|
||||||
|
|
||||||
**NetBird status -d output:**
|
**NetBird status -d output:**
|
||||||
If applicable, add the output of the `netbird status -d` command
|
|
||||||
|
If applicable, add the `netbird status -d' command output.
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
|
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for this project
|
about: Suggest an idea for this project
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ['feature-request']
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
19
.github/workflows/golang-test-darwin.yml
vendored
19
.github/workflows/golang-test-darwin.yml
vendored
@@ -12,25 +12,34 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
store: ['sqlite']
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20.x"
|
go-version: "1.21.x"
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: macos-go-${{ hashFiles('**/go.sum') }}
|
key: macos-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
macos-go-
|
macos-go-
|
||||||
|
|
||||||
|
- name: Install libpcap
|
||||||
|
run: brew install libpcap
|
||||||
|
|
||||||
- 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: Test
|
- name: Test
|
||||||
run: go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
run: NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 ./...
|
||||||
|
|||||||
39
.github/workflows/golang-test-freebsd.yml
vendored
Normal file
39
.github/workflows/golang-test-freebsd.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
|
||||||
|
name: Test Code FreeBSD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Test in FreeBSD
|
||||||
|
id: test
|
||||||
|
uses: vmactions/freebsd-vm@v1
|
||||||
|
with:
|
||||||
|
usesh: true
|
||||||
|
prepare: |
|
||||||
|
pkg install -y curl
|
||||||
|
pkg install -y git
|
||||||
|
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
curl -o go.tar.gz https://go.dev/dl/go1.21.11.freebsd-amd64.tar.gz -L
|
||||||
|
tar zxf go.tar.gz
|
||||||
|
mv go /usr/local/go
|
||||||
|
ln -s /usr/local/go/bin/go /usr/local/bin/go
|
||||||
|
go mod tidy
|
||||||
|
go test -timeout 5m -p 1 ./iface/...
|
||||||
|
go test -timeout 5m -p 1 ./client/...
|
||||||
|
cd client
|
||||||
|
go build .
|
||||||
|
cd ..
|
||||||
54
.github/workflows/golang-test-linux.yml
vendored
54
.github/workflows/golang-test-linux.yml
vendored
@@ -15,16 +15,17 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: [ '386','amd64' ]
|
||||||
|
store: [ 'sqlite', 'postgres']
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20.x"
|
go-version: "1.21.x"
|
||||||
|
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -32,28 +33,34 @@ jobs:
|
|||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
||||||
|
|
||||||
|
- name: Install 32-bit libpcap
|
||||||
|
if: matrix.arch == '386'
|
||||||
|
run: sudo dpkg --add-architecture i386 && sudo apt update && sudo apt-get install -y libpcap0.8-dev:i386
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} go test -exec 'sudo --preserve-env=CI' -timeout 5m -p 1 ./...
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} go test -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 5m -p 1 ./...
|
||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20.x"
|
go-version: "1.21.x"
|
||||||
|
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: ~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
@@ -61,14 +68,17 @@ jobs:
|
|||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev gcc-multilib libpcap-dev
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Generate Iface Test bin
|
- name: Generate Iface Test bin
|
||||||
run: CGO_ENABLED=0 go test -c -o iface-testing.bin ./iface/
|
run: CGO_ENABLED=0 go test -c -o iface-testing.bin ./iface/
|
||||||
|
|
||||||
@@ -76,13 +86,16 @@ jobs:
|
|||||||
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
|
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock
|
||||||
|
|
||||||
- name: Generate RouteManager Test bin
|
- name: Generate RouteManager Test bin
|
||||||
run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager/...
|
run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager
|
||||||
|
|
||||||
|
- name: Generate SystemOps Test bin
|
||||||
|
run: CGO_ENABLED=1 go test -c -o systemops-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/systemops
|
||||||
|
|
||||||
- name: Generate nftables Manager Test bin
|
- name: Generate nftables Manager Test bin
|
||||||
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
|
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
|
||||||
|
|
||||||
- name: Generate Engine Test bin
|
- name: Generate Engine Test bin
|
||||||
run: CGO_ENABLED=0 go test -c -o engine-testing.bin ./client/internal
|
run: CGO_ENABLED=1 go test -c -o engine-testing.bin ./client/internal
|
||||||
|
|
||||||
- name: Generate Peer Test bin
|
- name: Generate Peer Test bin
|
||||||
run: CGO_ENABLED=0 go test -c -o peer-testing.bin ./client/internal/peer/...
|
run: CGO_ENABLED=0 go test -c -o peer-testing.bin ./client/internal/peer/...
|
||||||
@@ -95,15 +108,20 @@ jobs:
|
|||||||
- name: Run Iface tests in docker
|
- 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
|
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
|
- 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
|
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 SystemOps tests in docker
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager/systemops --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/systemops-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
- name: Run nftables Manager tests in docker
|
- name: Run nftables Manager tests in docker
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
- name: Run Engine tests in docker
|
- name: Run Engine tests in docker with file store
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="jsonfile" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
|
- name: Run Engine tests in docker with sqlite store
|
||||||
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal -e NETBIRD_STORE_ENGINE="sqlite" --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/engine-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
|
|
||||||
- name: Run Peer tests in docker
|
- name: Run Peer tests in docker
|
||||||
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1
|
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/peer --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/peer-testing.bin -test.timeout 5m -test.parallel 1
|
||||||
10
.github/workflows/golang-test-windows.yml
vendored
10
.github/workflows/golang-test-windows.yml
vendored
@@ -23,13 +23,13 @@ jobs:
|
|||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v4
|
||||||
id: go
|
id: go
|
||||||
with:
|
with:
|
||||||
go-version: "1.20.x"
|
go-version: "1.21.x"
|
||||||
|
|
||||||
- name: Download wintun
|
- name: Download wintun
|
||||||
uses: carlosperate/download-file-action@v2
|
uses: carlosperate/download-file-action@v2
|
||||||
id: download-wintun
|
id: download-wintun
|
||||||
with:
|
with:
|
||||||
file-url: https://www.wintun.net/builds/wintun-0.14.1.zip
|
file-url: https://pkgs.netbird.io/wintun/wintun-0.14.1.zip
|
||||||
file-name: wintun.zip
|
file-name: wintun.zip
|
||||||
location: ${{ env.downloadPath }}
|
location: ${{ env.downloadPath }}
|
||||||
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
sha256: '07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51'
|
||||||
@@ -39,12 +39,14 @@ jobs:
|
|||||||
|
|
||||||
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
- run: mv ${{ env.downloadPath }}/wintun/bin/amd64/wintun.dll 'C:\Windows\System32\'
|
||||||
|
|
||||||
- run: choco install -y sysinternals
|
- run: choco install -y sysinternals --ignore-checksums
|
||||||
|
- run: choco install -y mingw
|
||||||
|
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=C:\Users\runneradmin\go\pkg\mod
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOMODCACHE=C:\Users\runneradmin\go\pkg\mod
|
||||||
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=C:\Users\runneradmin\AppData\Local\go-build
|
- run: PsExec64 -s -w ${{ github.workspace }} C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe env -w GOCACHE=C:\Users\runneradmin\AppData\Local\go-build
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 5m -p 1 ./... > test-out.txt 2>&1"
|
run: PsExec64 -s -w ${{ github.workspace }} cmd.exe /c "C:\hostedtoolcache\windows\go\${{ steps.go.outputs.go-version }}\x64\bin\go.exe test -timeout 10m -p 1 ./... > test-out.txt 2>&1"
|
||||||
- name: test output
|
- name: test output
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
run: Get-Content test-out.txt
|
run: Get-Content test-out.txt
|
||||||
47
.github/workflows/golangci-lint.yml
vendored
47
.github/workflows/golangci-lint.yml
vendored
@@ -1,21 +1,52 @@
|
|||||||
name: golangci-lint
|
name: golangci-lint
|
||||||
on: [pull_request]
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
golangci:
|
codespell:
|
||||||
name: lint
|
name: codespell
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: codespell
|
||||||
|
uses: codespell-project/actions-codespell@v2
|
||||||
|
with:
|
||||||
|
ignore_words_list: erro,clienta,hastable,
|
||||||
|
skip: go.mod,go.sum
|
||||||
|
only_warn: 1
|
||||||
|
golangci:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
|
name: lint
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Check for duplicate constants
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
! awk '/const \(/,/)/{print $0}' management/server/activity/codes.go | grep -o '= [0-9]*' | sort | uniq -d | grep .
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20.x"
|
go-version: "1.21.x"
|
||||||
|
cache: false
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: sudo apt update && sudo apt install -y -q libgtk-3-dev libayatana-appindicator3-dev libgl1-mesa-dev xorg-dev libpcap-dev
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v2
|
uses: golangci/golangci-lint-action@v3
|
||||||
with:
|
with:
|
||||||
args: --timeout=6m
|
version: latest
|
||||||
|
args: --timeout=12m
|
||||||
36
.github/workflows/install-script-test.yml
vendored
Normal file
36
.github/workflows/install-script-test.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Test installation
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "release_files/install.sh"
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
jobs:
|
||||||
|
test-install-script:
|
||||||
|
strategy:
|
||||||
|
max-parallel: 2
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest]
|
||||||
|
skip_ui_mode: [true, false]
|
||||||
|
install_binary: [true, false]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: run install script
|
||||||
|
env:
|
||||||
|
SKIP_UI_APP: ${{ matrix.skip_ui_mode }}
|
||||||
|
USE_BIN_INSTALL: ${{ matrix.install_binary }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.RO_API_CALLER_TOKEN }}
|
||||||
|
run: |
|
||||||
|
[ "$SKIP_UI_APP" == "false" ] && export XDG_CURRENT_DESKTOP="none"
|
||||||
|
cat release_files/install.sh | sh -x
|
||||||
|
|
||||||
|
- name: check cli binary
|
||||||
|
run: command -v netbird
|
||||||
60
.github/workflows/install-test-darwin.yml
vendored
60
.github/workflows/install-test-darwin.yml
vendored
@@ -1,60 +0,0 @@
|
|||||||
name: Test installation Darwin
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "release_files/install.sh"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
install-cli-only:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Rename brew package
|
|
||||||
if: ${{ matrix.check_bin_install }}
|
|
||||||
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
|
|
||||||
|
|
||||||
- name: Run install script
|
|
||||||
run: |
|
|
||||||
sh ./release_files/install.sh
|
|
||||||
env:
|
|
||||||
SKIP_UI_APP: true
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if ! command -v netbird &> /dev/null; then
|
|
||||||
echo "Error: netbird is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
install-all:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Rename brew package
|
|
||||||
if: ${{ matrix.check_bin_install }}
|
|
||||||
run: mv /opt/homebrew/bin/brew /opt/homebrew/bin/brew.bak
|
|
||||||
|
|
||||||
- name: Run install script
|
|
||||||
run: |
|
|
||||||
sh ./release_files/install.sh
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if ! command -v netbird &> /dev/null; then
|
|
||||||
echo "Error: netbird is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $(mdfind "kMDItemContentType == 'com.apple.application-bundle' && kMDItemFSName == '*NetBird UI.app'") ]]; then
|
|
||||||
echo "Error: NetBird UI is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
38
.github/workflows/install-test-linux.yml
vendored
38
.github/workflows/install-test-linux.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Test installation Linux
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "release_files/install.sh"
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
|
||||||
install-cli-only:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
check_bin_install: [true, false]
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Rename apt package
|
|
||||||
if: ${{ matrix.check_bin_install }}
|
|
||||||
run: |
|
|
||||||
sudo mv /usr/bin/apt /usr/bin/apt.bak
|
|
||||||
sudo mv /usr/bin/apt-get /usr/bin/apt-get.bak
|
|
||||||
|
|
||||||
- name: Run install script
|
|
||||||
run: |
|
|
||||||
sh ./release_files/install.sh
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: |
|
|
||||||
if ! command -v netbird &> /dev/null; then
|
|
||||||
echo "Error: netbird is not installed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
65
.github/workflows/mobile-build-validation.yml
vendored
Normal file
65
.github/workflows/mobile-build-validation.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: Mobile build validation
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
android_build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21.x"
|
||||||
|
- name: Setup Android SDK
|
||||||
|
uses: android-actions/setup-android@v3
|
||||||
|
with:
|
||||||
|
cmdline-tools-version: 8512546
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: "11"
|
||||||
|
distribution: "adopt"
|
||||||
|
- name: NDK Cache
|
||||||
|
id: ndk-cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: /usr/local/lib/android/sdk/ndk
|
||||||
|
key: ndk-cache-23.1.7779620
|
||||||
|
- name: Setup NDK
|
||||||
|
run: /usr/local/lib/android/sdk/cmdline-tools/7.0/bin/sdkmanager --install "ndk;23.1.7779620"
|
||||||
|
- name: install gomobile
|
||||||
|
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240404231514-09dbf07665ed
|
||||||
|
- name: gomobile init
|
||||||
|
run: gomobile init
|
||||||
|
- name: build android netbird lib
|
||||||
|
run: PATH=$PATH:$(go env GOPATH) gomobile bind -o $GITHUB_WORKSPACE/netbird.aar -javapkg=io.netbird.gomobile -ldflags="-X golang.zx2c4.com/wireguard/ipc.socketDirectory=/data/data/io.netbird.client/cache/wireguard -X github.com/netbirdio/netbird/version.version=buildtest" $GITHUB_WORKSPACE/client/android
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
ANDROID_NDK_HOME: /usr/local/lib/android/sdk/ndk/23.1.7779620
|
||||||
|
ios_build:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21.x"
|
||||||
|
- name: install gomobile
|
||||||
|
run: go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20240404231514-09dbf07665ed
|
||||||
|
- name: gomobile init
|
||||||
|
run: gomobile init
|
||||||
|
- name: build iOS netbird lib
|
||||||
|
run: PATH=$PATH:$(go env GOPATH) gomobile bind -target=ios -bundleid=io.netbird.framework -ldflags="-X github.com/netbirdio/netbird/version.version=buildtest" -o ./NetBirdSDK.xcframework ./client/ios/NetBirdSDK
|
||||||
|
env:
|
||||||
|
CGO_ENABLED: 0
|
||||||
110
.github/workflows/release.yml
vendored
110
.github/workflows/release.yml
vendored
@@ -8,8 +8,9 @@ on:
|
|||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.8"
|
SIGN_PIPE_VER: "v0.0.11"
|
||||||
GORELEASER_VER: "v1.14.1"
|
GORELEASER_VER: "v1.14.1"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -19,25 +20,32 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
flags: ""
|
||||||
steps:
|
steps:
|
||||||
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
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: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.21"
|
||||||
|
cache: false
|
||||||
-
|
-
|
||||||
name: Cache Go modules
|
name: Cache Go modules
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: |
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-go-releaser-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-releaser-
|
||||||
-
|
-
|
||||||
name: Install modules
|
name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
@@ -46,10 +54,10 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
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@v2
|
||||||
-
|
-
|
||||||
name: Set up Docker Buildx
|
name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
-
|
-
|
||||||
name: Login to Docker hub
|
name: Login to Docker hub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
@@ -72,10 +80,10 @@ jobs:
|
|||||||
run: rsrc -arch 386 -ico client/ui/netbird.ico -manifest client/manifest.xml -o client/resources_windows_386.syso
|
run: rsrc -arch 386 -ico client/ui/netbird.ico -manifest client/manifest.xml -o client/resources_windows_386.syso
|
||||||
-
|
-
|
||||||
name: Run GoReleaser
|
name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --rm-dist
|
args: release --rm-dist ${{ env.flags }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
@@ -83,31 +91,57 @@ jobs:
|
|||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
-
|
-
|
||||||
name: upload non tags for debug purposes
|
name: upload non tags for debug purposes
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: release
|
name: release
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
-
|
||||||
|
name: upload linux packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: linux-packages
|
||||||
|
path: dist/netbird_linux**
|
||||||
|
retention-days: 3
|
||||||
|
-
|
||||||
|
name: upload windows packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: windows-packages
|
||||||
|
path: dist/netbird_windows**
|
||||||
|
retention-days: 3
|
||||||
|
-
|
||||||
|
name: upload macos packages
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: macos-packages
|
||||||
|
path: dist/netbird_darwin**
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
release_ui:
|
release_ui:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
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: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.21"
|
||||||
|
cache: false
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: |
|
||||||
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-ui-go-releaser-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-ui-go-
|
${{ runner.os }}-ui-go-releaser-
|
||||||
|
|
||||||
- name: Install modules
|
- name: Install modules
|
||||||
run: go mod tidy
|
run: go mod tidy
|
||||||
@@ -122,58 +156,66 @@ jobs:
|
|||||||
- name: Generate windows rsrc
|
- 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
|
run: rsrc -arch amd64 -ico client/ui/netbird.ico -manifest client/ui/manifest.xml -o client/ui/resources_windows_amd64.syso
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui.yaml --rm-dist
|
args: release --config .goreleaser_ui.yaml --rm-dist ${{ env.flags }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }}
|
||||||
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_DEBIAN_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
UPLOAD_YUM_SECRET: ${{ secrets.PKG_UPLOAD_SECRET }}
|
||||||
- name: upload non tags for debug purposes
|
- name: upload non tags for debug purposes
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: release-ui
|
name: release-ui
|
||||||
path: dist/
|
path: dist/
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
release_ui_darwin:
|
release_ui_darwin:
|
||||||
runs-on: macos-11
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
|
- if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
run: echo "flags=--snapshot" >> $GITHUB_ENV
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
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: Set up Go
|
name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: "1.20"
|
go-version: "1.21"
|
||||||
|
cache: false
|
||||||
-
|
-
|
||||||
name: Cache Go modules
|
name: Cache Go modules
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/go/pkg/mod
|
path: |
|
||||||
key: ${{ runner.os }}-ui-go-${{ hashFiles('**/go.sum') }}
|
~/go/pkg/mod
|
||||||
|
~/.cache/go-build
|
||||||
|
key: ${{ runner.os }}-ui-go-releaser-darwin-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-ui-go-
|
${{ runner.os }}-ui-go-releaser-darwin-
|
||||||
-
|
-
|
||||||
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: Run GoReleaser
|
name: Run GoReleaser
|
||||||
id: goreleaser
|
id: goreleaser
|
||||||
uses: goreleaser/goreleaser-action@v2
|
uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
version: ${{ env.GORELEASER_VER }}
|
version: ${{ env.GORELEASER_VER }}
|
||||||
args: release --config .goreleaser_ui_darwin.yaml --rm-dist
|
args: release --config .goreleaser_ui_darwin.yaml --rm-dist ${{ env.flags }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
-
|
-
|
||||||
name: upload non tags for debug purposes
|
name: upload non tags for debug purposes
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: release-ui-darwin
|
name: release-ui-darwin
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|||||||
22
.github/workflows/sync-main.yml
vendored
Normal file
22
.github/workflows/sync-main.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: sync main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger_sync_main:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Trigger main branch sync
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: sync-main.yml
|
||||||
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "sha": "${{ github.sha }}" }'
|
||||||
23
.github/workflows/sync-tag.yml
vendored
Normal file
23
.github/workflows/sync-tag.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: sync tag
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger_sync_tag:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Trigger release tag sync
|
||||||
|
uses: benc-uk/workflow-dispatch@v1
|
||||||
|
with:
|
||||||
|
workflow: sync-tag.yml
|
||||||
|
ref: main
|
||||||
|
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||||
|
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||||
|
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||||
123
.github/workflows/test-docker-compose-linux.yml
vendored
123
.github/workflows/test-docker-compose-linux.yml
vendored
@@ -1,123 +0,0 @@
|
|||||||
name: Test Docker Compose Linux
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
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.20.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_DOMAIN: localhost
|
|
||||||
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
|
|
||||||
CI_NETBIRD_AUTH_CLIENT_SECRET: testing.client.secret
|
|
||||||
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
|
|
||||||
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
|
|
||||||
CI_NETBIRD_USE_AUTH0: true
|
|
||||||
CI_NETBIRD_MGMT_IDP: "none"
|
|
||||||
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
|
|
||||||
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
|
||||||
CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified"
|
|
||||||
|
|
||||||
- name: check values
|
|
||||||
working-directory: infrastructure_files
|
|
||||||
env:
|
|
||||||
CI_NETBIRD_DOMAIN: localhost
|
|
||||||
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
|
|
||||||
CI_NETBIRD_AUTH_CLIENT_SECRET: testing.client.secret
|
|
||||||
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
|
|
||||||
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
|
|
||||||
CI_NETBIRD_USE_AUTH0: true
|
|
||||||
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_PKCE_AUTHORIZATION_ENDPOINT: https://example.eu.auth0.com/authorize
|
|
||||||
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
|
|
||||||
CI_NETBIRD_TOKEN_SOURCE: "idToken"
|
|
||||||
CI_NETBIRD_AUTH_USER_ID_CLAIM: "email"
|
|
||||||
CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE: "super"
|
|
||||||
CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE: "openid email"
|
|
||||||
CI_NETBIRD_MGMT_IDP: "none"
|
|
||||||
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
|
|
||||||
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
|
||||||
|
|
||||||
run: |
|
|
||||||
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
|
||||||
grep AUTH_CLIENT_SECRET docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_SECRET
|
|
||||||
grep AUTH_AUTHORITY docker-compose.yml | grep $CI_NETBIRD_AUTH_AUTHORITY
|
|
||||||
grep AUTH_AUDIENCE docker-compose.yml | grep $CI_NETBIRD_AUTH_AUDIENCE
|
|
||||||
grep AUTH_SUPPORTED_SCOPES docker-compose.yml | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
|
||||||
grep USE_AUTH0 docker-compose.yml | grep $CI_NETBIRD_USE_AUTH0
|
|
||||||
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "$CI_NETBIRD_DOMAIN: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=$'
|
|
||||||
grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$'
|
|
||||||
grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE
|
|
||||||
grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM
|
|
||||||
grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
|
|
||||||
grep -A 8 DeviceAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE"
|
|
||||||
grep UseIDToken management.json | grep false
|
|
||||||
grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP
|
|
||||||
grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY
|
|
||||||
grep -A 4 IdpManagerConfig management.json | grep -A 2 ClientConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
|
|
||||||
grep -A 5 IdpManagerConfig management.json | grep -A 3 ClientConfig | grep ClientID | grep $CI_NETBIRD_IDP_MGMT_CLIENT_ID
|
|
||||||
grep -A 6 IdpManagerConfig management.json | grep -A 4 ClientConfig | grep ClientSecret | grep $CI_NETBIRD_IDP_MGMT_CLIENT_SECRET
|
|
||||||
grep -A 7 IdpManagerConfig management.json | grep -A 5 ClientConfig | grep GrantType | grep client_credentials
|
|
||||||
grep -A 2 PKCEAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_AUDIENCE
|
|
||||||
grep -A 3 PKCEAuthorizationFlow management.json | grep -A 2 ProviderConfig | grep ClientID | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
|
||||||
grep -A 4 PKCEAuthorizationFlow management.json | grep -A 3 ProviderConfig | grep ClientSecret | grep $CI_NETBIRD_AUTH_CLIENT_SECRET
|
|
||||||
grep -A 5 PKCEAuthorizationFlow management.json | grep -A 4 ProviderConfig | grep AuthorizationEndpoint | grep $CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
|
|
||||||
grep -A 6 PKCEAuthorizationFlow management.json | grep -A 5 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
|
|
||||||
grep -A 7 PKCEAuthorizationFlow management.json | grep -A 6 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
|
||||||
|
|
||||||
- name: run docker compose up
|
|
||||||
working-directory: infrastructure_files
|
|
||||||
run: |
|
|
||||||
docker-compose up -d
|
|
||||||
sleep 5
|
|
||||||
docker-compose ps
|
|
||||||
docker-compose logs --tail=20
|
|
||||||
|
|
||||||
- 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
|
|
||||||
256
.github/workflows/test-infrastructure-files.yml
vendored
Normal file
256
.github/workflows/test-infrastructure-files.yml
vendored
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
name: Test Infrastructure files
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'infrastructure_files/**'
|
||||||
|
- '.github/workflows/test-infrastructure-files.yml'
|
||||||
|
- 'management/cmd/**'
|
||||||
|
- 'signal/cmd/**'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-docker-compose:
|
||||||
|
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@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21.x"
|
||||||
|
|
||||||
|
- name: Cache Go modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- 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_DOMAIN: localhost
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_SECRET: testing.client.secret
|
||||||
|
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
|
||||||
|
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
|
||||||
|
CI_NETBIRD_USE_AUTH0: true
|
||||||
|
CI_NETBIRD_MGMT_IDP: "none"
|
||||||
|
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
|
||||||
|
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
||||||
|
CI_NETBIRD_AUTH_SUPPORTED_SCOPES: "openid profile email offline_access api email_verified"
|
||||||
|
CI_NETBIRD_STORE_CONFIG_ENGINE: "sqlite"
|
||||||
|
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
||||||
|
|
||||||
|
- name: check values
|
||||||
|
working-directory: infrastructure_files/artifacts
|
||||||
|
env:
|
||||||
|
CI_NETBIRD_DOMAIN: localhost
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_ID: testing.client.id
|
||||||
|
CI_NETBIRD_AUTH_CLIENT_SECRET: testing.client.secret
|
||||||
|
CI_NETBIRD_AUTH_AUDIENCE: testing.ci
|
||||||
|
CI_NETBIRD_AUTH_OIDC_CONFIGURATION_ENDPOINT: https://example.eu.auth0.com/.well-known/openid-configuration
|
||||||
|
CI_NETBIRD_USE_AUTH0: true
|
||||||
|
CI_NETBIRD_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_PKCE_AUTHORIZATION_ENDPOINT: https://example.eu.auth0.com/authorize
|
||||||
|
CI_NETBIRD_AUTH_REDIRECT_URI: "/peers"
|
||||||
|
CI_NETBIRD_TOKEN_SOURCE: "idToken"
|
||||||
|
CI_NETBIRD_AUTH_USER_ID_CLAIM: "email"
|
||||||
|
CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE: "super"
|
||||||
|
CI_NETBIRD_AUTH_DEVICE_AUTH_SCOPE: "openid email"
|
||||||
|
CI_NETBIRD_MGMT_IDP: "none"
|
||||||
|
CI_NETBIRD_IDP_MGMT_CLIENT_ID: testing.client.id
|
||||||
|
CI_NETBIRD_IDP_MGMT_CLIENT_SECRET: testing.client.secret
|
||||||
|
CI_NETBIRD_SIGNAL_PORT: 12345
|
||||||
|
CI_NETBIRD_STORE_CONFIG_ENGINE: "sqlite"
|
||||||
|
CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH: false
|
||||||
|
CI_NETBIRD_TURN_EXTERNAL_IP: "1.2.3.4"
|
||||||
|
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
grep AUTH_CLIENT_ID docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
||||||
|
grep AUTH_CLIENT_SECRET docker-compose.yml | grep $CI_NETBIRD_AUTH_CLIENT_SECRET
|
||||||
|
grep AUTH_AUTHORITY docker-compose.yml | grep $CI_NETBIRD_AUTH_AUTHORITY
|
||||||
|
grep AUTH_AUDIENCE docker-compose.yml | grep $CI_NETBIRD_AUTH_AUDIENCE
|
||||||
|
grep AUTH_SUPPORTED_SCOPES docker-compose.yml | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
||||||
|
grep USE_AUTH0 docker-compose.yml | grep $CI_NETBIRD_USE_AUTH0
|
||||||
|
grep NETBIRD_MGMT_API_ENDPOINT docker-compose.yml | grep "$CI_NETBIRD_DOMAIN: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=$'
|
||||||
|
grep $CI_NETBIRD_SIGNAL_PORT docker-compose.yml | grep ':80'
|
||||||
|
grep LETSENCRYPT_DOMAIN docker-compose.yml | egrep 'LETSENCRYPT_DOMAIN=$'
|
||||||
|
grep NETBIRD_TOKEN_SOURCE docker-compose.yml | grep $CI_NETBIRD_TOKEN_SOURCE
|
||||||
|
grep AuthUserIDClaim management.json | grep $CI_NETBIRD_AUTH_USER_ID_CLAIM
|
||||||
|
grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
|
||||||
|
grep -A 3 DeviceAuthorizationFlow management.json | grep -A 1 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_DEVICE_AUTH_AUDIENCE
|
||||||
|
grep Engine management.json | grep "$CI_NETBIRD_STORE_CONFIG_ENGINE"
|
||||||
|
grep IdpSignKeyRefreshEnabled management.json | grep "$CI_NETBIRD_MGMT_IDP_SIGNKEY_REFRESH"
|
||||||
|
grep UseIDToken management.json | grep false
|
||||||
|
grep -A 1 IdpManagerConfig management.json | grep ManagerType | grep $CI_NETBIRD_MGMT_IDP
|
||||||
|
grep -A 3 IdpManagerConfig management.json | grep -A 1 ClientConfig | grep Issuer | grep $CI_NETBIRD_AUTH_AUTHORITY
|
||||||
|
grep -A 4 IdpManagerConfig management.json | grep -A 2 ClientConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
|
||||||
|
grep -A 5 IdpManagerConfig management.json | grep -A 3 ClientConfig | grep ClientID | grep $CI_NETBIRD_IDP_MGMT_CLIENT_ID
|
||||||
|
grep -A 6 IdpManagerConfig management.json | grep -A 4 ClientConfig | grep ClientSecret | grep $CI_NETBIRD_IDP_MGMT_CLIENT_SECRET
|
||||||
|
grep -A 7 IdpManagerConfig management.json | grep -A 5 ClientConfig | grep GrantType | grep client_credentials
|
||||||
|
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep Audience | grep $CI_NETBIRD_AUTH_AUDIENCE
|
||||||
|
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep ClientID | grep $CI_NETBIRD_AUTH_CLIENT_ID
|
||||||
|
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep ClientSecret | grep $CI_NETBIRD_AUTH_CLIENT_SECRET
|
||||||
|
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep AuthorizationEndpoint | grep $CI_NETBIRD_AUTH_PKCE_AUTHORIZATION_ENDPOINT
|
||||||
|
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep TokenEndpoint | grep $CI_NETBIRD_AUTH_TOKEN_ENDPOINT
|
||||||
|
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep Scope | grep "$CI_NETBIRD_AUTH_SUPPORTED_SCOPES"
|
||||||
|
grep -A 10 PKCEAuthorizationFlow management.json | grep -A 10 ProviderConfig | grep -A 3 RedirectURLs | grep "http://localhost:53000"
|
||||||
|
grep "external-ip" turnserver.conf | grep $CI_NETBIRD_TURN_EXTERNAL_IP
|
||||||
|
|
||||||
|
- name: Install modules
|
||||||
|
run: go mod tidy
|
||||||
|
|
||||||
|
- name: check git status
|
||||||
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
|
- name: Build management binary
|
||||||
|
working-directory: management
|
||||||
|
run: CGO_ENABLED=1 go build -o netbird-mgmt main.go
|
||||||
|
|
||||||
|
- name: Build management docker image
|
||||||
|
working-directory: management
|
||||||
|
run: |
|
||||||
|
docker build -t netbirdio/management:latest .
|
||||||
|
|
||||||
|
- name: Build signal binary
|
||||||
|
working-directory: signal
|
||||||
|
run: CGO_ENABLED=0 go build -o netbird-signal main.go
|
||||||
|
|
||||||
|
- name: Build signal docker image
|
||||||
|
working-directory: signal
|
||||||
|
run: |
|
||||||
|
docker build -t netbirdio/signal:latest .
|
||||||
|
|
||||||
|
- name: run docker compose up
|
||||||
|
working-directory: infrastructure_files/artifacts
|
||||||
|
run: |
|
||||||
|
docker-compose up -d
|
||||||
|
sleep 5
|
||||||
|
docker-compose ps
|
||||||
|
docker-compose logs --tail=20
|
||||||
|
|
||||||
|
- name: test running containers
|
||||||
|
run: |
|
||||||
|
count=$(docker compose ps --format json | jq '. | select(.Name | contains("artifacts")) | .State' | grep -c running)
|
||||||
|
test $count -eq 4
|
||||||
|
working-directory: infrastructure_files/artifacts
|
||||||
|
|
||||||
|
- name: test geolocation databases
|
||||||
|
working-directory: infrastructure_files/artifacts
|
||||||
|
run: |
|
||||||
|
sleep 30
|
||||||
|
docker compose exec management ls -l /var/lib/netbird/ | grep -i GeoLite2-City.mmdb
|
||||||
|
docker compose exec management ls -l /var/lib/netbird/ | grep -i geonames.db
|
||||||
|
|
||||||
|
test-getting-started-script:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get install -y jq
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: run script with Zitadel PostgreSQL
|
||||||
|
run: NETBIRD_DOMAIN=use-ip bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
|
||||||
|
- name: test Caddy file gen postgres
|
||||||
|
run: test -f Caddyfile
|
||||||
|
|
||||||
|
- name: test docker-compose file gen postgres
|
||||||
|
run: test -f docker-compose.yml
|
||||||
|
|
||||||
|
- name: test management.json file gen postgres
|
||||||
|
run: test -f management.json
|
||||||
|
|
||||||
|
- name: test turnserver.conf file gen postgres
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
test -f turnserver.conf
|
||||||
|
grep external-ip turnserver.conf
|
||||||
|
|
||||||
|
- name: test zitadel.env file gen postgres
|
||||||
|
run: test -f zitadel.env
|
||||||
|
|
||||||
|
- name: test dashboard.env file gen postgres
|
||||||
|
run: test -f dashboard.env
|
||||||
|
|
||||||
|
- name: test zdb.env file gen postgres
|
||||||
|
run: test -f zdb.env
|
||||||
|
|
||||||
|
- name: Postgres run cleanup
|
||||||
|
run: |
|
||||||
|
docker-compose down --volumes --rmi all
|
||||||
|
rm -rf docker-compose.yml Caddyfile zitadel.env dashboard.env machinekey/zitadel-admin-sa.token turnserver.conf management.json zdb.env
|
||||||
|
|
||||||
|
- name: run script with Zitadel CockroachDB
|
||||||
|
run: bash -x infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
env:
|
||||||
|
NETBIRD_DOMAIN: use-ip
|
||||||
|
ZITADEL_DATABASE: cockroach
|
||||||
|
|
||||||
|
- name: test Caddy file gen CockroachDB
|
||||||
|
run: test -f Caddyfile
|
||||||
|
|
||||||
|
- name: test docker-compose file gen CockroachDB
|
||||||
|
run: test -f docker-compose.yml
|
||||||
|
|
||||||
|
- name: test management.json file gen CockroachDB
|
||||||
|
run: test -f management.json
|
||||||
|
|
||||||
|
- name: test turnserver.conf file gen CockroachDB
|
||||||
|
run: |
|
||||||
|
set -x
|
||||||
|
test -f turnserver.conf
|
||||||
|
grep external-ip turnserver.conf
|
||||||
|
|
||||||
|
- name: test zitadel.env file gen CockroachDB
|
||||||
|
run: test -f zitadel.env
|
||||||
|
|
||||||
|
- name: test dashboard.env file gen CockroachDB
|
||||||
|
run: test -f dashboard.env
|
||||||
|
|
||||||
|
test-download-geolite2-script:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install jq
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y unzip sqlite3
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: test script
|
||||||
|
run: bash -x infrastructure_files/download-geolite2.sh
|
||||||
|
|
||||||
|
- name: test mmdb file exists
|
||||||
|
run: test -f GeoLite2-City.mmdb
|
||||||
|
|
||||||
|
- name: test geonames file exists
|
||||||
|
run: test -f geonames.db
|
||||||
21
.gitignore
vendored
21
.gitignore
vendored
@@ -6,11 +6,20 @@ bin/
|
|||||||
.env
|
.env
|
||||||
conf.json
|
conf.json
|
||||||
http-cmds.sh
|
http-cmds.sh
|
||||||
infrastructure_files/management.json
|
setup.env
|
||||||
infrastructure_files/management-*.json
|
infrastructure_files/**/Caddyfile
|
||||||
infrastructure_files/docker-compose.yml
|
infrastructure_files/**/dashboard.env
|
||||||
infrastructure_files/openid-configuration.json
|
infrastructure_files/**/zitadel.env
|
||||||
infrastructure_files/turnserver.conf
|
infrastructure_files/**/management.json
|
||||||
|
infrastructure_files/**/management-*.json
|
||||||
|
infrastructure_files/**/docker-compose.yml
|
||||||
|
infrastructure_files/**/openid-configuration.json
|
||||||
|
infrastructure_files/**/turnserver.conf
|
||||||
|
infrastructure_files/**/management.json.bkp.**
|
||||||
|
infrastructure_files/**/management-*.json.bkp.**
|
||||||
|
infrastructure_files/**/docker-compose.yml.bkp.**
|
||||||
|
infrastructure_files/**/openid-configuration.json.bkp.**
|
||||||
|
infrastructure_files/**/turnserver.conf.bkp.**
|
||||||
management/management
|
management/management
|
||||||
client/client
|
client/client
|
||||||
client/client.exe
|
client/client.exe
|
||||||
@@ -19,3 +28,5 @@ client/.distfiles/
|
|||||||
infrastructure_files/setup.env
|
infrastructure_files/setup.env
|
||||||
infrastructure_files/setup-*.env
|
infrastructure_files/setup-*.env
|
||||||
.vscode
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
GeoLite2-City*
|
||||||
139
.golangci.yaml
Normal file
139
.golangci.yaml
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
run:
|
||||||
|
# Timeout for analysis, e.g. 30s, 5m.
|
||||||
|
# Default: 1m
|
||||||
|
timeout: 6m
|
||||||
|
|
||||||
|
# This file contains only configs which differ from defaults.
|
||||||
|
# All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
|
||||||
|
linters-settings:
|
||||||
|
errcheck:
|
||||||
|
# Report about not checking of errors in type assertions: `a := b.(MyStruct)`.
|
||||||
|
# Such cases aren't reported by default.
|
||||||
|
# Default: false
|
||||||
|
check-type-assertions: false
|
||||||
|
|
||||||
|
gosec:
|
||||||
|
includes:
|
||||||
|
- G101 # Look for hard coded credentials
|
||||||
|
#- G102 # Bind to all interfaces
|
||||||
|
- G103 # Audit the use of unsafe block
|
||||||
|
- G104 # Audit errors not checked
|
||||||
|
- G106 # Audit the use of ssh.InsecureIgnoreHostKey
|
||||||
|
#- G107 # Url provided to HTTP request as taint input
|
||||||
|
- G108 # Profiling endpoint automatically exposed on /debug/pprof
|
||||||
|
- G109 # Potential Integer overflow made by strconv.Atoi result conversion to int16/32
|
||||||
|
- G110 # Potential DoS vulnerability via decompression bomb
|
||||||
|
- G111 # Potential directory traversal
|
||||||
|
#- G112 # Potential slowloris attack
|
||||||
|
- G113 # Usage of Rat.SetString in math/big with an overflow (CVE-2022-23772)
|
||||||
|
#- G114 # Use of net/http serve function that has no support for setting timeouts
|
||||||
|
- G201 # SQL query construction using format string
|
||||||
|
- G202 # SQL query construction using string concatenation
|
||||||
|
- G203 # Use of unescaped data in HTML templates
|
||||||
|
#- G204 # Audit use of command execution
|
||||||
|
- G301 # Poor file permissions used when creating a directory
|
||||||
|
- G302 # Poor file permissions used with chmod
|
||||||
|
- G303 # Creating tempfile using a predictable path
|
||||||
|
- G304 # File path provided as taint input
|
||||||
|
- G305 # File traversal when extracting zip/tar archive
|
||||||
|
- G306 # Poor file permissions used when writing to a new file
|
||||||
|
- G307 # Poor file permissions used when creating a file with os.Create
|
||||||
|
#- G401 # Detect the usage of DES, RC4, MD5 or SHA1
|
||||||
|
#- G402 # Look for bad TLS connection settings
|
||||||
|
- G403 # Ensure minimum RSA key length of 2048 bits
|
||||||
|
#- G404 # Insecure random number source (rand)
|
||||||
|
#- G501 # Import blocklist: crypto/md5
|
||||||
|
- G502 # Import blocklist: crypto/des
|
||||||
|
- G503 # Import blocklist: crypto/rc4
|
||||||
|
- G504 # Import blocklist: net/http/cgi
|
||||||
|
#- G505 # Import blocklist: crypto/sha1
|
||||||
|
- G601 # Implicit memory aliasing of items from a range statement
|
||||||
|
- G602 # Slice access out of bounds
|
||||||
|
|
||||||
|
gocritic:
|
||||||
|
disabled-checks:
|
||||||
|
- commentFormatting
|
||||||
|
- captLocal
|
||||||
|
- deprecatedComment
|
||||||
|
|
||||||
|
govet:
|
||||||
|
# Enable all analyzers.
|
||||||
|
# Default: false
|
||||||
|
enable-all: false
|
||||||
|
enable:
|
||||||
|
- nilness
|
||||||
|
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: exported
|
||||||
|
severity: warning
|
||||||
|
disabled: false
|
||||||
|
arguments:
|
||||||
|
- "checkPrivateReceivers"
|
||||||
|
- "sayRepetitiveInsteadOfStutters"
|
||||||
|
tenv:
|
||||||
|
# The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures.
|
||||||
|
# Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked.
|
||||||
|
# Default: false
|
||||||
|
all: true
|
||||||
|
|
||||||
|
linters:
|
||||||
|
disable-all: true
|
||||||
|
enable:
|
||||||
|
## enabled by default
|
||||||
|
- errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
|
||||||
|
- gosimple # specializes in simplifying a code
|
||||||
|
- govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
|
||||||
|
- ineffassign # detects when assignments to existing variables are not used
|
||||||
|
- staticcheck # is a go vet on steroids, applying a ton of static analysis checks
|
||||||
|
- tenv # Tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17.
|
||||||
|
- typecheck # like the front-end of a Go compiler, parses and type-checks Go code
|
||||||
|
- unused # checks for unused constants, variables, functions and types
|
||||||
|
## disable by default but the have interesting results so lets add them
|
||||||
|
- bodyclose # checks whether HTTP response body is closed successfully
|
||||||
|
- dupword # dupword checks for duplicate words in the source code
|
||||||
|
- durationcheck # durationcheck checks for two durations multiplied together
|
||||||
|
- forbidigo # forbidigo forbids identifiers
|
||||||
|
- gocritic # provides diagnostics that check for bugs, performance and style issues
|
||||||
|
- gosec # inspects source code for security problems
|
||||||
|
- mirror # mirror reports wrong mirror patterns of bytes/strings usage
|
||||||
|
- misspell # misspess finds commonly misspelled English words in comments
|
||||||
|
- nilerr # finds the code that returns nil even if it checks that the error is not nil
|
||||||
|
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
|
||||||
|
- predeclared # predeclared finds code that shadows one of Go's predeclared identifiers
|
||||||
|
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
|
||||||
|
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
|
||||||
|
- thelper # thelper detects Go test helpers without t.Helper() call and checks the consistency of test helpers.
|
||||||
|
- wastedassign # wastedassign finds wasted assignment statements
|
||||||
|
issues:
|
||||||
|
# Maximum count of issues with the same text.
|
||||||
|
# Set to 0 to disable.
|
||||||
|
# Default: 3
|
||||||
|
max-same-issues: 5
|
||||||
|
|
||||||
|
exclude-rules:
|
||||||
|
# allow fmt
|
||||||
|
- path: management/cmd/root\.go
|
||||||
|
linters: forbidigo
|
||||||
|
- path: signal/cmd/root\.go
|
||||||
|
linters: forbidigo
|
||||||
|
- path: sharedsock/filter\.go
|
||||||
|
linters:
|
||||||
|
- unused
|
||||||
|
- path: client/firewall/iptables/rule\.go
|
||||||
|
linters:
|
||||||
|
- unused
|
||||||
|
- path: test\.go
|
||||||
|
linters:
|
||||||
|
- mirror
|
||||||
|
- gosec
|
||||||
|
- path: mock\.go
|
||||||
|
linters:
|
||||||
|
- nilnil
|
||||||
|
# Exclude specific deprecation warnings for grpc methods
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "grpc.DialContext is deprecated"
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "grpc.WithBlock is deprecated"
|
||||||
@@ -377,3 +377,13 @@ uploads:
|
|||||||
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
target: https://pkgs.wiretrustee.com/yum/{{ .Arch }}{{ if .Arm }}{{ .Arm }}{{ end }}
|
||||||
username: dev@wiretrustee.com
|
username: dev@wiretrustee.com
|
||||||
method: PUT
|
method: PUT
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
- glob: ./release_files/install.sh
|
||||||
|
|
||||||
|
release:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./infrastructure_files/getting-started-with-zitadel.sh
|
||||||
|
- glob: ./release_files/install.sh
|
||||||
@@ -54,7 +54,7 @@ nfpms:
|
|||||||
contents:
|
contents:
|
||||||
- src: client/ui/netbird.desktop
|
- src: client/ui/netbird.desktop
|
||||||
dst: /usr/share/applications/netbird.desktop
|
dst: /usr/share/applications/netbird.desktop
|
||||||
- src: client/ui/disconnected.png
|
- src: client/ui/netbird-systemtray-connected.png
|
||||||
dst: /usr/share/pixmaps/netbird.png
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
dependencies:
|
dependencies:
|
||||||
- netbird
|
- netbird
|
||||||
@@ -71,7 +71,7 @@ nfpms:
|
|||||||
contents:
|
contents:
|
||||||
- src: client/ui/netbird.desktop
|
- src: client/ui/netbird.desktop
|
||||||
dst: /usr/share/applications/netbird.desktop
|
dst: /usr/share/applications/netbird.desktop
|
||||||
- src: client/ui/disconnected.png
|
- src: client/ui/netbird-systemtray-connected.png
|
||||||
dst: /usr/share/pixmaps/netbird.png
|
dst: /usr/share/pixmaps/netbird.png
|
||||||
dependencies:
|
dependencies:
|
||||||
- netbird
|
- netbird
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ builds:
|
|||||||
- id: netbird-ui-darwin
|
- id: netbird-ui-darwin
|
||||||
dir: client/ui
|
dir: client/ui
|
||||||
binary: netbird-ui
|
binary: netbird-ui
|
||||||
env: [CGO_ENABLED=1]
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
- MACOSX_DEPLOYMENT_TARGET=11.0
|
||||||
|
- MACOS_DEPLOYMENT_TARGET=11.0
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
goarch:
|
goarch:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
We as members, contributors, and leaders pledge to make participation in our
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socioeconomic status,
|
||||||
nationality, personal appearance, race, caste, color, religion, or sexual
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
identity and orientation.
|
identity and orientation.
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ If you haven't already, join our slack workspace [here](https://join.slack.com/t
|
|||||||
- [Development setup](#development-setup)
|
- [Development setup](#development-setup)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
- [Local NetBird setup](#local-netbird-setup)
|
- [Local NetBird setup](#local-netbird-setup)
|
||||||
|
- [Dev Container Support](#dev-container-support)
|
||||||
- [Build and start](#build-and-start)
|
- [Build and start](#build-and-start)
|
||||||
- [Test suite](#test-suite)
|
- [Test suite](#test-suite)
|
||||||
- [Checklist before submitting a PR](#checklist-before-submitting-a-pr)
|
- [Checklist before submitting a PR](#checklist-before-submitting-a-pr)
|
||||||
- [Other project repositories](#other-project-repositories)
|
- [Other project repositories](#other-project-repositories)
|
||||||
- [Checklist before submitting a new node](#checklist-before-submitting-a-new-node)
|
|
||||||
- [Contributor License Agreement](#contributor-license-agreement)
|
- [Contributor License Agreement](#contributor-license-agreement)
|
||||||
|
|
||||||
## Code of conduct
|
## Code of conduct
|
||||||
@@ -70,7 +70,7 @@ dependencies are installed. Here is a short guide on how that can be done.
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
#### Go 1.19
|
#### Go 1.21
|
||||||
|
|
||||||
Follow the installation guide from https://go.dev/
|
Follow the installation guide from https://go.dev/
|
||||||
|
|
||||||
@@ -136,18 +136,61 @@ checked out and set up:
|
|||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Dev Container Support
|
||||||
|
|
||||||
|
If you prefer using a dev container for development, NetBird now includes support for dev containers.
|
||||||
|
Dev containers provide a consistent and isolated development environment, making it easier for contributors to get started quickly. Follow the steps below to set up NetBird in a dev container.
|
||||||
|
|
||||||
|
#### 1. Prerequisites:
|
||||||
|
|
||||||
|
* Install Docker on your machine: [Docker Installation Guide](https://docs.docker.com/get-docker/)
|
||||||
|
* Install Visual Studio Code: [VS Code Installation Guide](https://code.visualstudio.com/download)
|
||||||
|
* If you prefer JetBrains Goland please follow this [manual](https://www.jetbrains.com/help/go/connect-to-devcontainer.html)
|
||||||
|
|
||||||
|
#### 2. Clone the Repository:
|
||||||
|
|
||||||
|
Clone the repository following previous [Local NetBird setup](#local-netbird-setup).
|
||||||
|
|
||||||
|
#### 3. Open in project in IDE of your choice:
|
||||||
|
|
||||||
|
**VScode**:
|
||||||
|
|
||||||
|
Open the project folder in Visual Studio Code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
code .
|
||||||
|
```
|
||||||
|
|
||||||
|
When you open the project in VS Code, it will detect the presence of a dev container configuration.
|
||||||
|
Click on the green "Reopen in Container" button in the bottom-right corner of VS Code.
|
||||||
|
|
||||||
|
**Goland**:
|
||||||
|
|
||||||
|
Open GoLand and select `"File" > "Open"` to open the NetBird project folder.
|
||||||
|
GoLand will detect the dev container configuration and prompt you to open the project in the container. Accept the prompt.
|
||||||
|
|
||||||
|
#### 4. Wait for the Container to Build:
|
||||||
|
|
||||||
|
VsCode or GoLand will use the specified Docker image to build the dev container. This might take some time, depending on your internet connection.
|
||||||
|
|
||||||
|
#### 6. Development:
|
||||||
|
|
||||||
|
Once the container is built, you can start developing within the dev container. All the necessary dependencies and configurations are set up within the container.
|
||||||
|
|
||||||
|
|
||||||
### Build and start
|
### Build and start
|
||||||
#### Client
|
#### Client
|
||||||
|
|
||||||
> Windows clients have a Wireguard driver requirement. We provide a bash script that can be executed in WLS 2 with docker support [wireguard_nt.sh](/client/wireguard_nt.sh).
|
|
||||||
|
|
||||||
To start NetBird, execute:
|
To start NetBird, execute:
|
||||||
```
|
```
|
||||||
cd client
|
cd client
|
||||||
# bash wireguard_nt.sh # if windows
|
CGO_ENABLED=0 go build .
|
||||||
go build .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> Windows clients have a Wireguard driver requirement. You can download the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to the same path as your binary file or to `C:\Windows\System32\wintun.dll`.
|
||||||
|
|
||||||
|
> To test the client GUI application on Windows machines with RDP or vituralized environments (e.g. virtualbox or cloud), you need to download and extract the opengl32.dll from https://fdossena.com/?p=mesa/index.frag next to the built application.
|
||||||
|
|
||||||
To start NetBird the client in the foreground:
|
To start NetBird the client in the foreground:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -185,6 +228,42 @@ To start NetBird the management service:
|
|||||||
./management management --log-level debug --log-file console --config ./management.json
|
./management management --log-level debug --log-file console --config ./management.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Windows Netbird Installer
|
||||||
|
Create dist directory
|
||||||
|
```shell
|
||||||
|
mkdir -p dist/netbird_windows_amd64
|
||||||
|
```
|
||||||
|
|
||||||
|
UI client
|
||||||
|
```shell
|
||||||
|
CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -o netbird-ui.exe -ldflags "-s -w -H windowsgui" ./client/ui
|
||||||
|
mv netbird-ui.exe ./dist/netbird_windows_amd64/
|
||||||
|
```
|
||||||
|
|
||||||
|
Client
|
||||||
|
```shell
|
||||||
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o netbird.exe ./client/
|
||||||
|
mv netbird.exe ./dist/netbird_windows_amd64/
|
||||||
|
```
|
||||||
|
> Windows clients have a Wireguard driver requirement. You can download the wintun driver from https://www.wintun.net/builds/wintun-0.14.1.zip, after decompressing, you can copy the file `windtun\bin\ARCH\wintun.dll` to `./dist/netbird_windows_amd64/`.
|
||||||
|
|
||||||
|
NSIS compiler
|
||||||
|
- [Windows-nsis]( https://nsis.sourceforge.io/Download)
|
||||||
|
- [MacOS-makensis](https://formulae.brew.sh/formula/makensis#default)
|
||||||
|
- [Linux-makensis](https://manpages.ubuntu.com/manpages/trusty/man1/makensis.1.html)
|
||||||
|
|
||||||
|
NSIS Plugins. Download and move them to the NSIS plugins folder.
|
||||||
|
- [EnVar](https://nsis.sourceforge.io/mediawiki/images/7/7f/EnVar_plugin.zip)
|
||||||
|
- [ShellExecAsUser](https://nsis.sourceforge.io/mediawiki/images/6/68/ShellExecAsUser_amd64-Unicode.7z)
|
||||||
|
|
||||||
|
Windows Installer
|
||||||
|
```shell
|
||||||
|
export APPVER=0.0.0.1
|
||||||
|
makensis -V4 client/installer.nsis
|
||||||
|
```
|
||||||
|
|
||||||
|
The installer `netbird-installer.exe` will be created in root directory.
|
||||||
|
|
||||||
### Test suite
|
### Test suite
|
||||||
|
|
||||||
The tests can be started via:
|
The tests can be started via:
|
||||||
@@ -195,6 +274,8 @@ go test -exec sudo ./...
|
|||||||
```
|
```
|
||||||
> On Windows use a powershell with administrator privileges
|
> On Windows use a powershell with administrator privileges
|
||||||
|
|
||||||
|
> Non-GTK environments will need the `libayatana-appindicator3-dev` (debian/ubuntu) package installed
|
||||||
|
|
||||||
## Checklist before submitting a PR
|
## Checklist before submitting a PR
|
||||||
As a critical network service and open-source project, we must enforce a few things before submitting the pull-requests:
|
As a critical network service and open-source project, we must enforce a few things before submitting the pull-requests:
|
||||||
- Keep functions as simple as possible, with a single purpose
|
- Keep functions as simple as possible, with a single purpose
|
||||||
|
|||||||
98
README.md
98
README.md
@@ -1,6 +1,6 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>:hatching_chick: New Release! Peer expiration.</strong>
|
<strong>:hatching_chick: New Release! Device Posture Checks.</strong>
|
||||||
<a href="https://github.com/netbirdio/netbird/releases">
|
<a href="https://docs.netbird.io/how-to/manage-posture-checks">
|
||||||
Learn more
|
Learn more
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Start using NetBird at <a href="https://app.netbird.io/">app.netbird.io</a>
|
Start using NetBird at <a href="https://netbird.io/pricing">netbird.io</a>
|
||||||
<br/>
|
<br/>
|
||||||
See <a href="https://netbird.io/docs/">Documentation</a>
|
See <a href="https://netbird.io/docs/">Documentation</a>
|
||||||
<br/>
|
<br/>
|
||||||
@@ -36,69 +36,83 @@
|
|||||||
|
|
||||||
<br>
|
<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.**
|
**NetBird combines a configuration-free peer-to-peer private network and a centralized access control system in a single platform, 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.
|
**Connect.** NetBird creates a WireGuard-based overlay network that automatically connects your machines over an encrypted tunnel, leaving behind the hassle of opening ports, complex firewall rules, VPN gateways, and so forth.
|
||||||
|
|
||||||
NetBird uses [NAT traversal techniques](https://en.wikipedia.org/wiki/Interactive_Connectivity_Establishment) to automatically create an overlay peer-to-peer network connecting machines regardless of location (home, office, data center, container, cloud, or edge environments), unifying virtual private network management experience.
|
**Secure.** NetBird enables secure remote access by applying granular access policies while allowing you to manage them intuitively from a single place. Works universally on any infrastructure.
|
||||||
|
|
||||||
**Key features:**
|
### Open-Source Network Security in a Single Platform
|
||||||
- \[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.
|
|
||||||
- \[x] Private DNS.
|
|
||||||
- \[x] Network Activity Monitoring.
|
|
||||||
|
|
||||||
**Coming soon:**
|
|
||||||
- \[ ] Mobile clients.
|
|
||||||
|
|
||||||
### Secure peer-to-peer VPN with SSO and MFA in minutes
|

|
||||||
|
|
||||||
https://user-images.githubusercontent.com/700848/197345890-2e2cded5-7b7a-436f-a444-94e80dd24f46.mov
|
|
||||||
|
|
||||||
**Note**: The `main` branch may be in an *unstable or even broken state* during development.
|
### Key features
|
||||||
For stable versions, see [releases](https://github.com/netbirdio/netbird/releases).
|
|
||||||
|
|
||||||
### Start using NetBird
|
| Connectivity | Management | Security | Automation | Platforms |
|
||||||
- Hosted version: [https://app.netbird.io/](https://app.netbird.io/).
|
|------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|
|
||||||
- See our documentation for [Quickstart Guide](https://docs.netbird.io/how-to/getting-started).
|
| <ul><li> - \[x] Kernel WireGuard </ul></li> | <ul><li> - \[x] [Admin Web UI](https://github.com/netbirdio/dashboard) </ul></li> | <ul><li> - \[x] [SSO & MFA support](https://docs.netbird.io/how-to/installation#running-net-bird-with-sso-login) </ul></li> | <ul><li> - \[x] [Public API](https://docs.netbird.io/api) </ul></li> | <ul><li> - \[x] Linux </ul></li> |
|
||||||
- If you are looking to self-host NetBird, check our [Self-Hosting Guide](https://docs.netbird.io/selfhosted/selfhosted-guide).
|
| <ul><li> - \[x] Peer-to-peer connections </ul></li> | <ul><li> - \[x] Auto peer discovery and configuration </ul></li> | <ul><li> - \[x] [Access control - groups & rules](https://docs.netbird.io/how-to/manage-network-access) </ul></li> | <ul><li> - \[x] [Setup keys for bulk network provisioning](https://docs.netbird.io/how-to/register-machines-using-setup-keys) </ul></li> | <ul><li> - \[x] Mac </ul></li> |
|
||||||
- Step-by-step [Installation Guide](https://docs.netbird.io/how-to/getting-started#installation) for different platforms.
|
| <ul><li> - \[x] Connection relay fallback </ul></li> | <ul><li> - \[x] [IdP integrations](https://docs.netbird.io/selfhosted/identity-providers) </ul></li> | <ul><li> - \[x] [Activity logging](https://docs.netbird.io/how-to/monitor-system-and-network-activity) </ul></li> | <ul><li> - \[x] [Self-hosting quickstart script](https://docs.netbird.io/selfhosted/selfhosted-quickstart) </ul></li> | <ul><li> - \[x] Windows </ul></li> |
|
||||||
- Web UI [repository](https://github.com/netbirdio/dashboard).
|
| <ul><li> - \[x] [Routes to external networks](https://docs.netbird.io/how-to/routing-traffic-to-private-networks) </ul></li> | <ul><li> - \[x] [Private DNS](https://docs.netbird.io/how-to/manage-dns-in-your-network) </ul></li> | <ul><li> - \[x] [Device posture checks](https://docs.netbird.io/how-to/manage-posture-checks) </ul></li> | <ul><li> - \[x] IdP groups sync with JWT </ul></li> | <ul><li> - \[x] Android </ul></li> |
|
||||||
- 5 min [demo video](https://youtu.be/Tu9tPsUWaY0) on YouTube.
|
| <ul><li> - \[x] NAT traversal with BPF </ul></li> | <ul><li> - \[x] [Multiuser support](https://docs.netbird.io/how-to/add-users-to-your-network) </ul></li> | <ul><li> - \[x] Peer-to-peer encryption </ul></li> | | <ul><li> - \[x] iOS </ul></li> |
|
||||||
|
| | | <ul><li> - \[x] [Quantum-resistance with Rosenpass](https://netbird.io/knowledge-hub/the-first-quantum-resistant-mesh-vpn) </ul></li> | | <ul><li> - \[x] OpenWRT </ul></li> |
|
||||||
|
| | | <ui><li> - \[x] [Periodic re-authentication](https://docs.netbird.io/how-to/enforce-periodic-user-authentication)</ul></li> | | <ul><li> - \[x] [Serverless](https://docs.netbird.io/how-to/netbird-on-faas) </ul></li> |
|
||||||
|
| | | | | <ul><li> - \[x] Docker </ul></li> |
|
||||||
|
### Quickstart with NetBird Cloud
|
||||||
|
|
||||||
|
- Download and install NetBird at [https://app.netbird.io/install](https://app.netbird.io/install)
|
||||||
|
- Follow the steps to sign-up with Google, Microsoft, GitHub or your email address.
|
||||||
|
- Check NetBird [admin UI](https://app.netbird.io/).
|
||||||
|
- Add more machines.
|
||||||
|
|
||||||
|
### Quickstart with self-hosted NetBird
|
||||||
|
|
||||||
|
> This is the quickest way to try self-hosted NetBird. It should take around 5 minutes to get started if you already have a public domain and a VM.
|
||||||
|
Follow the [Advanced guide with a custom identity provider](https://docs.netbird.io/selfhosted/selfhosted-guide#advanced-guide-with-a-custom-identity-provider) for installations with different IDPs.
|
||||||
|
|
||||||
|
**Infrastructure requirements:**
|
||||||
|
- A Linux VM with at least **1CPU** and **2GB** of memory.
|
||||||
|
- The VM should be publicly accessible on TCP ports **80** and **443** and UDP ports: **3478**, **49152-65535**.
|
||||||
|
- **Public domain** name pointing to the VM.
|
||||||
|
|
||||||
|
**Software requirements:**
|
||||||
|
- Docker installed on the VM with the docker-compose plugin ([Docker installation guide](https://docs.docker.com/engine/install/)) or docker with docker-compose in version 2 or higher.
|
||||||
|
- [jq](https://jqlang.github.io/jq/) installed. In most distributions
|
||||||
|
Usually available in the official repositories and can be installed with `sudo apt install jq` or `sudo yum install jq`
|
||||||
|
- [curl](https://curl.se/) installed.
|
||||||
|
Usually available in the official repositories and can be installed with `sudo apt install curl` or `sudo yum install curl`
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
- Download and run the installation script:
|
||||||
|
```bash
|
||||||
|
export NETBIRD_DOMAIN=netbird.example.com; curl -fsSL https://github.com/netbirdio/netbird/releases/latest/download/getting-started-with-zitadel.sh | bash
|
||||||
|
```
|
||||||
|
- Once finished, you can manage the resources via `docker-compose`
|
||||||
|
|
||||||
### A bit on NetBird internals
|
### A bit on NetBird internals
|
||||||
- Every machine in the network runs [NetBird Agent (or Client)](client/) that manages WireGuard.
|
- 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).
|
- 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.
|
- 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.
|
- Connection candidates are discovered with the help of [STUN](https://en.wikipedia.org/wiki/STUN) servers.
|
||||||
- Agents negotiate a connection through [Signal Service](signal/) passing p2p encrypted messages with candidates.
|
- 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.
|
- Sometimes the NAT traversal is unsuccessful due to strict NATs (e.g. mobile carrier-grade NAT) and a 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.
|
[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">
|
<p float="left" align="middle">
|
||||||
<img src="https://netbird.io/docs/img/architecture/high-level-dia.png" width="700"/>
|
<img src="https://docs.netbird.io/docs-static/img/architecture/high-level-dia.png" width="700"/>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
See a complete [architecture overview](https://docs.netbird.io/about-netbird/how-netbird-works#architecture) for details.
|
||||||
|
|
||||||
### Roadmap
|
|
||||||
- [Public Roadmap](https://github.com/netbirdio/netbird/projects/2)
|
|
||||||
|
|
||||||
### Community projects
|
### Community projects
|
||||||
- [NetBird on OpenWRT](https://github.com/messense/openwrt-netbird)
|
|
||||||
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
- [NetBird installer script](https://github.com/physk/netbird-installer)
|
||||||
|
- [NetBird ansible collection by Dominion Solutions](https://galaxy.ansible.com/ui/repo/published/dominion_solutions/netbird/)
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
### Support acknowledgement
|
### Support acknowledgement
|
||||||
|
|
||||||
@@ -107,7 +121,7 @@ In November 2022, NetBird joined the [StartUpSecure program](https://www.forschu
|
|||||||

|

|
||||||
|
|
||||||
### Testimonials
|
### 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).
|
We use open-source technologies like [WireGuard®](https://www.wireguard.com/), [Pion ICE (WebRTC)](https://github.com/pion/ice), [Coturn](https://github.com/coturn/coturn), and [Rosenpass](https://rosenpass.eu). 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., by giving a star or a contribution).
|
||||||
|
|
||||||
### Legal
|
### Legal
|
||||||
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
_WireGuard_ and the _WireGuard_ logo are [registered trademarks](https://www.wireguard.com/trademark-policy/) of Jason A. Donenfeld.
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ func Encode(num uint32) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var encoded strings.Builder
|
var encoded strings.Builder
|
||||||
remainder := uint32(0)
|
|
||||||
|
|
||||||
for num > 0 {
|
for num > 0 {
|
||||||
remainder = num % base
|
remainder := num % base
|
||||||
encoded.WriteByte(alphabet[remainder])
|
encoded.WriteByte(alphabet[remainder])
|
||||||
num /= base
|
num /= base
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
FROM gcr.io/distroless/base:debug
|
FROM alpine:3.19
|
||||||
|
RUN apk add --no-cache ca-certificates iptables ip6tables
|
||||||
ENV NB_FOREGROUND_MODE=true
|
ENV NB_FOREGROUND_MODE=true
|
||||||
ENV PATH=/sbin:/usr/sbin:/bin:/usr/bin:/busybox
|
ENTRYPOINT [ "/usr/local/bin/netbird","up"]
|
||||||
SHELL ["/busybox/sh","-c"]
|
COPY netbird /usr/local/bin/netbird
|
||||||
RUN sed -i -E 's/(^root:.+)\/sbin\/nologin/\1\/busybox\/sh/g' /etc/passwd
|
|
||||||
ENTRYPOINT [ "/go/bin/netbird","up"]
|
|
||||||
COPY netbird /go/bin/netbird
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
package android
|
package android
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,12 +10,13 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
"github.com/netbirdio/netbird/formatter"
|
"github.com/netbirdio/netbird/formatter"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
"github.com/netbirdio/netbird/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionListener export internal Listener for mobile
|
// ConnectionListener export internal Listener for mobile
|
||||||
@@ -31,9 +34,9 @@ type IFaceDiscover interface {
|
|||||||
stdnet.ExternalIFaceDiscover
|
stdnet.ExternalIFaceDiscover
|
||||||
}
|
}
|
||||||
|
|
||||||
// RouteListener export internal RouteListener for mobile
|
// NetworkChangeListener export internal NetworkChangeListener for mobile
|
||||||
type RouteListener interface {
|
type NetworkChangeListener interface {
|
||||||
routemanager.RouteListener
|
listener.NetworkChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// DnsReadyListener export internal dns ReadyListener for mobile
|
// DnsReadyListener export internal dns ReadyListener for mobile
|
||||||
@@ -54,20 +57,22 @@ type Client struct {
|
|||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
ctxCancelLock *sync.Mutex
|
ctxCancelLock *sync.Mutex
|
||||||
deviceName string
|
deviceName string
|
||||||
routeListener routemanager.RouteListener
|
uiVersion string
|
||||||
onHostDnsFn func([]string)
|
networkChangeListener listener.NetworkChangeListener
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient instantiate a new Client
|
// NewClient instantiate a new Client
|
||||||
func NewClient(cfgFile, deviceName string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, routeListener RouteListener) *Client {
|
func NewClient(cfgFile, deviceName string, uiVersion string, tunAdapter TunAdapter, iFaceDiscover IFaceDiscover, networkChangeListener NetworkChangeListener) *Client {
|
||||||
|
net.SetAndroidProtectSocketFn(tunAdapter.ProtectSocket)
|
||||||
return &Client{
|
return &Client{
|
||||||
cfgFile: cfgFile,
|
cfgFile: cfgFile,
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
|
uiVersion: uiVersion,
|
||||||
tunAdapter: tunAdapter,
|
tunAdapter: tunAdapter,
|
||||||
iFaceDiscover: iFaceDiscover,
|
iFaceDiscover: iFaceDiscover,
|
||||||
recorder: peer.NewRecorder(""),
|
recorder: peer.NewRecorder(""),
|
||||||
ctxCancelLock: &sync.Mutex{},
|
ctxCancelLock: &sync.Mutex{},
|
||||||
routeListener: routeListener,
|
networkChangeListener: networkChangeListener,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,10 +85,14 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
|
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
|
||||||
|
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
|
||||||
|
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
//nolint
|
//nolint
|
||||||
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
||||||
|
//nolint
|
||||||
|
ctxWithValues = context.WithValue(ctxWithValues, system.UiVersionCtxKey, c.uiVersion)
|
||||||
|
|
||||||
c.ctxCancelLock.Lock()
|
c.ctxCancelLock.Lock()
|
||||||
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
||||||
defer c.ctxCancel()
|
defer c.ctxCancel()
|
||||||
@@ -97,8 +106,34 @@ func (c *Client) Run(urlOpener URLOpener, dns *DNSList, dnsReadyListener DnsRead
|
|||||||
|
|
||||||
// todo do not throw error in case of cancelled context
|
// todo do not throw error in case of cancelled context
|
||||||
ctx = internal.CtxInitState(ctx)
|
ctx = internal.CtxInitState(ctx)
|
||||||
c.onHostDnsFn = func([]string) {}
|
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
return internal.RunClientMobile(ctx, cfg, c.recorder, c.tunAdapter, c.iFaceDiscover, c.routeListener, dns.items, dnsReadyListener)
|
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunWithoutLogin we apply this type of run function when the backed has been started without UI (i.e. after reboot).
|
||||||
|
// In this case make no sense handle registration steps.
|
||||||
|
func (c *Client) RunWithoutLogin(dns *DNSList, dnsReadyListener DnsReadyListener) error {
|
||||||
|
cfg, err := internal.UpdateOrCreateConfig(internal.ConfigInput{
|
||||||
|
ConfigPath: c.cfgFile,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.recorder.UpdateManagementAddress(cfg.ManagementURL.String())
|
||||||
|
c.recorder.UpdateRosenpass(cfg.RosenpassEnabled, cfg.RosenpassPermissive)
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
//nolint
|
||||||
|
ctxWithValues := context.WithValue(context.Background(), system.DeviceNameCtxKey, c.deviceName)
|
||||||
|
c.ctxCancelLock.Lock()
|
||||||
|
ctx, c.ctxCancel = context.WithCancel(ctxWithValues)
|
||||||
|
defer c.ctxCancel()
|
||||||
|
c.ctxCancelLock.Unlock()
|
||||||
|
|
||||||
|
// todo do not throw error in case of cancelled context
|
||||||
|
ctx = internal.CtxInitState(ctx)
|
||||||
|
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
|
return connectClient.RunOnAndroid(c.tunAdapter, c.iFaceDiscover, c.networkChangeListener, dns.items, dnsReadyListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the internal client and free the resources
|
// Stop the internal client and free the resources
|
||||||
@@ -117,6 +152,11 @@ func (c *Client) SetTraceLogLevel() {
|
|||||||
log.SetLevel(log.TraceLevel)
|
log.SetLevel(log.TraceLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetInfoLogLevel configure the logger to info level
|
||||||
|
func (c *Client) SetInfoLogLevel() {
|
||||||
|
log.SetLevel(log.InfoLevel)
|
||||||
|
}
|
||||||
|
|
||||||
// PeersList return with the list of the PeerInfos
|
// PeersList return with the list of the PeerInfos
|
||||||
func (c *Client) PeersList() *PeerInfoArray {
|
func (c *Client) PeersList() *PeerInfoArray {
|
||||||
|
|
||||||
|
|||||||
@@ -84,10 +84,14 @@ func (a *Auth) SaveConfigIfSSOSupported(listener SSOListener) {
|
|||||||
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
|
func (a *Auth) saveConfigIfSSOSupported() (bool, error) {
|
||||||
supportsSSO := true
|
supportsSSO := true
|
||||||
err := a.withBackOff(a.ctx, func() (err error) {
|
err := a.withBackOff(a.ctx, func() (err error) {
|
||||||
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
|
||||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
|
|
||||||
_, err = internal.GetPKCEAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
_, err = internal.GetPKCEAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
||||||
if s, ok := gstatus.FromError(err); ok && s.Code() == codes.NotFound {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.NotFound || s.Code() == codes.Unimplemented) {
|
||||||
|
_, err = internal.GetDeviceAuthorizationFlowInfo(a.ctx, a.config.PrivateKey, a.config.ManagementURL)
|
||||||
|
s, ok := gstatus.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s.Code() == codes.NotFound || s.Code() == codes.Unimplemented {
|
||||||
supportsSSO = false
|
supportsSSO = false
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
@@ -189,7 +193,7 @@ func (a *Auth) login(urlOpener URLOpener) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) {
|
func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, error) {
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config)
|
oAuthFlow, err := auth.NewOAuthFlow(a.ctx, a.config, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -201,8 +205,8 @@ func (a *Auth) foregroundGetTokenInfo(urlOpener URLOpener) (*auth.TokenInfo, err
|
|||||||
|
|
||||||
go urlOpener.Open(flowInfo.VerificationURIComplete)
|
go urlOpener.Open(flowInfo.VerificationURIComplete)
|
||||||
|
|
||||||
waitTimeout := time.Duration(flowInfo.ExpiresIn)
|
waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second
|
||||||
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout*time.Second)
|
waitCTX, cancel := context.WithTimeout(a.ctx, waitTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo)
|
tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ func TestPreferences_ReadUncommitedValues(t *testing.T) {
|
|||||||
p.SetManagementURL(exampleString)
|
p.SetManagementURL(exampleString)
|
||||||
resp, err = p.GetManagementURL()
|
resp, err = p.GetManagementURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read managmenet url: %s", err)
|
t.Fatalf("failed to read management url: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != exampleString {
|
if resp != exampleString {
|
||||||
t.Errorf("unexpected managemenet url: %s", resp)
|
t.Errorf("unexpected management url: %s", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
p.SetPreSharedKey(exampleString)
|
p.SetPreSharedKey(exampleString)
|
||||||
@@ -102,11 +102,11 @@ func TestPreferences_Commit(t *testing.T) {
|
|||||||
|
|
||||||
resp, err = p.GetManagementURL()
|
resp, err = p.GetManagementURL()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read managmenet url: %s", err)
|
t.Fatalf("failed to read management url: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp != exampleURL {
|
if resp != exampleURL {
|
||||||
t.Errorf("unexpected managemenet url: %s", resp)
|
t.Errorf("unexpected management url: %s", resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err = p.GetPreSharedKey()
|
resp, err = p.GetPreSharedKey()
|
||||||
|
|||||||
212
client/anonymize/anonymize.go
Normal file
212
client/anonymize/anonymize.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package anonymize
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Anonymizer struct {
|
||||||
|
ipAnonymizer map[netip.Addr]netip.Addr
|
||||||
|
domainAnonymizer map[string]string
|
||||||
|
currentAnonIPv4 netip.Addr
|
||||||
|
currentAnonIPv6 netip.Addr
|
||||||
|
startAnonIPv4 netip.Addr
|
||||||
|
startAnonIPv6 netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultAddresses() (netip.Addr, netip.Addr) {
|
||||||
|
// 192.51.100.0, 100::
|
||||||
|
return netip.AddrFrom4([4]byte{198, 51, 100, 0}), netip.AddrFrom16([16]byte{0x01})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnonymizer(startIPv4, startIPv6 netip.Addr) *Anonymizer {
|
||||||
|
return &Anonymizer{
|
||||||
|
ipAnonymizer: map[netip.Addr]netip.Addr{},
|
||||||
|
domainAnonymizer: map[string]string{},
|
||||||
|
currentAnonIPv4: startIPv4,
|
||||||
|
currentAnonIPv6: startIPv6,
|
||||||
|
startAnonIPv4: startIPv4,
|
||||||
|
startAnonIPv6: startIPv6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeIP(ip netip.Addr) netip.Addr {
|
||||||
|
if ip.IsLoopback() ||
|
||||||
|
ip.IsLinkLocalUnicast() ||
|
||||||
|
ip.IsLinkLocalMulticast() ||
|
||||||
|
ip.IsInterfaceLocalMulticast() ||
|
||||||
|
ip.IsPrivate() ||
|
||||||
|
ip.IsUnspecified() ||
|
||||||
|
ip.IsMulticast() ||
|
||||||
|
isWellKnown(ip) ||
|
||||||
|
a.isInAnonymizedRange(ip) {
|
||||||
|
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := a.ipAnonymizer[ip]; !ok {
|
||||||
|
if ip.Is4() {
|
||||||
|
a.ipAnonymizer[ip] = a.currentAnonIPv4
|
||||||
|
a.currentAnonIPv4 = a.currentAnonIPv4.Next()
|
||||||
|
} else {
|
||||||
|
a.ipAnonymizer[ip] = a.currentAnonIPv6
|
||||||
|
a.currentAnonIPv6 = a.currentAnonIPv6.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.ipAnonymizer[ip]
|
||||||
|
}
|
||||||
|
|
||||||
|
// isInAnonymizedRange checks if an IP is within the range of already assigned anonymized IPs
|
||||||
|
func (a *Anonymizer) isInAnonymizedRange(ip netip.Addr) bool {
|
||||||
|
if ip.Is4() && ip.Compare(a.startAnonIPv4) >= 0 && ip.Compare(a.currentAnonIPv4) <= 0 {
|
||||||
|
return true
|
||||||
|
} else if !ip.Is4() && ip.Compare(a.startAnonIPv6) >= 0 && ip.Compare(a.currentAnonIPv6) <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeIPString(ip string) string {
|
||||||
|
addr, err := netip.ParseAddr(ip)
|
||||||
|
if err != nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.AnonymizeIP(addr).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeDomain(domain string) string {
|
||||||
|
if strings.HasSuffix(domain, "netbird.io") ||
|
||||||
|
strings.HasSuffix(domain, "netbird.selfhosted") ||
|
||||||
|
strings.HasSuffix(domain, "netbird.cloud") ||
|
||||||
|
strings.HasSuffix(domain, "netbird.stage") ||
|
||||||
|
strings.HasSuffix(domain, ".domain") {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(domain, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDomain := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
||||||
|
|
||||||
|
anonymized, ok := a.domainAnonymizer[baseDomain]
|
||||||
|
if !ok {
|
||||||
|
anonymizedBase := "anon-" + generateRandomString(5) + ".domain"
|
||||||
|
a.domainAnonymizer[baseDomain] = anonymizedBase
|
||||||
|
anonymized = anonymizedBase
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Replace(domain, baseDomain, anonymized, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeURI(uri string) string {
|
||||||
|
u, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
|
||||||
|
var anonymizedHost string
|
||||||
|
if u.Opaque != "" {
|
||||||
|
host, port, err := net.SplitHostPort(u.Opaque)
|
||||||
|
if err == nil {
|
||||||
|
anonymizedHost = fmt.Sprintf("%s:%s", a.AnonymizeDomain(host), port)
|
||||||
|
} else {
|
||||||
|
anonymizedHost = a.AnonymizeDomain(u.Opaque)
|
||||||
|
}
|
||||||
|
u.Opaque = anonymizedHost
|
||||||
|
} else if u.Host != "" {
|
||||||
|
host, port, err := net.SplitHostPort(u.Host)
|
||||||
|
if err == nil {
|
||||||
|
anonymizedHost = fmt.Sprintf("%s:%s", a.AnonymizeDomain(host), port)
|
||||||
|
} else {
|
||||||
|
anonymizedHost = a.AnonymizeDomain(u.Host)
|
||||||
|
}
|
||||||
|
u.Host = anonymizedHost
|
||||||
|
}
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Anonymizer) AnonymizeString(str string) string {
|
||||||
|
ipv4Regex := regexp.MustCompile(`\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b`)
|
||||||
|
ipv6Regex := regexp.MustCompile(`\b([0-9a-fA-F:]+:+[0-9a-fA-F]{0,4})(?:%[0-9a-zA-Z]+)?(?:\/[0-9]{1,3})?(?::[0-9]{1,5})?\b`)
|
||||||
|
|
||||||
|
str = ipv4Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString)
|
||||||
|
str = ipv6Regex.ReplaceAllStringFunc(str, a.AnonymizeIPString)
|
||||||
|
|
||||||
|
for domain, anonDomain := range a.domainAnonymizer {
|
||||||
|
str = strings.ReplaceAll(str, domain, anonDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
str = a.AnonymizeSchemeURI(str)
|
||||||
|
str = a.AnonymizeDNSLogLine(str)
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonymizeSchemeURI finds and anonymizes URIs with stun, stuns, turn, and turns schemes.
|
||||||
|
func (a *Anonymizer) AnonymizeSchemeURI(text string) string {
|
||||||
|
re := regexp.MustCompile(`(?i)\b(stuns?:|turns?:|https?://)\S+\b`)
|
||||||
|
|
||||||
|
return re.ReplaceAllStringFunc(text, a.AnonymizeURI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnonymizeDNSLogLine anonymizes domain names in DNS log entries by replacing them with a random string.
|
||||||
|
func (a *Anonymizer) AnonymizeDNSLogLine(logEntry string) string {
|
||||||
|
domainPattern := `dns\.Question{Name:"([^"]+)",`
|
||||||
|
domainRegex := regexp.MustCompile(domainPattern)
|
||||||
|
|
||||||
|
return domainRegex.ReplaceAllStringFunc(logEntry, func(match string) string {
|
||||||
|
parts := strings.Split(match, `"`)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
domain := parts[1]
|
||||||
|
if strings.HasSuffix(domain, ".domain") {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
randomDomain := generateRandomString(10) + ".domain"
|
||||||
|
return strings.Replace(match, domain, randomDomain, 1)
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWellKnown(addr netip.Addr) bool {
|
||||||
|
wellKnown := []string{
|
||||||
|
"8.8.8.8", "8.8.4.4", // Google DNS IPv4
|
||||||
|
"2001:4860:4860::8888", "2001:4860:4860::8844", // Google DNS IPv6
|
||||||
|
"1.1.1.1", "1.0.0.1", // Cloudflare DNS IPv4
|
||||||
|
"2606:4700:4700::1111", "2606:4700:4700::1001", // Cloudflare DNS IPv6
|
||||||
|
"9.9.9.9", "149.112.112.112", // Quad9 DNS IPv4
|
||||||
|
"2620:fe::fe", "2620:fe::9", // Quad9 DNS IPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(wellKnown, addr.String()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
cgnatRangeStart := netip.AddrFrom4([4]byte{100, 64, 0, 0})
|
||||||
|
cgnatRange := netip.PrefixFrom(cgnatRangeStart, 10)
|
||||||
|
|
||||||
|
return cgnatRange.Contains(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomString(length int) string {
|
||||||
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
result := make([]byte, length)
|
||||||
|
for i := range result {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[i] = letters[num.Int64()]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
223
client/anonymize/anonymize_test.go
Normal file
223
client/anonymize/anonymize_test.go
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
package anonymize_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAnonymizeIP(t *testing.T) {
|
||||||
|
startIPv4 := netip.MustParseAddr("198.51.100.0")
|
||||||
|
startIPv6 := netip.MustParseAddr("100::")
|
||||||
|
anonymizer := anonymize.NewAnonymizer(startIPv4, startIPv6)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{"Well known", "8.8.8.8", "8.8.8.8"},
|
||||||
|
{"First Public IPv4", "1.2.3.4", "198.51.100.0"},
|
||||||
|
{"Second Public IPv4", "4.3.2.1", "198.51.100.1"},
|
||||||
|
{"Repeated IPv4", "1.2.3.4", "198.51.100.0"},
|
||||||
|
{"Private IPv4", "192.168.1.1", "192.168.1.1"},
|
||||||
|
{"First Public IPv6", "2607:f8b0:4005:805::200e", "100::"},
|
||||||
|
{"Second Public IPv6", "a::b", "100::1"},
|
||||||
|
{"Repeated IPv6", "2607:f8b0:4005:805::200e", "100::"},
|
||||||
|
{"Private IPv6", "fe80::1", "fe80::1"},
|
||||||
|
{"In Range IPv4", "198.51.100.2", "198.51.100.2"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
ip := netip.MustParseAddr(tc.ip)
|
||||||
|
anonymizedIP := anonymizer.AnonymizeIP(ip)
|
||||||
|
if anonymizedIP.String() != tc.expect {
|
||||||
|
t.Errorf("%s: expected %s, got %s", tc.name, tc.expect, anonymizedIP)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeDNSLogLine(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
testLog := `2024-04-23T20:01:11+02:00 TRAC client/internal/dns/local.go:25: received question: dns.Question{Name:"example.com", Qtype:0x1c, Qclass:0x1}`
|
||||||
|
|
||||||
|
result := anonymizer.AnonymizeDNSLogLine(testLog)
|
||||||
|
require.NotEqual(t, testLog, result)
|
||||||
|
assert.NotContains(t, result, "example.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeDomain(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
domain string
|
||||||
|
expectPattern string
|
||||||
|
shouldAnonymize bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"General Domain",
|
||||||
|
"example.com",
|
||||||
|
`^anon-[a-zA-Z0-9]+\.domain$`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Subdomain",
|
||||||
|
"sub.example.com",
|
||||||
|
`^sub\.anon-[a-zA-Z0-9]+\.domain$`,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Protected Domain",
|
||||||
|
"netbird.io",
|
||||||
|
`^netbird\.io$`,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeDomain(tc.domain)
|
||||||
|
if tc.shouldAnonymize {
|
||||||
|
assert.Regexp(t, tc.expectPattern, result, "The anonymized domain should match the expected pattern")
|
||||||
|
assert.NotContains(t, result, tc.domain, "The original domain should not be present in the result")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, tc.domain, result, "Protected domains should not be anonymized")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeURI(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uri string
|
||||||
|
regex string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"HTTP URI with Port",
|
||||||
|
"http://example.com:80/path",
|
||||||
|
`^http://anon-[a-zA-Z0-9]+\.domain:80/path$`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"HTTP URI without Port",
|
||||||
|
"http://example.com/path",
|
||||||
|
`^http://anon-[a-zA-Z0-9]+\.domain/path$`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Opaque URI with Port",
|
||||||
|
"stun:example.com:80?transport=udp",
|
||||||
|
`^stun:anon-[a-zA-Z0-9]+\.domain:80\?transport=udp$`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Opaque URI without Port",
|
||||||
|
"stun:example.com?transport=udp",
|
||||||
|
`^stun:anon-[a-zA-Z0-9]+\.domain\?transport=udp$`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeURI(tc.uri)
|
||||||
|
assert.Regexp(t, regexp.MustCompile(tc.regex), result, "URI should match expected pattern")
|
||||||
|
require.NotContains(t, result, "example.com", "Original domain should not be present")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeSchemeURI(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{"STUN URI in text", "Connection made via stun:example.com", `Connection made via stun:anon-[a-zA-Z0-9]+\.domain`},
|
||||||
|
{"TURN URI in log", "Failed attempt turn:some.example.com:3478?transport=tcp: retrying", `Failed attempt turn:some.anon-[a-zA-Z0-9]+\.domain:3478\?transport=tcp: retrying`},
|
||||||
|
{"HTTPS URI in message", "Visit https://example.com for more", `Visit https://anon-[a-zA-Z0-9]+\.domain for more`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeSchemeURI(tc.input)
|
||||||
|
assert.Regexp(t, tc.expect, result, "The anonymized output should match expected pattern")
|
||||||
|
require.NotContains(t, result, "example.com", "Original domain should not be present")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizString_MemorizedDomain(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
domain := "example.com"
|
||||||
|
anonymizedDomain := anonymizer.AnonymizeDomain(domain)
|
||||||
|
|
||||||
|
sampleString := "This is a test string including the domain example.com which should be anonymized."
|
||||||
|
|
||||||
|
firstPassResult := anonymizer.AnonymizeString(sampleString)
|
||||||
|
secondPassResult := anonymizer.AnonymizeString(firstPassResult)
|
||||||
|
|
||||||
|
assert.Contains(t, firstPassResult, anonymizedDomain, "The domain should be anonymized in the first pass")
|
||||||
|
assert.NotContains(t, firstPassResult, domain, "The original domain should not appear in the first pass output")
|
||||||
|
|
||||||
|
assert.Equal(t, firstPassResult, secondPassResult, "The second pass should not further anonymize the string")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeString_DoubleURI(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(netip.Addr{}, netip.Addr{})
|
||||||
|
domain := "example.com"
|
||||||
|
anonymizedDomain := anonymizer.AnonymizeDomain(domain)
|
||||||
|
|
||||||
|
sampleString := "Check out our site at https://example.com for more info."
|
||||||
|
|
||||||
|
firstPassResult := anonymizer.AnonymizeString(sampleString)
|
||||||
|
secondPassResult := anonymizer.AnonymizeString(firstPassResult)
|
||||||
|
|
||||||
|
assert.Contains(t, firstPassResult, "https://"+anonymizedDomain, "The URI should be anonymized in the first pass")
|
||||||
|
assert.NotContains(t, firstPassResult, "https://example.com", "The original URI should not appear in the first pass output")
|
||||||
|
|
||||||
|
assert.Equal(t, firstPassResult, secondPassResult, "The second pass should not further anonymize the URI")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAnonymizeString_IPAddresses(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "IPv4 Address",
|
||||||
|
input: "Error occurred at IP 122.138.1.1",
|
||||||
|
expect: "Error occurred at IP 198.51.100.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 Address",
|
||||||
|
input: "Access attempted from 2001:db8::ff00:42",
|
||||||
|
expect: "Access attempted from 100::",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 Address with Port",
|
||||||
|
input: "Access attempted from [2001:db8::ff00:42]:8080",
|
||||||
|
expect: "Access attempted from [100::]:8080",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Both IPv4 and IPv6",
|
||||||
|
input: "IPv4: 142.108.0.1 and IPv6: 2001:db8::ff00:43",
|
||||||
|
expect: "IPv4: 198.51.100.1 and IPv6: 100::1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := anonymizer.AnonymizeString(tc.input)
|
||||||
|
assert.Equal(t, tc.expect, result, "IP addresses should be anonymized correctly")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
255
client/cmd/debug.go
Normal file
255
client/cmd/debug.go
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
"github.com/netbirdio/netbird/client/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var debugCmd = &cobra.Command{
|
||||||
|
Use: "debug",
|
||||||
|
Short: "Debugging commands",
|
||||||
|
Long: "Provides commands for debugging and logging control within the Netbird daemon.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var debugBundleCmd = &cobra.Command{
|
||||||
|
Use: "bundle",
|
||||||
|
Example: " netbird debug bundle",
|
||||||
|
Short: "Create a debug bundle",
|
||||||
|
Long: "Generates a compressed archive of the daemon's logs and status for debugging purposes.",
|
||||||
|
RunE: debugBundle,
|
||||||
|
}
|
||||||
|
|
||||||
|
var logCmd = &cobra.Command{
|
||||||
|
Use: "log",
|
||||||
|
Short: "Manage logging for the Netbird daemon",
|
||||||
|
Long: `Commands to manage logging settings for the Netbird daemon, including ICE, gRPC, and general log levels.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var logLevelCmd = &cobra.Command{
|
||||||
|
Use: "level <level>",
|
||||||
|
Short: "Set the logging level for this session",
|
||||||
|
Long: `Sets the logging level for the current session. This setting is temporary and will revert to the default on daemon restart.
|
||||||
|
Available log levels are:
|
||||||
|
panic: for panic level, highest level of severity
|
||||||
|
fatal: for fatal level errors that cause the program to exit
|
||||||
|
error: for error conditions
|
||||||
|
warn: for warning conditions
|
||||||
|
info: for informational messages
|
||||||
|
debug: for debug-level messages
|
||||||
|
trace: for trace-level messages, which include more fine-grained information than debug`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: setLogLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
var forCmd = &cobra.Command{
|
||||||
|
Use: "for <time>",
|
||||||
|
Short: "Run debug logs for a specified duration and create a debug bundle",
|
||||||
|
Long: `Sets the logging level to trace, runs for the specified duration, and then generates a debug bundle.`,
|
||||||
|
Example: " netbird debug for 5m",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
RunE: runForDuration,
|
||||||
|
}
|
||||||
|
|
||||||
|
func debugBundle(cmd *cobra.Command, _ []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
|
||||||
|
Anonymize: anonymizeFlag,
|
||||||
|
Status: getStatusOutput(cmd),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println(resp.GetPath())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setLogLevel(cmd *cobra.Command, args []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
level := server.ParseLogLevel(args[0])
|
||||||
|
if level == proto.LogLevel_UNKNOWN {
|
||||||
|
return fmt.Errorf("unknown log level: %s. Available levels are: panic, fatal, error, warn, info, debug, trace\n", args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{
|
||||||
|
Level: level,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set log level: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Log level set successfully to", args[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runForDuration(cmd *cobra.Command, args []string) error {
|
||||||
|
duration, err := time.ParseDuration(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid duration format: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
|
||||||
|
stat, err := client.Status(cmd.Context(), &proto.StatusRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get status: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreUp := stat.Status == string(internal.StatusConnected) || stat.Status == string(internal.StatusConnecting)
|
||||||
|
|
||||||
|
initialLogLevel, err := client.GetLogLevel(cmd.Context(), &proto.GetLogLevelRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get log level: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird down")
|
||||||
|
|
||||||
|
initialLevelTrace := initialLogLevel.GetLevel() >= proto.LogLevel_TRACE
|
||||||
|
if !initialLevelTrace {
|
||||||
|
_, err = client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{
|
||||||
|
Level: proto.LogLevel_TRACE,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to set log level to TRACE: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Log level set to trace.")
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird up")
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
headerPostUp := fmt.Sprintf("----- Netbird post-up - Timestamp: %s", time.Now().Format(time.RFC3339))
|
||||||
|
statusOutput := fmt.Sprintf("%s\n%s", headerPostUp, getStatusOutput(cmd))
|
||||||
|
|
||||||
|
if waitErr := waitForDurationOrCancel(cmd.Context(), duration, cmd); waitErr != nil {
|
||||||
|
return waitErr
|
||||||
|
}
|
||||||
|
cmd.Println("\nDuration completed")
|
||||||
|
|
||||||
|
headerPreDown := fmt.Sprintf("----- Netbird pre-down - Timestamp: %s - Duration: %s", time.Now().Format(time.RFC3339), duration)
|
||||||
|
statusOutput = fmt.Sprintf("%s\n%s\n%s", statusOutput, headerPreDown, getStatusOutput(cmd))
|
||||||
|
|
||||||
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to down: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird down")
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
if restoreUp {
|
||||||
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
||||||
|
return fmt.Errorf("failed to up: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Netbird up")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !initialLevelTrace {
|
||||||
|
if _, err := client.SetLogLevel(cmd.Context(), &proto.SetLogLevelRequest{Level: initialLogLevel.GetLevel()}); err != nil {
|
||||||
|
return fmt.Errorf("failed to restore log level: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
cmd.Println("Log level restored to", initialLogLevel.GetLevel())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Creating debug bundle...")
|
||||||
|
|
||||||
|
resp, err := client.DebugBundle(cmd.Context(), &proto.DebugBundleRequest{
|
||||||
|
Anonymize: anonymizeFlag,
|
||||||
|
Status: statusOutput,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println(resp.GetPath())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStatusOutput(cmd *cobra.Command) string {
|
||||||
|
var statusOutputString string
|
||||||
|
statusResp, err := getStatus(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrf("Failed to get status: %v\n", err)
|
||||||
|
} else {
|
||||||
|
statusOutputString = parseToFullDetailSummary(convertToStatusOutputOverview(statusResp))
|
||||||
|
}
|
||||||
|
return statusOutputString
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForDurationOrCancel(ctx context.Context, duration time.Duration, cmd *cobra.Command) error {
|
||||||
|
ticker := time.NewTicker(1 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
elapsed := time.Since(startTime)
|
||||||
|
if elapsed >= duration {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
remaining := duration - elapsed
|
||||||
|
cmd.Printf("\rRemaining time: %s", formatDuration(remaining))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatDuration(d time.Duration) string {
|
||||||
|
d = d.Round(time.Second)
|
||||||
|
h := d / time.Hour
|
||||||
|
d %= time.Hour
|
||||||
|
m := d / time.Minute
|
||||||
|
d %= time.Minute
|
||||||
|
s := d / time.Second
|
||||||
|
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,20 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/netbirdio/netbird/client/internal/auth"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/skratchdot/open-golang/open"
|
"github.com/skratchdot/open-golang/open"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/auth"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var loginCmd = &cobra.Command{
|
var loginCmd = &cobra.Command{
|
||||||
@@ -52,7 +51,7 @@ var loginCmd = &cobra.Command{
|
|||||||
AdminURL: adminURL,
|
AdminURL: adminURL,
|
||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
}
|
}
|
||||||
if preSharedKey != "" {
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
ic.PreSharedKey = &preSharedKey
|
ic.PreSharedKey = &preSharedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ var loginCmd = &cobra.Command{
|
|||||||
return fmt.Errorf("get config file: %v", err)
|
return fmt.Errorf("get config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, setupKey)
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -83,8 +82,13 @@ var loginCmd = &cobra.Command{
|
|||||||
|
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: setupKey,
|
SetupKey: setupKey,
|
||||||
PreSharedKey: preSharedKey,
|
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
|
IsLinuxDesktopClient: isLinuxRunningDesktop(),
|
||||||
|
Hostname: hostName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
|
loginRequest.OptionalPreSharedKey = &preSharedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginErr error
|
var loginErr error
|
||||||
@@ -114,7 +118,7 @@ var loginCmd = &cobra.Command{
|
|||||||
if loginResp.NeedsSSOLogin {
|
if loginResp.NeedsSSOLogin {
|
||||||
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode)
|
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode)
|
||||||
|
|
||||||
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("waiting sso login failed with: %v", err)
|
return fmt.Errorf("waiting sso login failed with: %v", err)
|
||||||
}
|
}
|
||||||
@@ -150,13 +154,21 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
|
|||||||
jwtToken = tokenInfo.GetTokenToUse()
|
jwtToken = tokenInfo.GetTokenToUse()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastError error
|
||||||
|
|
||||||
err = WithBackOff(func() error {
|
err = WithBackOff(func() error {
|
||||||
err := internal.Login(ctx, config, setupKey, jwtToken)
|
err := internal.Login(ctx, config, setupKey, jwtToken)
|
||||||
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
if s, ok := gstatus.FromError(err); ok && (s.Code() == codes.InvalidArgument || s.Code() == codes.PermissionDenied) {
|
||||||
|
lastError = err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if lastError != nil {
|
||||||
|
return fmt.Errorf("login failed: %v", lastError)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("backoff cycle failed: %v", err)
|
return fmt.Errorf("backoff cycle failed: %v", err)
|
||||||
}
|
}
|
||||||
@@ -165,7 +177,7 @@ func foregroundLogin(ctx context.Context, cmd *cobra.Command, config *internal.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
|
func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *internal.Config) (*auth.TokenInfo, error) {
|
||||||
oAuthFlow, err := auth.NewOAuthFlow(ctx, config)
|
oAuthFlow, err := auth.NewOAuthFlow(ctx, config, isLinuxRunningDesktop())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -177,8 +189,8 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
|
|||||||
|
|
||||||
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode)
|
openURL(cmd, flowInfo.VerificationURIComplete, flowInfo.UserCode)
|
||||||
|
|
||||||
waitTimeout := time.Duration(flowInfo.ExpiresIn)
|
waitTimeout := time.Duration(flowInfo.ExpiresIn) * time.Second
|
||||||
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
|
waitCTX, c := context.WithTimeout(context.TODO(), waitTimeout)
|
||||||
defer c()
|
defer c()
|
||||||
|
|
||||||
tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo)
|
tokenInfo, err := oAuthFlow.WaitToken(waitCTX, flowInfo)
|
||||||
@@ -191,17 +203,21 @@ func foregroundGetTokenInfo(ctx context.Context, cmd *cobra.Command, config *int
|
|||||||
|
|
||||||
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
|
func openURL(cmd *cobra.Command, verificationURIComplete, userCode string) {
|
||||||
var codeMsg string
|
var codeMsg string
|
||||||
if userCode != "" {
|
if userCode != "" && !strings.Contains(verificationURIComplete, userCode) {
|
||||||
if !strings.Contains(verificationURIComplete, userCode) {
|
|
||||||
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
codeMsg = fmt.Sprintf("and enter the code %s to authenticate.", userCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd.Println("Please do the SSO login in your browser. \n" +
|
||||||
|
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
||||||
|
verificationURIComplete + " " + codeMsg)
|
||||||
|
cmd.Println("")
|
||||||
|
if err := open.Run(verificationURIComplete); err != nil {
|
||||||
|
cmd.Println("\nAlternatively, you may want to use a setup key, see:\n\n" +
|
||||||
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := open.Run(verificationURIComplete)
|
// isLinuxRunningDesktop checks if a Linux OS is running desktop environment
|
||||||
cmd.Printf("Please do the SSO login in your browser. \n" +
|
func isLinuxRunningDesktop() bool {
|
||||||
"If your browser didn't open automatically, use this URL to log in:\n\n" +
|
return os.Getenv("DESKTOP_SESSION") != "" || os.Getenv("XDG_CURRENT_DESKTOP") != ""
|
||||||
" " + verificationURIComplete + " " + codeMsg + " \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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ import (
|
|||||||
const (
|
const (
|
||||||
externalIPMapFlag = "external-ip-map"
|
externalIPMapFlag = "external-ip-map"
|
||||||
dnsResolverAddress = "dns-resolver-address"
|
dnsResolverAddress = "dns-resolver-address"
|
||||||
|
enableRosenpassFlag = "enable-rosenpass"
|
||||||
|
rosenpassPermissiveFlag = "rosenpass-permissive"
|
||||||
|
preSharedKeyFlag = "preshared-key"
|
||||||
|
interfaceNameFlag = "interface-name"
|
||||||
|
wireguardPortFlag = "wireguard-port"
|
||||||
|
networkMonitorFlag = "network-monitor"
|
||||||
|
disableAutoConnectFlag = "disable-auto-connect"
|
||||||
|
serverSSHAllowedFlag = "allow-server-ssh"
|
||||||
|
extraIFaceBlackListFlag = "extra-iface-blacklist"
|
||||||
|
dnsRouteIntervalFlag = "dns-router-interval"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -49,6 +59,18 @@ var (
|
|||||||
preSharedKey string
|
preSharedKey string
|
||||||
natExternalIPs []string
|
natExternalIPs []string
|
||||||
customDNSAddress string
|
customDNSAddress string
|
||||||
|
rosenpassEnabled bool
|
||||||
|
rosenpassPermissive bool
|
||||||
|
serverSSHAllowed bool
|
||||||
|
interfaceName string
|
||||||
|
wireguardPort uint16
|
||||||
|
networkMonitor bool
|
||||||
|
serviceName string
|
||||||
|
autoConnectDisabled bool
|
||||||
|
extraIFaceBlackList []string
|
||||||
|
anonymizeFlag bool
|
||||||
|
dnsRouteInterval time.Duration
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird",
|
Use: "netbird",
|
||||||
Short: "",
|
Short: "",
|
||||||
@@ -87,15 +109,24 @@ func init() {
|
|||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
defaultDaemonAddr = "tcp://127.0.0.1:41731"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defaultServiceName := "netbird"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
defaultServiceName = "Netbird"
|
||||||
|
}
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
rootCmd.PersistentFlags().StringVar(&daemonAddr, "daemon-addr", defaultDaemonAddr, "Daemon service address to serve CLI requests [unix|tcp]://[path|host:port]")
|
||||||
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
|
rootCmd.PersistentFlags().StringVarP(&managementURL, "management-url", "m", "", fmt.Sprintf("Management Service URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultManagementURL))
|
||||||
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
|
rootCmd.PersistentFlags().StringVar(&adminURL, "admin-url", "", fmt.Sprintf("Admin Panel URL [http|https]://[host]:[port] (default \"%s\")", internal.DefaultAdminURL))
|
||||||
|
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||||
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
|
rootCmd.PersistentFlags().StringVarP(&configPath, "config", "c", defaultConfigPath, "Netbird config file location")
|
||||||
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "info", "sets Netbird log level")
|
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", "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(&logFile, "log-file", defaultLogFile, "sets Netbird log path. If console is specified the log will be output to stdout")
|
||||||
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
|
rootCmd.PersistentFlags().StringVarP(&setupKey, "setup-key", "k", "", "Setup key obtained from the Management Service Dashboard (used to register peer)")
|
||||||
rootCmd.PersistentFlags().StringVar(&preSharedKey, "preshared-key", "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
|
rootCmd.PersistentFlags().StringVar(&preSharedKey, preSharedKeyFlag, "", "Sets Wireguard PreSharedKey property. If set, then only peers that have the same key can communicate.")
|
||||||
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
|
rootCmd.PersistentFlags().StringVarP(&hostName, "hostname", "n", "", "Sets a custom hostname for the device")
|
||||||
|
rootCmd.PersistentFlags().BoolVarP(&anonymizeFlag, "anonymize", "A", false, "anonymize IP addresses and non-netbird.io domains in logs and status output")
|
||||||
|
|
||||||
rootCmd.AddCommand(serviceCmd)
|
rootCmd.AddCommand(serviceCmd)
|
||||||
rootCmd.AddCommand(upCmd)
|
rootCmd.AddCommand(upCmd)
|
||||||
rootCmd.AddCommand(downCmd)
|
rootCmd.AddCommand(downCmd)
|
||||||
@@ -103,8 +134,20 @@ func init() {
|
|||||||
rootCmd.AddCommand(loginCmd)
|
rootCmd.AddCommand(loginCmd)
|
||||||
rootCmd.AddCommand(versionCmd)
|
rootCmd.AddCommand(versionCmd)
|
||||||
rootCmd.AddCommand(sshCmd)
|
rootCmd.AddCommand(sshCmd)
|
||||||
|
rootCmd.AddCommand(routesCmd)
|
||||||
|
rootCmd.AddCommand(debugCmd)
|
||||||
|
|
||||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd) // service control commands are subcommands of service
|
||||||
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
|
serviceCmd.AddCommand(installCmd, uninstallCmd) // service installer commands are subcommands of service
|
||||||
|
|
||||||
|
routesCmd.AddCommand(routesListCmd)
|
||||||
|
routesCmd.AddCommand(routesSelectCmd, routesDeselectCmd)
|
||||||
|
|
||||||
|
debugCmd.AddCommand(debugBundleCmd)
|
||||||
|
debugCmd.AddCommand(logCmd)
|
||||||
|
logCmd.AddCommand(logLevelCmd)
|
||||||
|
debugCmd.AddCommand(forCmd)
|
||||||
|
|
||||||
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
|
upCmd.PersistentFlags().StringSliceVar(&natExternalIPs, externalIPMapFlag, nil,
|
||||||
`Sets external IPs maps between local addresses and interfaces.`+
|
`Sets external IPs maps between local addresses and interfaces.`+
|
||||||
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
|
`You can specify a comma-separated list with a single IP and IP/IP or IP/Interface Name. `+
|
||||||
@@ -118,6 +161,10 @@ func init() {
|
|||||||
`An empty string "" clears the previous configuration. `+
|
`An empty string "" clears the previous configuration. `+
|
||||||
`E.g. --dns-resolver-address 127.0.0.1:5053 or --dns-resolver-address ""`,
|
`E.g. --dns-resolver-address 127.0.0.1:5053 or --dns-resolver-address ""`,
|
||||||
)
|
)
|
||||||
|
upCmd.PersistentFlags().BoolVar(&rosenpassEnabled, enableRosenpassFlag, false, "[Experimental] Enable Rosenpass feature. If enabled, the connection will be post-quantum secured via Rosenpass.")
|
||||||
|
upCmd.PersistentFlags().BoolVar(&rosenpassPermissive, rosenpassPermissiveFlag, false, "[Experimental] Enable Rosenpass in permissive mode to allow this peer to accept WireGuard connections without requiring Rosenpass functionality from peers that do not have Rosenpass enabled.")
|
||||||
|
upCmd.PersistentFlags().BoolVar(&serverSSHAllowed, serverSSHAllowedFlag, false, "Allow SSH server on peer. If enabled, the SSH server will be permitted")
|
||||||
|
upCmd.PersistentFlags().BoolVar(&autoConnectDisabled, disableAutoConnectFlag, false, "Disables auto-connect feature. If enabled, then the client won't connect automatically when the service starts.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupCloseHandler handles SIGTERM signal and exits with success
|
// SetupCloseHandler handles SIGTERM signal and exits with success
|
||||||
@@ -168,7 +215,7 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
|
|||||||
return prefix + upper
|
return prefix + upper
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialClientGRPCServer returns client connection to the dameno server.
|
// DialClientGRPCServer returns client connection to the daemon server.
|
||||||
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
|
ctx, cancel := context.WithTimeout(ctx, time.Second*3)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -308,3 +355,17 @@ func migrateToNetbird(oldPath, newPath string) bool {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getClient(cmd *cobra.Command) (*grpc.ClientConn, error) {
|
||||||
|
SetFlagsFromEnvVars(rootCmd)
|
||||||
|
cmd.SetOut(cmd.OutOrStdout())
|
||||||
|
|
||||||
|
conn, err := DialClientGRPCServer(cmd.Context(), daemonAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|||||||
174
client/cmd/route.go
Normal file
174
client/cmd/route.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var appendFlag bool
|
||||||
|
|
||||||
|
var routesCmd = &cobra.Command{
|
||||||
|
Use: "routes",
|
||||||
|
Short: "Manage network routes",
|
||||||
|
Long: `Commands to list, select, or deselect network routes.`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var routesListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Aliases: []string{"ls"},
|
||||||
|
Short: "List routes",
|
||||||
|
Example: " netbird routes list",
|
||||||
|
Long: "List all available network routes.",
|
||||||
|
RunE: routesList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var routesSelectCmd = &cobra.Command{
|
||||||
|
Use: "select route...|all",
|
||||||
|
Short: "Select routes",
|
||||||
|
Long: "Select a list of routes by identifiers or 'all' to clear all selections and to accept all (including new) routes.\nDefault mode is replace, use -a to append to already selected routes.",
|
||||||
|
Example: " netbird routes select all\n netbird routes select route1 route2\n netbird routes select -a route3",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: routesSelect,
|
||||||
|
}
|
||||||
|
|
||||||
|
var routesDeselectCmd = &cobra.Command{
|
||||||
|
Use: "deselect route...|all",
|
||||||
|
Short: "Deselect routes",
|
||||||
|
Long: "Deselect previously selected routes by identifiers or 'all' to disable accepting any routes.",
|
||||||
|
Example: " netbird routes deselect all\n netbird routes deselect route1 route2",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: routesDeselect,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routesSelectCmd.PersistentFlags().BoolVarP(&appendFlag, "append", "a", false, "Append to current route selection instead of replacing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func routesList(cmd *cobra.Command, _ []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.ListRoutes(cmd.Context(), &proto.ListRoutesRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list routes: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Routes) == 0 {
|
||||||
|
cmd.Println("No routes available.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
printRoutes(cmd, resp)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRoutes(cmd *cobra.Command, resp *proto.ListRoutesResponse) {
|
||||||
|
cmd.Println("Available Routes:")
|
||||||
|
for _, route := range resp.Routes {
|
||||||
|
printRoute(cmd, route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printRoute(cmd *cobra.Command, route *proto.Route) {
|
||||||
|
selectedStatus := getSelectedStatus(route)
|
||||||
|
domains := route.GetDomains()
|
||||||
|
|
||||||
|
if len(domains) > 0 {
|
||||||
|
printDomainRoute(cmd, route, domains, selectedStatus)
|
||||||
|
} else {
|
||||||
|
printNetworkRoute(cmd, route, selectedStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSelectedStatus(route *proto.Route) string {
|
||||||
|
if route.GetSelected() {
|
||||||
|
return "Selected"
|
||||||
|
}
|
||||||
|
return "Not Selected"
|
||||||
|
}
|
||||||
|
|
||||||
|
func printDomainRoute(cmd *cobra.Command, route *proto.Route, domains []string, selectedStatus string) {
|
||||||
|
cmd.Printf("\n - ID: %s\n Domains: %s\n Status: %s\n", route.GetID(), strings.Join(domains, ", "), selectedStatus)
|
||||||
|
resolvedIPs := route.GetResolvedIPs()
|
||||||
|
|
||||||
|
if len(resolvedIPs) > 0 {
|
||||||
|
printResolvedIPs(cmd, domains, resolvedIPs)
|
||||||
|
} else {
|
||||||
|
cmd.Printf(" Resolved IPs: -\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNetworkRoute(cmd *cobra.Command, route *proto.Route, selectedStatus string) {
|
||||||
|
cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetNetwork(), selectedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printResolvedIPs(cmd *cobra.Command, domains []string, resolvedIPs map[string]*proto.IPList) {
|
||||||
|
cmd.Printf(" Resolved IPs:\n")
|
||||||
|
for _, domain := range domains {
|
||||||
|
if ipList, exists := resolvedIPs[domain]; exists {
|
||||||
|
cmd.Printf(" [%s]: %s\n", domain, strings.Join(ipList.GetIps(), ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func routesSelect(cmd *cobra.Command, args []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
req := &proto.SelectRoutesRequest{
|
||||||
|
RouteIDs: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 1 && args[0] == "all" {
|
||||||
|
req.All = true
|
||||||
|
} else if appendFlag {
|
||||||
|
req.Append = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.SelectRoutes(cmd.Context(), req); err != nil {
|
||||||
|
return fmt.Errorf("failed to select routes: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Routes selected successfully.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func routesDeselect(cmd *cobra.Command, args []string) error {
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
req := &proto.SelectRoutesRequest{
|
||||||
|
RouteIDs: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 1 && args[0] == "all" {
|
||||||
|
req.All = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.DeselectRoutes(cmd.Context(), req); err != nil {
|
||||||
|
return fmt.Errorf("failed to deselect routes: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Routes deselected successfully.")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -24,12 +22,8 @@ func newProgram(ctx context.Context, cancel context.CancelFunc) *program {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newSVCConfig() *service.Config {
|
func newSVCConfig() *service.Config {
|
||||||
name := "netbird"
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
name = "Netbird"
|
|
||||||
}
|
|
||||||
return &service.Config{
|
return &service.Config{
|
||||||
Name: name,
|
Name: serviceName,
|
||||||
DisplayName: "Netbird",
|
DisplayName: "Netbird",
|
||||||
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
|
Description: "A WireGuard-based mesh network that connects your devices into a single private network.",
|
||||||
Option: make(service.KeyValue),
|
Option: make(service.KeyValue),
|
||||||
|
|||||||
@@ -11,11 +11,12 @@ import (
|
|||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/server"
|
"github.com/netbirdio/netbird/client/server"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *program) Start(svc service.Service) error {
|
func (p *program) Start(svc service.Service) error {
|
||||||
@@ -109,7 +110,6 @@ var runCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cmd.Printf("Netbird service is running")
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ var installCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
svcConfig.Option["OnFailure"] = "restart"
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(cmd.Context())
|
ctx, cancel := context.WithCancel(cmd.Context())
|
||||||
|
|
||||||
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
|
s, err := newSVC(newProgram(ctx, cancel), svcConfig)
|
||||||
@@ -77,6 +81,7 @@ var installCmd = &cobra.Command{
|
|||||||
cmd.PrintErrln(err)
|
cmd.PrintErrln(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println("Netbird service has been installed")
|
cmd.Println("Netbird service has been installed")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -106,7 +111,7 @@ var uninstallCmd = &cobra.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cmd.Println("Netbird has been uninstalled")
|
cmd.Println("Netbird service has been uninstalled")
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var sshCmd = &cobra.Command{
|
var sshCmd = &cobra.Command{
|
||||||
Use: "ssh",
|
Use: "ssh [user@]host",
|
||||||
Args: func(cmd *cobra.Command, args []string) error {
|
Args: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return errors.New("requires a host argument")
|
return errors.New("requires a host argument")
|
||||||
@@ -94,7 +94,7 @@ func runSSH(ctx context.Context, addr string, pemKey []byte, cmd *cobra.Command)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.Printf("Error: %v\n", err)
|
cmd.Printf("Error: %v\n", err)
|
||||||
cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" +
|
cmd.Printf("Couldn't connect. Please check the connection status or if the ssh server is enabled on the other peer" +
|
||||||
"You can verify the connection by running:\n\n" +
|
"\nYou can verify the connection by running:\n\n" +
|
||||||
" netbird status\n\n")
|
" netbird status\n\n")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -14,6 +16,7 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
@@ -30,6 +33,13 @@ type peerStateDetailOutput struct {
|
|||||||
ConnType string `json:"connectionType" yaml:"connectionType"`
|
ConnType string `json:"connectionType" yaml:"connectionType"`
|
||||||
Direct bool `json:"direct" yaml:"direct"`
|
Direct bool `json:"direct" yaml:"direct"`
|
||||||
IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"`
|
IceCandidateType iceCandidateType `json:"iceCandidateType" yaml:"iceCandidateType"`
|
||||||
|
IceCandidateEndpoint iceCandidateType `json:"iceCandidateEndpoint" yaml:"iceCandidateEndpoint"`
|
||||||
|
LastWireguardHandshake time.Time `json:"lastWireguardHandshake" yaml:"lastWireguardHandshake"`
|
||||||
|
TransferReceived int64 `json:"transferReceived" yaml:"transferReceived"`
|
||||||
|
TransferSent int64 `json:"transferSent" yaml:"transferSent"`
|
||||||
|
Latency time.Duration `json:"latency" yaml:"latency"`
|
||||||
|
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
|
||||||
|
Routes []string `json:"routes" yaml:"routes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type peersStateOutput struct {
|
type peersStateOutput struct {
|
||||||
@@ -41,11 +51,25 @@ type peersStateOutput struct {
|
|||||||
type signalStateOutput struct {
|
type signalStateOutput struct {
|
||||||
URL string `json:"url" yaml:"url"`
|
URL string `json:"url" yaml:"url"`
|
||||||
Connected bool `json:"connected" yaml:"connected"`
|
Connected bool `json:"connected" yaml:"connected"`
|
||||||
|
Error string `json:"error" yaml:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type managementStateOutput struct {
|
type managementStateOutput struct {
|
||||||
URL string `json:"url" yaml:"url"`
|
URL string `json:"url" yaml:"url"`
|
||||||
Connected bool `json:"connected" yaml:"connected"`
|
Connected bool `json:"connected" yaml:"connected"`
|
||||||
|
Error string `json:"error" yaml:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type relayStateOutputDetail struct {
|
||||||
|
URI string `json:"uri" yaml:"uri"`
|
||||||
|
Available bool `json:"available" yaml:"available"`
|
||||||
|
Error string `json:"error" yaml:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type relayStateOutput struct {
|
||||||
|
Total int `json:"total" yaml:"total"`
|
||||||
|
Available int `json:"available" yaml:"available"`
|
||||||
|
Details []relayStateOutputDetail `json:"details" yaml:"details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type iceCandidateType struct {
|
type iceCandidateType struct {
|
||||||
@@ -53,16 +77,28 @@ type iceCandidateType struct {
|
|||||||
Remote string `json:"remote" yaml:"remote"`
|
Remote string `json:"remote" yaml:"remote"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nsServerGroupStateOutput struct {
|
||||||
|
Servers []string `json:"servers" yaml:"servers"`
|
||||||
|
Domains []string `json:"domains" yaml:"domains"`
|
||||||
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
||||||
|
Error string `json:"error" yaml:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
type statusOutputOverview struct {
|
type statusOutputOverview struct {
|
||||||
Peers peersStateOutput `json:"peers" yaml:"peers"`
|
Peers peersStateOutput `json:"peers" yaml:"peers"`
|
||||||
CliVersion string `json:"cliVersion" yaml:"cliVersion"`
|
CliVersion string `json:"cliVersion" yaml:"cliVersion"`
|
||||||
DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"`
|
DaemonVersion string `json:"daemonVersion" yaml:"daemonVersion"`
|
||||||
ManagementState managementStateOutput `json:"management" yaml:"management"`
|
ManagementState managementStateOutput `json:"management" yaml:"management"`
|
||||||
SignalState signalStateOutput `json:"signal" yaml:"signal"`
|
SignalState signalStateOutput `json:"signal" yaml:"signal"`
|
||||||
|
Relays relayStateOutput `json:"relays" yaml:"relays"`
|
||||||
IP string `json:"netbirdIp" yaml:"netbirdIp"`
|
IP string `json:"netbirdIp" yaml:"netbirdIp"`
|
||||||
PubKey string `json:"publicKey" yaml:"publicKey"`
|
PubKey string `json:"publicKey" yaml:"publicKey"`
|
||||||
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
|
KernelInterface bool `json:"usesKernelInterface" yaml:"usesKernelInterface"`
|
||||||
FQDN string `json:"fqdn" yaml:"fqdn"`
|
FQDN string `json:"fqdn" yaml:"fqdn"`
|
||||||
|
RosenpassEnabled bool `json:"quantumResistance" yaml:"quantumResistance"`
|
||||||
|
RosenpassPermissive bool `json:"quantumResistancePermissive" yaml:"quantumResistancePermissive"`
|
||||||
|
Routes []string `json:"routes" yaml:"routes"`
|
||||||
|
NSServerGroups []nsServerGroupStateOutput `json:"dnsServers" yaml:"dnsServers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -71,8 +107,10 @@ var (
|
|||||||
jsonFlag bool
|
jsonFlag bool
|
||||||
yamlFlag bool
|
yamlFlag bool
|
||||||
ipsFilter []string
|
ipsFilter []string
|
||||||
|
prefixNamesFilter []string
|
||||||
statusFilter string
|
statusFilter string
|
||||||
ipsFilterMap map[string]struct{}
|
ipsFilterMap map[string]struct{}
|
||||||
|
prefixNamesFilterMap map[string]struct{}
|
||||||
)
|
)
|
||||||
|
|
||||||
var statusCmd = &cobra.Command{
|
var statusCmd = &cobra.Command{
|
||||||
@@ -83,12 +121,14 @@ var statusCmd = &cobra.Command{
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
ipsFilterMap = make(map[string]struct{})
|
ipsFilterMap = make(map[string]struct{})
|
||||||
|
prefixNamesFilterMap = make(map[string]struct{})
|
||||||
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information in human-readable format")
|
statusCmd.PersistentFlags().BoolVarP(&detailFlag, "detail", "d", false, "display detailed status information in human-readable format")
|
||||||
statusCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "display detailed status information in json format")
|
statusCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "display detailed status information in json format")
|
||||||
statusCmd.PersistentFlags().BoolVar(&yamlFlag, "yaml", false, "display detailed status information in yaml format")
|
statusCmd.PersistentFlags().BoolVar(&yamlFlag, "yaml", false, "display detailed status information in yaml format")
|
||||||
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
|
||||||
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
|
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
|
||||||
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
|
||||||
|
statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
|
||||||
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(connected|disconnected), e.g., --filter-by-status connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,11 +147,11 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("failed initializing log %v", err)
|
return fmt.Errorf("failed initializing log %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := internal.CtxInitState(context.Background())
|
ctx := internal.CtxInitState(cmd.Context())
|
||||||
|
|
||||||
resp, _ := getStatus(ctx, cmd)
|
resp, err := getStatus(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
|
if resp.GetStatus() == string(internal.StatusNeedsLogin) || resp.GetStatus() == string(internal.StatusLoginFailed) {
|
||||||
@@ -120,7 +160,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
" netbird up \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"+
|
"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"+
|
"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",
|
"More info: https://docs.netbird.io/how-to/register-machines-using-setup-keys\n\n",
|
||||||
resp.GetStatus(),
|
resp.GetStatus(),
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
@@ -133,7 +173,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
outputInformationHolder := convertToStatusOutputOverview(resp)
|
outputInformationHolder := convertToStatusOutputOverview(resp)
|
||||||
|
|
||||||
statusOutputString := ""
|
var statusOutputString string
|
||||||
switch {
|
switch {
|
||||||
case detailFlag:
|
case detailFlag:
|
||||||
statusOutputString = parseToFullDetailSummary(outputInformationHolder)
|
statusOutputString = parseToFullDetailSummary(outputInformationHolder)
|
||||||
@@ -142,7 +182,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
case yamlFlag:
|
case yamlFlag:
|
||||||
statusOutputString, err = parseToYAML(outputInformationHolder)
|
statusOutputString, err = parseToYAML(outputInformationHolder)
|
||||||
default:
|
default:
|
||||||
statusOutputString = parseGeneralSummary(outputInformationHolder, false)
|
statusOutputString = parseGeneralSummary(outputInformationHolder, false, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -154,7 +194,7 @@ func statusFunc(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse, error) {
|
func getStatus(ctx context.Context) (*proto.StatusResponse, error) {
|
||||||
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
conn, err := DialClientGRPCServer(ctx, daemonAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
return nil, fmt.Errorf("failed to connect to daemon error: %v\n"+
|
||||||
@@ -163,7 +203,7 @@ func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse,
|
|||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
resp, err := proto.NewDaemonServiceClient(conn).Status(cmd.Context(), &proto.StatusRequest{GetFullPeerStatus: true})
|
resp, err := proto.NewDaemonServiceClient(conn).Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
return nil, fmt.Errorf("status failed: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
@@ -172,8 +212,12 @@ func getStatus(ctx context.Context, cmd *cobra.Command) (*proto.StatusResponse,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseFilters() error {
|
func parseFilters() error {
|
||||||
|
|
||||||
switch strings.ToLower(statusFilter) {
|
switch strings.ToLower(statusFilter) {
|
||||||
case "", "disconnected", "connected":
|
case "", "disconnected", "connected":
|
||||||
|
if strings.ToLower(statusFilter) != "" {
|
||||||
|
enableDetailFlagWhenFilterFlag()
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
|
return fmt.Errorf("wrong status filter, should be one of connected|disconnected, got: %s", statusFilter)
|
||||||
}
|
}
|
||||||
@@ -185,11 +229,26 @@ func parseFilters() error {
|
|||||||
return fmt.Errorf("got an invalid IP address in the filter: address %s, error %s", addr, err)
|
return fmt.Errorf("got an invalid IP address in the filter: address %s, error %s", addr, err)
|
||||||
}
|
}
|
||||||
ipsFilterMap[addr] = struct{}{}
|
ipsFilterMap[addr] = struct{}{}
|
||||||
|
enableDetailFlagWhenFilterFlag()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(prefixNamesFilter) > 0 {
|
||||||
|
for _, name := range prefixNamesFilter {
|
||||||
|
prefixNamesFilterMap[strings.ToLower(name)] = struct{}{}
|
||||||
|
}
|
||||||
|
enableDetailFlagWhenFilterFlag()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enableDetailFlagWhenFilterFlag() {
|
||||||
|
if !detailFlag && !jsonFlag && !yamlFlag {
|
||||||
|
detailFlag = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview {
|
func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverview {
|
||||||
pbFullStatus := resp.GetFullStatus()
|
pbFullStatus := resp.GetFullStatus()
|
||||||
|
|
||||||
@@ -197,14 +256,17 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
|
|||||||
managementOverview := managementStateOutput{
|
managementOverview := managementStateOutput{
|
||||||
URL: managementState.GetURL(),
|
URL: managementState.GetURL(),
|
||||||
Connected: managementState.GetConnected(),
|
Connected: managementState.GetConnected(),
|
||||||
|
Error: managementState.Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
signalState := pbFullStatus.GetSignalState()
|
signalState := pbFullStatus.GetSignalState()
|
||||||
signalOverview := signalStateOutput{
|
signalOverview := signalStateOutput{
|
||||||
URL: signalState.GetURL(),
|
URL: signalState.GetURL(),
|
||||||
Connected: signalState.GetConnected(),
|
Connected: signalState.GetConnected(),
|
||||||
|
Error: signalState.Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relayOverview := mapRelays(pbFullStatus.GetRelays())
|
||||||
peersOverview := mapPeers(resp.GetFullStatus().GetPeers())
|
peersOverview := mapPeers(resp.GetFullStatus().GetPeers())
|
||||||
|
|
||||||
overview := statusOutputOverview{
|
overview := statusOutputOverview{
|
||||||
@@ -213,35 +275,94 @@ func convertToStatusOutputOverview(resp *proto.StatusResponse) statusOutputOverv
|
|||||||
DaemonVersion: resp.GetDaemonVersion(),
|
DaemonVersion: resp.GetDaemonVersion(),
|
||||||
ManagementState: managementOverview,
|
ManagementState: managementOverview,
|
||||||
SignalState: signalOverview,
|
SignalState: signalOverview,
|
||||||
|
Relays: relayOverview,
|
||||||
IP: pbFullStatus.GetLocalPeerState().GetIP(),
|
IP: pbFullStatus.GetLocalPeerState().GetIP(),
|
||||||
PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(),
|
PubKey: pbFullStatus.GetLocalPeerState().GetPubKey(),
|
||||||
KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(),
|
KernelInterface: pbFullStatus.GetLocalPeerState().GetKernelInterface(),
|
||||||
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
|
FQDN: pbFullStatus.GetLocalPeerState().GetFqdn(),
|
||||||
|
RosenpassEnabled: pbFullStatus.GetLocalPeerState().GetRosenpassEnabled(),
|
||||||
|
RosenpassPermissive: pbFullStatus.GetLocalPeerState().GetRosenpassPermissive(),
|
||||||
|
Routes: pbFullStatus.GetLocalPeerState().GetRoutes(),
|
||||||
|
NSServerGroups: mapNSGroups(pbFullStatus.GetDnsServers()),
|
||||||
|
}
|
||||||
|
|
||||||
|
if anonymizeFlag {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||||
|
anonymizeOverview(anonymizer, &overview)
|
||||||
}
|
}
|
||||||
|
|
||||||
return overview
|
return overview
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mapRelays(relays []*proto.RelayState) relayStateOutput {
|
||||||
|
var relayStateDetail []relayStateOutputDetail
|
||||||
|
|
||||||
|
var relaysAvailable int
|
||||||
|
for _, relay := range relays {
|
||||||
|
available := relay.GetAvailable()
|
||||||
|
relayStateDetail = append(relayStateDetail,
|
||||||
|
relayStateOutputDetail{
|
||||||
|
URI: relay.URI,
|
||||||
|
Available: available,
|
||||||
|
Error: relay.GetError(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if available {
|
||||||
|
relaysAvailable++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relayStateOutput{
|
||||||
|
Total: len(relays),
|
||||||
|
Available: relaysAvailable,
|
||||||
|
Details: relayStateDetail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapNSGroups(servers []*proto.NSGroupState) []nsServerGroupStateOutput {
|
||||||
|
mappedNSGroups := make([]nsServerGroupStateOutput, 0, len(servers))
|
||||||
|
for _, pbNsGroupServer := range servers {
|
||||||
|
mappedNSGroups = append(mappedNSGroups, nsServerGroupStateOutput{
|
||||||
|
Servers: pbNsGroupServer.GetServers(),
|
||||||
|
Domains: pbNsGroupServer.GetDomains(),
|
||||||
|
Enabled: pbNsGroupServer.GetEnabled(),
|
||||||
|
Error: pbNsGroupServer.GetError(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mappedNSGroups
|
||||||
|
}
|
||||||
|
|
||||||
func mapPeers(peers []*proto.PeerState) peersStateOutput {
|
func mapPeers(peers []*proto.PeerState) peersStateOutput {
|
||||||
var peersStateDetail []peerStateDetailOutput
|
var peersStateDetail []peerStateDetailOutput
|
||||||
localICE := ""
|
localICE := ""
|
||||||
remoteICE := ""
|
remoteICE := ""
|
||||||
|
localICEEndpoint := ""
|
||||||
|
remoteICEEndpoint := ""
|
||||||
connType := ""
|
connType := ""
|
||||||
peersConnected := 0
|
peersConnected := 0
|
||||||
|
lastHandshake := time.Time{}
|
||||||
|
transferReceived := int64(0)
|
||||||
|
transferSent := int64(0)
|
||||||
for _, pbPeerState := range peers {
|
for _, pbPeerState := range peers {
|
||||||
isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
|
isPeerConnected := pbPeerState.ConnStatus == peer.StatusConnected.String()
|
||||||
if skipDetailByFilters(pbPeerState, isPeerConnected) {
|
if skipDetailByFilters(pbPeerState, isPeerConnected) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isPeerConnected {
|
if isPeerConnected {
|
||||||
peersConnected = peersConnected + 1
|
peersConnected++
|
||||||
|
|
||||||
localICE = pbPeerState.GetLocalIceCandidateType()
|
localICE = pbPeerState.GetLocalIceCandidateType()
|
||||||
remoteICE = pbPeerState.GetRemoteIceCandidateType()
|
remoteICE = pbPeerState.GetRemoteIceCandidateType()
|
||||||
|
localICEEndpoint = pbPeerState.GetLocalIceCandidateEndpoint()
|
||||||
|
remoteICEEndpoint = pbPeerState.GetRemoteIceCandidateEndpoint()
|
||||||
connType = "P2P"
|
connType = "P2P"
|
||||||
if pbPeerState.Relayed {
|
if pbPeerState.Relayed {
|
||||||
connType = "Relayed"
|
connType = "Relayed"
|
||||||
}
|
}
|
||||||
|
lastHandshake = pbPeerState.GetLastWireguardHandshake().AsTime().Local()
|
||||||
|
transferReceived = pbPeerState.GetBytesRx()
|
||||||
|
transferSent = pbPeerState.GetBytesTx()
|
||||||
}
|
}
|
||||||
|
|
||||||
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
|
timeLocal := pbPeerState.GetConnStatusUpdate().AsTime().Local()
|
||||||
@@ -256,7 +377,17 @@ func mapPeers(peers []*proto.PeerState) peersStateOutput {
|
|||||||
Local: localICE,
|
Local: localICE,
|
||||||
Remote: remoteICE,
|
Remote: remoteICE,
|
||||||
},
|
},
|
||||||
|
IceCandidateEndpoint: iceCandidateType{
|
||||||
|
Local: localICEEndpoint,
|
||||||
|
Remote: remoteICEEndpoint,
|
||||||
|
},
|
||||||
FQDN: pbPeerState.GetFqdn(),
|
FQDN: pbPeerState.GetFqdn(),
|
||||||
|
LastWireguardHandshake: lastHandshake,
|
||||||
|
TransferReceived: transferReceived,
|
||||||
|
TransferSent: transferSent,
|
||||||
|
Latency: pbPeerState.GetLatency().AsDuration(),
|
||||||
|
RosenpassEnabled: pbPeerState.GetRosenpassEnabled(),
|
||||||
|
Routes: pbPeerState.GetRoutes(),
|
||||||
}
|
}
|
||||||
|
|
||||||
peersStateDetail = append(peersStateDetail, peerState)
|
peersStateDetail = append(peersStateDetail, peerState)
|
||||||
@@ -306,22 +437,31 @@ func parseToYAML(overview statusOutputOverview) (string, error) {
|
|||||||
return string(yamlBytes), nil
|
return string(yamlBytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
|
func parseGeneralSummary(overview statusOutputOverview, showURL bool, showRelays bool, showNameServers bool) string {
|
||||||
|
var managementConnString string
|
||||||
managementConnString := "Disconnected"
|
|
||||||
if overview.ManagementState.Connected {
|
if overview.ManagementState.Connected {
|
||||||
managementConnString = "Connected"
|
managementConnString = "Connected"
|
||||||
if showURL {
|
if showURL {
|
||||||
managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL)
|
managementConnString = fmt.Sprintf("%s to %s", managementConnString, overview.ManagementState.URL)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
managementConnString = "Disconnected"
|
||||||
|
if overview.ManagementState.Error != "" {
|
||||||
|
managementConnString = fmt.Sprintf("%s, reason: %s", managementConnString, overview.ManagementState.Error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
signalConnString := "Disconnected"
|
var signalConnString string
|
||||||
if overview.SignalState.Connected {
|
if overview.SignalState.Connected {
|
||||||
signalConnString = "Connected"
|
signalConnString = "Connected"
|
||||||
if showURL {
|
if showURL {
|
||||||
signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL)
|
signalConnString = fmt.Sprintf("%s to %s", signalConnString, overview.SignalState.URL)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
signalConnString = "Disconnected"
|
||||||
|
if overview.SignalState.Error != "" {
|
||||||
|
signalConnString = fmt.Sprintf("%s, reason: %s", signalConnString, overview.SignalState.Error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interfaceTypeString := "Userspace"
|
interfaceTypeString := "Userspace"
|
||||||
@@ -333,32 +473,107 @@ func parseGeneralSummary(overview statusOutputOverview, showURL bool) string {
|
|||||||
interfaceIP = "N/A"
|
interfaceIP = "N/A"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var relaysString string
|
||||||
|
if showRelays {
|
||||||
|
for _, relay := range overview.Relays.Details {
|
||||||
|
available := "Available"
|
||||||
|
reason := ""
|
||||||
|
if !relay.Available {
|
||||||
|
available = "Unavailable"
|
||||||
|
reason = fmt.Sprintf(", reason: %s", relay.Error)
|
||||||
|
}
|
||||||
|
relaysString += fmt.Sprintf("\n [%s] is %s%s", relay.URI, available, reason)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
relaysString = fmt.Sprintf("%d/%d Available", overview.Relays.Available, overview.Relays.Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := "-"
|
||||||
|
if len(overview.Routes) > 0 {
|
||||||
|
sort.Strings(overview.Routes)
|
||||||
|
routes = strings.Join(overview.Routes, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
var dnsServersString string
|
||||||
|
if showNameServers {
|
||||||
|
for _, nsServerGroup := range overview.NSServerGroups {
|
||||||
|
enabled := "Available"
|
||||||
|
if !nsServerGroup.Enabled {
|
||||||
|
enabled = "Unavailable"
|
||||||
|
}
|
||||||
|
errorString := ""
|
||||||
|
if nsServerGroup.Error != "" {
|
||||||
|
errorString = fmt.Sprintf(", reason: %s", nsServerGroup.Error)
|
||||||
|
errorString = strings.TrimSpace(errorString)
|
||||||
|
}
|
||||||
|
|
||||||
|
domainsString := strings.Join(nsServerGroup.Domains, ", ")
|
||||||
|
if domainsString == "" {
|
||||||
|
domainsString = "." // Show "." for the default zone
|
||||||
|
}
|
||||||
|
dnsServersString += fmt.Sprintf(
|
||||||
|
"\n [%s] for [%s] is %s%s",
|
||||||
|
strings.Join(nsServerGroup.Servers, ", "),
|
||||||
|
domainsString,
|
||||||
|
enabled,
|
||||||
|
errorString,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dnsServersString = fmt.Sprintf("%d/%d Available", countEnabled(overview.NSServerGroups), len(overview.NSServerGroups))
|
||||||
|
}
|
||||||
|
|
||||||
|
rosenpassEnabledStatus := "false"
|
||||||
|
if overview.RosenpassEnabled {
|
||||||
|
rosenpassEnabledStatus = "true"
|
||||||
|
if overview.RosenpassPermissive {
|
||||||
|
rosenpassEnabledStatus = "true (permissive)" //nolint:gosec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
|
peersCountString := fmt.Sprintf("%d/%d Connected", overview.Peers.Connected, overview.Peers.Total)
|
||||||
|
|
||||||
|
goos := runtime.GOOS
|
||||||
|
goarch := runtime.GOARCH
|
||||||
|
goarm := ""
|
||||||
|
if goarch == "arm" {
|
||||||
|
goarm = fmt.Sprintf(" (ARMv%s)", os.Getenv("GOARM"))
|
||||||
|
}
|
||||||
|
|
||||||
summary := fmt.Sprintf(
|
summary := fmt.Sprintf(
|
||||||
|
"OS: %s\n"+
|
||||||
"Daemon version: %s\n"+
|
"Daemon version: %s\n"+
|
||||||
"CLI version: %s\n"+
|
"CLI version: %s\n"+
|
||||||
"Management: %s\n"+
|
"Management: %s\n"+
|
||||||
"Signal: %s\n"+
|
"Signal: %s\n"+
|
||||||
|
"Relays: %s\n"+
|
||||||
|
"Nameservers: %s\n"+
|
||||||
"FQDN: %s\n"+
|
"FQDN: %s\n"+
|
||||||
"NetBird IP: %s\n"+
|
"NetBird IP: %s\n"+
|
||||||
"Interface type: %s\n"+
|
"Interface type: %s\n"+
|
||||||
|
"Quantum resistance: %s\n"+
|
||||||
|
"Routes: %s\n"+
|
||||||
"Peers count: %s\n",
|
"Peers count: %s\n",
|
||||||
|
fmt.Sprintf("%s/%s%s", goos, goarch, goarm),
|
||||||
overview.DaemonVersion,
|
overview.DaemonVersion,
|
||||||
version.NetbirdVersion(),
|
version.NetbirdVersion(),
|
||||||
managementConnString,
|
managementConnString,
|
||||||
signalConnString,
|
signalConnString,
|
||||||
|
relaysString,
|
||||||
|
dnsServersString,
|
||||||
overview.FQDN,
|
overview.FQDN,
|
||||||
interfaceIP,
|
interfaceIP,
|
||||||
interfaceTypeString,
|
interfaceTypeString,
|
||||||
|
rosenpassEnabledStatus,
|
||||||
|
routes,
|
||||||
peersCountString,
|
peersCountString,
|
||||||
)
|
)
|
||||||
return summary
|
return summary
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseToFullDetailSummary(overview statusOutputOverview) string {
|
func parseToFullDetailSummary(overview statusOutputOverview) string {
|
||||||
parsedPeersString := parsePeers(overview.Peers)
|
parsedPeersString := parsePeers(overview.Peers, overview.RosenpassEnabled, overview.RosenpassPermissive)
|
||||||
summary := parseGeneralSummary(overview, true)
|
summary := parseGeneralSummary(overview, true, true, true)
|
||||||
|
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"Peers detail:"+
|
"Peers detail:"+
|
||||||
@@ -369,7 +584,7 @@ func parseToFullDetailSummary(overview statusOutputOverview) string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePeers(peers peersStateOutput) string {
|
func parsePeers(peers peersStateOutput, rosenpassEnabled, rosenpassPermissive bool) string {
|
||||||
var (
|
var (
|
||||||
peersString = ""
|
peersString = ""
|
||||||
)
|
)
|
||||||
@@ -386,6 +601,39 @@ func parsePeers(peers peersStateOutput) string {
|
|||||||
remoteICE = peerState.IceCandidateType.Remote
|
remoteICE = peerState.IceCandidateType.Remote
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localICEEndpoint := "-"
|
||||||
|
if peerState.IceCandidateEndpoint.Local != "" {
|
||||||
|
localICEEndpoint = peerState.IceCandidateEndpoint.Local
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteICEEndpoint := "-"
|
||||||
|
if peerState.IceCandidateEndpoint.Remote != "" {
|
||||||
|
remoteICEEndpoint = peerState.IceCandidateEndpoint.Remote
|
||||||
|
}
|
||||||
|
|
||||||
|
rosenpassEnabledStatus := "false"
|
||||||
|
if rosenpassEnabled {
|
||||||
|
if peerState.RosenpassEnabled {
|
||||||
|
rosenpassEnabledStatus = "true"
|
||||||
|
} else {
|
||||||
|
if rosenpassPermissive {
|
||||||
|
rosenpassEnabledStatus = "false (remote didn't enable quantum resistance)"
|
||||||
|
} else {
|
||||||
|
rosenpassEnabledStatus = "false (connection won't work without a permissive mode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if peerState.RosenpassEnabled {
|
||||||
|
rosenpassEnabledStatus = "false (connection might not work without a remote permissive mode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := "-"
|
||||||
|
if len(peerState.Routes) > 0 {
|
||||||
|
sort.Strings(peerState.Routes)
|
||||||
|
routes = strings.Join(peerState.Routes, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
peerString := fmt.Sprintf(
|
peerString := fmt.Sprintf(
|
||||||
"\n %s:\n"+
|
"\n %s:\n"+
|
||||||
" NetBird IP: %s\n"+
|
" NetBird IP: %s\n"+
|
||||||
@@ -395,7 +643,13 @@ func parsePeers(peers peersStateOutput) string {
|
|||||||
" Connection type: %s\n"+
|
" Connection type: %s\n"+
|
||||||
" Direct: %t\n"+
|
" Direct: %t\n"+
|
||||||
" ICE candidate (Local/Remote): %s/%s\n"+
|
" ICE candidate (Local/Remote): %s/%s\n"+
|
||||||
" Last connection update: %s\n",
|
" ICE candidate endpoints (Local/Remote): %s/%s\n"+
|
||||||
|
" Last connection update: %s\n"+
|
||||||
|
" Last WireGuard handshake: %s\n"+
|
||||||
|
" Transfer status (received/sent) %s/%s\n"+
|
||||||
|
" Quantum resistance: %s\n"+
|
||||||
|
" Routes: %s\n"+
|
||||||
|
" Latency: %s\n",
|
||||||
peerState.FQDN,
|
peerState.FQDN,
|
||||||
peerState.IP,
|
peerState.IP,
|
||||||
peerState.PubKey,
|
peerState.PubKey,
|
||||||
@@ -404,10 +658,18 @@ func parsePeers(peers peersStateOutput) string {
|
|||||||
peerState.Direct,
|
peerState.Direct,
|
||||||
localICE,
|
localICE,
|
||||||
remoteICE,
|
remoteICE,
|
||||||
peerState.LastStatusUpdate.Format("2006-01-02 15:04:05"),
|
localICEEndpoint,
|
||||||
|
remoteICEEndpoint,
|
||||||
|
timeAgo(peerState.LastStatusUpdate),
|
||||||
|
timeAgo(peerState.LastWireguardHandshake),
|
||||||
|
toIEC(peerState.TransferReceived),
|
||||||
|
toIEC(peerState.TransferSent),
|
||||||
|
rosenpassEnabledStatus,
|
||||||
|
routes,
|
||||||
|
peerState.Latency.String(),
|
||||||
)
|
)
|
||||||
|
|
||||||
peersString = peersString + peerString
|
peersString += peerString
|
||||||
}
|
}
|
||||||
return peersString
|
return peersString
|
||||||
}
|
}
|
||||||
@@ -415,6 +677,7 @@ func parsePeers(peers peersStateOutput) string {
|
|||||||
func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
|
func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
|
||||||
statusEval := false
|
statusEval := false
|
||||||
ipEval := false
|
ipEval := false
|
||||||
|
nameEval := false
|
||||||
|
|
||||||
if statusFilter != "" {
|
if statusFilter != "" {
|
||||||
lowerStatusFilter := strings.ToLower(statusFilter)
|
lowerStatusFilter := strings.ToLower(statusFilter)
|
||||||
@@ -431,5 +694,170 @@ func skipDetailByFilters(peerState *proto.PeerState, isConnected bool) bool {
|
|||||||
ipEval = true
|
ipEval = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return statusEval || ipEval
|
|
||||||
|
if len(prefixNamesFilter) > 0 {
|
||||||
|
for prefixNameFilter := range prefixNamesFilterMap {
|
||||||
|
if !strings.HasPrefix(peerState.Fqdn, prefixNameFilter) {
|
||||||
|
nameEval = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusEval || ipEval || nameEval
|
||||||
|
}
|
||||||
|
|
||||||
|
func toIEC(b int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if b < unit {
|
||||||
|
return fmt.Sprintf("%d B", b)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := b / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %ciB",
|
||||||
|
float64(b)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
||||||
|
|
||||||
|
func countEnabled(dnsServers []nsServerGroupStateOutput) int {
|
||||||
|
count := 0
|
||||||
|
for _, server := range dnsServers {
|
||||||
|
if server.Enabled {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// timeAgo returns a string representing the duration since the provided time in a human-readable format.
|
||||||
|
func timeAgo(t time.Time) string {
|
||||||
|
if t.IsZero() || t.Equal(time.Unix(0, 0)) {
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
duration := time.Since(t)
|
||||||
|
switch {
|
||||||
|
case duration < time.Second:
|
||||||
|
return "Now"
|
||||||
|
case duration < time.Minute:
|
||||||
|
seconds := int(duration.Seconds())
|
||||||
|
if seconds == 1 {
|
||||||
|
return "1 second ago"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d seconds ago", seconds)
|
||||||
|
case duration < time.Hour:
|
||||||
|
minutes := int(duration.Minutes())
|
||||||
|
seconds := int(duration.Seconds()) % 60
|
||||||
|
if minutes == 1 {
|
||||||
|
if seconds == 1 {
|
||||||
|
return "1 minute, 1 second ago"
|
||||||
|
} else if seconds > 0 {
|
||||||
|
return fmt.Sprintf("1 minute, %d seconds ago", seconds)
|
||||||
|
}
|
||||||
|
return "1 minute ago"
|
||||||
|
}
|
||||||
|
if seconds > 0 {
|
||||||
|
return fmt.Sprintf("%d minutes, %d seconds ago", minutes, seconds)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d minutes ago", minutes)
|
||||||
|
case duration < 24*time.Hour:
|
||||||
|
hours := int(duration.Hours())
|
||||||
|
minutes := int(duration.Minutes()) % 60
|
||||||
|
if hours == 1 {
|
||||||
|
if minutes == 1 {
|
||||||
|
return "1 hour, 1 minute ago"
|
||||||
|
} else if minutes > 0 {
|
||||||
|
return fmt.Sprintf("1 hour, %d minutes ago", minutes)
|
||||||
|
}
|
||||||
|
return "1 hour ago"
|
||||||
|
}
|
||||||
|
if minutes > 0 {
|
||||||
|
return fmt.Sprintf("%d hours, %d minutes ago", hours, minutes)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d hours ago", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
days := int(duration.Hours()) / 24
|
||||||
|
hours := int(duration.Hours()) % 24
|
||||||
|
if days == 1 {
|
||||||
|
if hours == 1 {
|
||||||
|
return "1 day, 1 hour ago"
|
||||||
|
} else if hours > 0 {
|
||||||
|
return fmt.Sprintf("1 day, %d hours ago", hours)
|
||||||
|
}
|
||||||
|
return "1 day ago"
|
||||||
|
}
|
||||||
|
if hours > 0 {
|
||||||
|
return fmt.Sprintf("%d days, %d hours ago", days, hours)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d days ago", days)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) {
|
||||||
|
peer.FQDN = a.AnonymizeDomain(peer.FQDN)
|
||||||
|
if localIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Local); err == nil {
|
||||||
|
peer.IceCandidateEndpoint.Local = fmt.Sprintf("%s:%s", a.AnonymizeIPString(localIP), port)
|
||||||
|
}
|
||||||
|
if remoteIP, port, err := net.SplitHostPort(peer.IceCandidateEndpoint.Remote); err == nil {
|
||||||
|
peer.IceCandidateEndpoint.Remote = fmt.Sprintf("%s:%s", a.AnonymizeIPString(remoteIP), port)
|
||||||
|
}
|
||||||
|
for i, route := range peer.Routes {
|
||||||
|
peer.Routes[i] = a.AnonymizeIPString(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, route := range peer.Routes {
|
||||||
|
peer.Routes[i] = anonymizeRoute(a, route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview) {
|
||||||
|
for i, peer := range overview.Peers.Details {
|
||||||
|
peer := peer
|
||||||
|
anonymizePeerDetail(a, &peer)
|
||||||
|
overview.Peers.Details[i] = peer
|
||||||
|
}
|
||||||
|
|
||||||
|
overview.ManagementState.URL = a.AnonymizeURI(overview.ManagementState.URL)
|
||||||
|
overview.ManagementState.Error = a.AnonymizeString(overview.ManagementState.Error)
|
||||||
|
overview.SignalState.URL = a.AnonymizeURI(overview.SignalState.URL)
|
||||||
|
overview.SignalState.Error = a.AnonymizeString(overview.SignalState.Error)
|
||||||
|
|
||||||
|
overview.IP = a.AnonymizeIPString(overview.IP)
|
||||||
|
for i, detail := range overview.Relays.Details {
|
||||||
|
detail.URI = a.AnonymizeURI(detail.URI)
|
||||||
|
detail.Error = a.AnonymizeString(detail.Error)
|
||||||
|
overview.Relays.Details[i] = detail
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, nsGroup := range overview.NSServerGroups {
|
||||||
|
for j, domain := range nsGroup.Domains {
|
||||||
|
overview.NSServerGroups[i].Domains[j] = a.AnonymizeDomain(domain)
|
||||||
|
}
|
||||||
|
for j, ns := range nsGroup.Servers {
|
||||||
|
host, port, err := net.SplitHostPort(ns)
|
||||||
|
if err == nil {
|
||||||
|
overview.NSServerGroups[i].Servers[j] = fmt.Sprintf("%s:%s", a.AnonymizeIPString(host), port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, route := range overview.Routes {
|
||||||
|
overview.Routes[i] = anonymizeRoute(a, route)
|
||||||
|
}
|
||||||
|
|
||||||
|
overview.FQDN = a.AnonymizeDomain(overview.FQDN)
|
||||||
|
}
|
||||||
|
|
||||||
|
func anonymizeRoute(a *anonymize.Anonymizer, route string) string {
|
||||||
|
prefix, err := netip.ParsePrefix(route)
|
||||||
|
if err == nil {
|
||||||
|
ip := a.AnonymizeIPString(prefix.Addr().String())
|
||||||
|
return fmt.Sprintf("%s/%d", ip, prefix.Bits())
|
||||||
|
}
|
||||||
|
domains := strings.Split(route, ", ")
|
||||||
|
for i, domain := range domains {
|
||||||
|
domains[i] = a.AnonymizeDomain(domain)
|
||||||
|
}
|
||||||
|
return strings.Join(domains, ", ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
@@ -34,6 +40,15 @@ var resp = &proto.StatusResponse{
|
|||||||
Direct: true,
|
Direct: true,
|
||||||
LocalIceCandidateType: "",
|
LocalIceCandidateType: "",
|
||||||
RemoteIceCandidateType: "",
|
RemoteIceCandidateType: "",
|
||||||
|
LocalIceCandidateEndpoint: "",
|
||||||
|
RemoteIceCandidateEndpoint: "",
|
||||||
|
LastWireguardHandshake: timestamppb.New(time.Date(2001, time.Month(1), 1, 1, 1, 2, 0, time.UTC)),
|
||||||
|
BytesRx: 200,
|
||||||
|
BytesTx: 100,
|
||||||
|
Routes: []string{
|
||||||
|
"10.1.0.0/24",
|
||||||
|
},
|
||||||
|
Latency: durationpb.New(time.Duration(10000000)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IP: "192.168.178.102",
|
IP: "192.168.178.102",
|
||||||
@@ -45,21 +60,66 @@ var resp = &proto.StatusResponse{
|
|||||||
Direct: false,
|
Direct: false,
|
||||||
LocalIceCandidateType: "relay",
|
LocalIceCandidateType: "relay",
|
||||||
RemoteIceCandidateType: "prflx",
|
RemoteIceCandidateType: "prflx",
|
||||||
|
LocalIceCandidateEndpoint: "10.0.0.1:10001",
|
||||||
|
RemoteIceCandidateEndpoint: "10.0.10.1:10002",
|
||||||
|
LastWireguardHandshake: timestamppb.New(time.Date(2002, time.Month(2), 2, 2, 2, 3, 0, time.UTC)),
|
||||||
|
BytesRx: 2000,
|
||||||
|
BytesTx: 1000,
|
||||||
|
Latency: durationpb.New(time.Duration(10000000)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ManagementState: &proto.ManagementState{
|
ManagementState: &proto.ManagementState{
|
||||||
URL: "my-awesome-management.com:443",
|
URL: "my-awesome-management.com:443",
|
||||||
Connected: true,
|
Connected: true,
|
||||||
|
Error: "",
|
||||||
},
|
},
|
||||||
SignalState: &proto.SignalState{
|
SignalState: &proto.SignalState{
|
||||||
URL: "my-awesome-signal.com:443",
|
URL: "my-awesome-signal.com:443",
|
||||||
Connected: true,
|
Connected: true,
|
||||||
|
Error: "",
|
||||||
|
},
|
||||||
|
Relays: []*proto.RelayState{
|
||||||
|
{
|
||||||
|
URI: "stun:my-awesome-stun.com:3478",
|
||||||
|
Available: true,
|
||||||
|
Error: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URI: "turns:my-awesome-turn.com:443?transport=tcp",
|
||||||
|
Available: false,
|
||||||
|
Error: "context: deadline exceeded",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
LocalPeerState: &proto.LocalPeerState{
|
LocalPeerState: &proto.LocalPeerState{
|
||||||
IP: "192.168.178.100/16",
|
IP: "192.168.178.100/16",
|
||||||
PubKey: "Some-Pub-Key",
|
PubKey: "Some-Pub-Key",
|
||||||
KernelInterface: true,
|
KernelInterface: true,
|
||||||
Fqdn: "some-localhost.awesome-domain.com",
|
Fqdn: "some-localhost.awesome-domain.com",
|
||||||
|
Routes: []string{
|
||||||
|
"10.10.0.0/24",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
DnsServers: []*proto.NSGroupState{
|
||||||
|
{
|
||||||
|
Servers: []string{
|
||||||
|
"8.8.8.8:53",
|
||||||
|
},
|
||||||
|
Domains: nil,
|
||||||
|
Enabled: true,
|
||||||
|
Error: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Servers: []string{
|
||||||
|
"1.1.1.1:53",
|
||||||
|
"2.2.2.2:53",
|
||||||
|
},
|
||||||
|
Domains: []string{
|
||||||
|
"example.com",
|
||||||
|
"example.net",
|
||||||
|
},
|
||||||
|
Enabled: false,
|
||||||
|
Error: "timeout",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
DaemonVersion: "0.14.1",
|
DaemonVersion: "0.14.1",
|
||||||
@@ -82,6 +142,17 @@ var overview = statusOutputOverview{
|
|||||||
Local: "",
|
Local: "",
|
||||||
Remote: "",
|
Remote: "",
|
||||||
},
|
},
|
||||||
|
IceCandidateEndpoint: iceCandidateType{
|
||||||
|
Local: "",
|
||||||
|
Remote: "",
|
||||||
|
},
|
||||||
|
LastWireguardHandshake: time.Date(2001, 1, 1, 1, 1, 2, 0, time.UTC),
|
||||||
|
TransferReceived: 200,
|
||||||
|
TransferSent: 100,
|
||||||
|
Routes: []string{
|
||||||
|
"10.1.0.0/24",
|
||||||
|
},
|
||||||
|
Latency: time.Duration(10000000),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
IP: "192.168.178.102",
|
IP: "192.168.178.102",
|
||||||
@@ -95,6 +166,14 @@ var overview = statusOutputOverview{
|
|||||||
Local: "relay",
|
Local: "relay",
|
||||||
Remote: "prflx",
|
Remote: "prflx",
|
||||||
},
|
},
|
||||||
|
IceCandidateEndpoint: iceCandidateType{
|
||||||
|
Local: "10.0.0.1:10001",
|
||||||
|
Remote: "10.0.10.1:10002",
|
||||||
|
},
|
||||||
|
LastWireguardHandshake: time.Date(2002, 2, 2, 2, 2, 3, 0, time.UTC),
|
||||||
|
TransferReceived: 2000,
|
||||||
|
TransferSent: 1000,
|
||||||
|
Latency: time.Duration(10000000),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -103,15 +182,58 @@ var overview = statusOutputOverview{
|
|||||||
ManagementState: managementStateOutput{
|
ManagementState: managementStateOutput{
|
||||||
URL: "my-awesome-management.com:443",
|
URL: "my-awesome-management.com:443",
|
||||||
Connected: true,
|
Connected: true,
|
||||||
|
Error: "",
|
||||||
},
|
},
|
||||||
SignalState: signalStateOutput{
|
SignalState: signalStateOutput{
|
||||||
URL: "my-awesome-signal.com:443",
|
URL: "my-awesome-signal.com:443",
|
||||||
Connected: true,
|
Connected: true,
|
||||||
|
Error: "",
|
||||||
|
},
|
||||||
|
Relays: relayStateOutput{
|
||||||
|
Total: 2,
|
||||||
|
Available: 1,
|
||||||
|
Details: []relayStateOutputDetail{
|
||||||
|
{
|
||||||
|
URI: "stun:my-awesome-stun.com:3478",
|
||||||
|
Available: true,
|
||||||
|
Error: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URI: "turns:my-awesome-turn.com:443?transport=tcp",
|
||||||
|
Available: false,
|
||||||
|
Error: "context: deadline exceeded",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
IP: "192.168.178.100/16",
|
IP: "192.168.178.100/16",
|
||||||
PubKey: "Some-Pub-Key",
|
PubKey: "Some-Pub-Key",
|
||||||
KernelInterface: true,
|
KernelInterface: true,
|
||||||
FQDN: "some-localhost.awesome-domain.com",
|
FQDN: "some-localhost.awesome-domain.com",
|
||||||
|
NSServerGroups: []nsServerGroupStateOutput{
|
||||||
|
{
|
||||||
|
Servers: []string{
|
||||||
|
"8.8.8.8:53",
|
||||||
|
},
|
||||||
|
Domains: nil,
|
||||||
|
Enabled: true,
|
||||||
|
Error: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Servers: []string{
|
||||||
|
"1.1.1.1:53",
|
||||||
|
"2.2.2.2:53",
|
||||||
|
},
|
||||||
|
Domains: []string{
|
||||||
|
"example.com",
|
||||||
|
"example.net",
|
||||||
|
},
|
||||||
|
Enabled: false,
|
||||||
|
Error: "timeout",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Routes: []string{
|
||||||
|
"10.10.0.0/24",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
|
func TestConversionFromFullStatusToOutputOverview(t *testing.T) {
|
||||||
@@ -145,158 +267,309 @@ func TestSortingOfPeers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParsingToJSON(t *testing.T) {
|
func TestParsingToJSON(t *testing.T) {
|
||||||
json, _ := parseToJSON(overview)
|
jsonString, _ := parseToJSON(overview)
|
||||||
|
|
||||||
//@formatter:off
|
//@formatter:off
|
||||||
expectedJSON := "{\"" +
|
expectedJSONString := `
|
||||||
"peers\":" +
|
{
|
||||||
"{" +
|
"peers": {
|
||||||
"\"total\":2," +
|
"total": 2,
|
||||||
"\"connected\":2," +
|
"connected": 2,
|
||||||
"\"details\":" +
|
"details": [
|
||||||
"[" +
|
{
|
||||||
"{" +
|
"fqdn": "peer-1.awesome-domain.com",
|
||||||
"\"fqdn\":\"peer-1.awesome-domain.com\"," +
|
"netbirdIp": "192.168.178.101",
|
||||||
"\"netbirdIp\":\"192.168.178.101\"," +
|
"publicKey": "Pubkey1",
|
||||||
"\"publicKey\":\"Pubkey1\"," +
|
"status": "Connected",
|
||||||
"\"status\":\"Connected\"," +
|
"lastStatusUpdate": "2001-01-01T01:01:01Z",
|
||||||
"\"lastStatusUpdate\":\"2001-01-01T01:01:01Z\"," +
|
"connectionType": "P2P",
|
||||||
"\"connectionType\":\"P2P\"," +
|
"direct": true,
|
||||||
"\"direct\":true," +
|
"iceCandidateType": {
|
||||||
"\"iceCandidateType\":" +
|
"local": "",
|
||||||
"{" +
|
"remote": ""
|
||||||
"\"local\":\"\"," +
|
},
|
||||||
"\"remote\":\"\"" +
|
"iceCandidateEndpoint": {
|
||||||
"}" +
|
"local": "",
|
||||||
"}," +
|
"remote": ""
|
||||||
"{" +
|
},
|
||||||
"\"fqdn\":\"peer-2.awesome-domain.com\"," +
|
"lastWireguardHandshake": "2001-01-01T01:01:02Z",
|
||||||
"\"netbirdIp\":\"192.168.178.102\"," +
|
"transferReceived": 200,
|
||||||
"\"publicKey\":\"Pubkey2\"," +
|
"transferSent": 100,
|
||||||
"\"status\":\"Connected\"," +
|
"latency": 10000000,
|
||||||
"\"lastStatusUpdate\":\"2002-02-02T02:02:02Z\"," +
|
"quantumResistance": false,
|
||||||
"\"connectionType\":\"Relayed\"," +
|
"routes": [
|
||||||
"\"direct\":false," +
|
"10.1.0.0/24"
|
||||||
"\"iceCandidateType\":" +
|
]
|
||||||
"{" +
|
},
|
||||||
"\"local\":\"relay\"," +
|
{
|
||||||
"\"remote\":\"prflx\"" +
|
"fqdn": "peer-2.awesome-domain.com",
|
||||||
"}" +
|
"netbirdIp": "192.168.178.102",
|
||||||
"}" +
|
"publicKey": "Pubkey2",
|
||||||
"]" +
|
"status": "Connected",
|
||||||
"}," +
|
"lastStatusUpdate": "2002-02-02T02:02:02Z",
|
||||||
"\"cliVersion\":\"development\"," +
|
"connectionType": "Relayed",
|
||||||
"\"daemonVersion\":\"0.14.1\"," +
|
"direct": false,
|
||||||
"\"management\":" +
|
"iceCandidateType": {
|
||||||
"{" +
|
"local": "relay",
|
||||||
"\"url\":\"my-awesome-management.com:443\"," +
|
"remote": "prflx"
|
||||||
"\"connected\":true" +
|
},
|
||||||
"}," +
|
"iceCandidateEndpoint": {
|
||||||
"\"signal\":" +
|
"local": "10.0.0.1:10001",
|
||||||
"{\"" +
|
"remote": "10.0.10.1:10002"
|
||||||
"url\":\"my-awesome-signal.com:443\"," +
|
},
|
||||||
"\"connected\":true" +
|
"lastWireguardHandshake": "2002-02-02T02:02:03Z",
|
||||||
"}," +
|
"transferReceived": 2000,
|
||||||
"\"netbirdIp\":\"192.168.178.100/16\"," +
|
"transferSent": 1000,
|
||||||
"\"publicKey\":\"Some-Pub-Key\"," +
|
"latency": 10000000,
|
||||||
"\"usesKernelInterface\":true," +
|
"quantumResistance": false,
|
||||||
"\"fqdn\":\"some-localhost.awesome-domain.com\"" +
|
"routes": null
|
||||||
"}"
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"cliVersion": "development",
|
||||||
|
"daemonVersion": "0.14.1",
|
||||||
|
"management": {
|
||||||
|
"url": "my-awesome-management.com:443",
|
||||||
|
"connected": true,
|
||||||
|
"error": ""
|
||||||
|
},
|
||||||
|
"signal": {
|
||||||
|
"url": "my-awesome-signal.com:443",
|
||||||
|
"connected": true,
|
||||||
|
"error": ""
|
||||||
|
},
|
||||||
|
"relays": {
|
||||||
|
"total": 2,
|
||||||
|
"available": 1,
|
||||||
|
"details": [
|
||||||
|
{
|
||||||
|
"uri": "stun:my-awesome-stun.com:3478",
|
||||||
|
"available": true,
|
||||||
|
"error": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uri": "turns:my-awesome-turn.com:443?transport=tcp",
|
||||||
|
"available": false,
|
||||||
|
"error": "context: deadline exceeded"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"netbirdIp": "192.168.178.100/16",
|
||||||
|
"publicKey": "Some-Pub-Key",
|
||||||
|
"usesKernelInterface": true,
|
||||||
|
"fqdn": "some-localhost.awesome-domain.com",
|
||||||
|
"quantumResistance": false,
|
||||||
|
"quantumResistancePermissive": false,
|
||||||
|
"routes": [
|
||||||
|
"10.10.0.0/24"
|
||||||
|
],
|
||||||
|
"dnsServers": [
|
||||||
|
{
|
||||||
|
"servers": [
|
||||||
|
"8.8.8.8:53"
|
||||||
|
],
|
||||||
|
"domains": null,
|
||||||
|
"enabled": true,
|
||||||
|
"error": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"servers": [
|
||||||
|
"1.1.1.1:53",
|
||||||
|
"2.2.2.2:53"
|
||||||
|
],
|
||||||
|
"domains": [
|
||||||
|
"example.com",
|
||||||
|
"example.net"
|
||||||
|
],
|
||||||
|
"enabled": false,
|
||||||
|
"error": "timeout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
assert.Equal(t, expectedJSON, json)
|
var expectedJSON bytes.Buffer
|
||||||
|
require.NoError(t, json.Compact(&expectedJSON, []byte(expectedJSONString)))
|
||||||
|
|
||||||
|
assert.Equal(t, expectedJSON.String(), jsonString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsingToYAML(t *testing.T) {
|
func TestParsingToYAML(t *testing.T) {
|
||||||
yaml, _ := parseToYAML(overview)
|
yaml, _ := parseToYAML(overview)
|
||||||
|
|
||||||
expectedYAML := "peers:\n" +
|
expectedYAML :=
|
||||||
" total: 2\n" +
|
`peers:
|
||||||
" connected: 2\n" +
|
total: 2
|
||||||
" details:\n" +
|
connected: 2
|
||||||
" - fqdn: peer-1.awesome-domain.com\n" +
|
details:
|
||||||
" netbirdIp: 192.168.178.101\n" +
|
- fqdn: peer-1.awesome-domain.com
|
||||||
" publicKey: Pubkey1\n" +
|
netbirdIp: 192.168.178.101
|
||||||
" status: Connected\n" +
|
publicKey: Pubkey1
|
||||||
" lastStatusUpdate: 2001-01-01T01:01:01Z\n" +
|
status: Connected
|
||||||
" connectionType: P2P\n" +
|
lastStatusUpdate: 2001-01-01T01:01:01Z
|
||||||
" direct: true\n" +
|
connectionType: P2P
|
||||||
" iceCandidateType:\n" +
|
direct: true
|
||||||
" local: \"\"\n" +
|
iceCandidateType:
|
||||||
" remote: \"\"\n" +
|
local: ""
|
||||||
" - fqdn: peer-2.awesome-domain.com\n" +
|
remote: ""
|
||||||
" netbirdIp: 192.168.178.102\n" +
|
iceCandidateEndpoint:
|
||||||
" publicKey: Pubkey2\n" +
|
local: ""
|
||||||
" status: Connected\n" +
|
remote: ""
|
||||||
" lastStatusUpdate: 2002-02-02T02:02:02Z\n" +
|
lastWireguardHandshake: 2001-01-01T01:01:02Z
|
||||||
" connectionType: Relayed\n" +
|
transferReceived: 200
|
||||||
" direct: false\n" +
|
transferSent: 100
|
||||||
" iceCandidateType:\n" +
|
latency: 10ms
|
||||||
" local: relay\n" +
|
quantumResistance: false
|
||||||
" remote: prflx\n" +
|
routes:
|
||||||
"cliVersion: development\n" +
|
- 10.1.0.0/24
|
||||||
"daemonVersion: 0.14.1\n" +
|
- fqdn: peer-2.awesome-domain.com
|
||||||
"management:\n" +
|
netbirdIp: 192.168.178.102
|
||||||
" url: my-awesome-management.com:443\n" +
|
publicKey: Pubkey2
|
||||||
" connected: true\n" +
|
status: Connected
|
||||||
"signal:\n" +
|
lastStatusUpdate: 2002-02-02T02:02:02Z
|
||||||
" url: my-awesome-signal.com:443\n" +
|
connectionType: Relayed
|
||||||
" connected: true\n" +
|
direct: false
|
||||||
"netbirdIp: 192.168.178.100/16\n" +
|
iceCandidateType:
|
||||||
"publicKey: Some-Pub-Key\n" +
|
local: relay
|
||||||
"usesKernelInterface: true\n" +
|
remote: prflx
|
||||||
"fqdn: some-localhost.awesome-domain.com\n"
|
iceCandidateEndpoint:
|
||||||
|
local: 10.0.0.1:10001
|
||||||
|
remote: 10.0.10.1:10002
|
||||||
|
lastWireguardHandshake: 2002-02-02T02:02:03Z
|
||||||
|
transferReceived: 2000
|
||||||
|
transferSent: 1000
|
||||||
|
latency: 10ms
|
||||||
|
quantumResistance: false
|
||||||
|
routes: []
|
||||||
|
cliVersion: development
|
||||||
|
daemonVersion: 0.14.1
|
||||||
|
management:
|
||||||
|
url: my-awesome-management.com:443
|
||||||
|
connected: true
|
||||||
|
error: ""
|
||||||
|
signal:
|
||||||
|
url: my-awesome-signal.com:443
|
||||||
|
connected: true
|
||||||
|
error: ""
|
||||||
|
relays:
|
||||||
|
total: 2
|
||||||
|
available: 1
|
||||||
|
details:
|
||||||
|
- uri: stun:my-awesome-stun.com:3478
|
||||||
|
available: true
|
||||||
|
error: ""
|
||||||
|
- uri: turns:my-awesome-turn.com:443?transport=tcp
|
||||||
|
available: false
|
||||||
|
error: 'context: deadline exceeded'
|
||||||
|
netbirdIp: 192.168.178.100/16
|
||||||
|
publicKey: Some-Pub-Key
|
||||||
|
usesKernelInterface: true
|
||||||
|
fqdn: some-localhost.awesome-domain.com
|
||||||
|
quantumResistance: false
|
||||||
|
quantumResistancePermissive: false
|
||||||
|
routes:
|
||||||
|
- 10.10.0.0/24
|
||||||
|
dnsServers:
|
||||||
|
- servers:
|
||||||
|
- 8.8.8.8:53
|
||||||
|
domains: []
|
||||||
|
enabled: true
|
||||||
|
error: ""
|
||||||
|
- servers:
|
||||||
|
- 1.1.1.1:53
|
||||||
|
- 2.2.2.2:53
|
||||||
|
domains:
|
||||||
|
- example.com
|
||||||
|
- example.net
|
||||||
|
enabled: false
|
||||||
|
error: timeout
|
||||||
|
`
|
||||||
|
|
||||||
assert.Equal(t, expectedYAML, yaml)
|
assert.Equal(t, expectedYAML, yaml)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsingToDetail(t *testing.T) {
|
func TestParsingToDetail(t *testing.T) {
|
||||||
|
// Calculate time ago based on the fixture dates
|
||||||
|
lastConnectionUpdate1 := timeAgo(overview.Peers.Details[0].LastStatusUpdate)
|
||||||
|
lastHandshake1 := timeAgo(overview.Peers.Details[0].LastWireguardHandshake)
|
||||||
|
lastConnectionUpdate2 := timeAgo(overview.Peers.Details[1].LastStatusUpdate)
|
||||||
|
lastHandshake2 := timeAgo(overview.Peers.Details[1].LastWireguardHandshake)
|
||||||
|
|
||||||
detail := parseToFullDetailSummary(overview)
|
detail := parseToFullDetailSummary(overview)
|
||||||
|
|
||||||
expectedDetail := "Peers detail:\n" +
|
expectedDetail := fmt.Sprintf(
|
||||||
" peer-1.awesome-domain.com:\n" +
|
`Peers detail:
|
||||||
" NetBird IP: 192.168.178.101\n" +
|
peer-1.awesome-domain.com:
|
||||||
" Public key: Pubkey1\n" +
|
NetBird IP: 192.168.178.101
|
||||||
" Status: Connected\n" +
|
Public key: Pubkey1
|
||||||
" -- detail --\n" +
|
Status: Connected
|
||||||
" Connection type: P2P\n" +
|
-- detail --
|
||||||
" Direct: true\n" +
|
Connection type: P2P
|
||||||
" ICE candidate (Local/Remote): -/-\n" +
|
Direct: true
|
||||||
" Last connection update: 2001-01-01 01:01:01\n" +
|
ICE candidate (Local/Remote): -/-
|
||||||
"\n" +
|
ICE candidate endpoints (Local/Remote): -/-
|
||||||
" peer-2.awesome-domain.com:\n" +
|
Last connection update: %s
|
||||||
" NetBird IP: 192.168.178.102\n" +
|
Last WireGuard handshake: %s
|
||||||
" Public key: Pubkey2\n" +
|
Transfer status (received/sent) 200 B/100 B
|
||||||
" Status: Connected\n" +
|
Quantum resistance: false
|
||||||
" -- detail --\n" +
|
Routes: 10.1.0.0/24
|
||||||
" Connection type: Relayed\n" +
|
Latency: 10ms
|
||||||
" Direct: false\n" +
|
|
||||||
" ICE candidate (Local/Remote): relay/prflx\n" +
|
peer-2.awesome-domain.com:
|
||||||
" Last connection update: 2002-02-02 02:02:02\n" +
|
NetBird IP: 192.168.178.102
|
||||||
"\n" +
|
Public key: Pubkey2
|
||||||
"Daemon version: 0.14.1\n" +
|
Status: Connected
|
||||||
"CLI version: development\n" +
|
-- detail --
|
||||||
"Management: Connected to my-awesome-management.com:443\n" +
|
Connection type: Relayed
|
||||||
"Signal: Connected to my-awesome-signal.com:443\n" +
|
Direct: false
|
||||||
"FQDN: some-localhost.awesome-domain.com\n" +
|
ICE candidate (Local/Remote): relay/prflx
|
||||||
"NetBird IP: 192.168.178.100/16\n" +
|
ICE candidate endpoints (Local/Remote): 10.0.0.1:10001/10.0.10.1:10002
|
||||||
"Interface type: Kernel\n" +
|
Last connection update: %s
|
||||||
"Peers count: 2/2 Connected\n"
|
Last WireGuard handshake: %s
|
||||||
|
Transfer status (received/sent) 2.0 KiB/1000 B
|
||||||
|
Quantum resistance: false
|
||||||
|
Routes: -
|
||||||
|
Latency: 10ms
|
||||||
|
|
||||||
|
OS: %s/%s
|
||||||
|
Daemon version: 0.14.1
|
||||||
|
CLI version: %s
|
||||||
|
Management: Connected to my-awesome-management.com:443
|
||||||
|
Signal: Connected to my-awesome-signal.com:443
|
||||||
|
Relays:
|
||||||
|
[stun:my-awesome-stun.com:3478] is Available
|
||||||
|
[turns:my-awesome-turn.com:443?transport=tcp] is Unavailable, reason: context: deadline exceeded
|
||||||
|
Nameservers:
|
||||||
|
[8.8.8.8:53] for [.] is Available
|
||||||
|
[1.1.1.1:53, 2.2.2.2:53] for [example.com, example.net] is Unavailable, reason: timeout
|
||||||
|
FQDN: some-localhost.awesome-domain.com
|
||||||
|
NetBird IP: 192.168.178.100/16
|
||||||
|
Interface type: Kernel
|
||||||
|
Quantum resistance: false
|
||||||
|
Routes: 10.10.0.0/24
|
||||||
|
Peers count: 2/2 Connected
|
||||||
|
`, lastConnectionUpdate1, lastHandshake1, lastConnectionUpdate2, lastHandshake2, runtime.GOOS, runtime.GOARCH, overview.CliVersion)
|
||||||
|
|
||||||
assert.Equal(t, expectedDetail, detail)
|
assert.Equal(t, expectedDetail, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParsingToShortVersion(t *testing.T) {
|
func TestParsingToShortVersion(t *testing.T) {
|
||||||
shortVersion := parseGeneralSummary(overview, false)
|
shortVersion := parseGeneralSummary(overview, false, false, false)
|
||||||
|
|
||||||
expectedString := "Daemon version: 0.14.1\n" +
|
expectedString := fmt.Sprintf("OS: %s/%s", runtime.GOOS, runtime.GOARCH) + `
|
||||||
"CLI version: development\n" +
|
Daemon version: 0.14.1
|
||||||
"Management: Connected\n" +
|
CLI version: development
|
||||||
"Signal: Connected\n" +
|
Management: Connected
|
||||||
"FQDN: some-localhost.awesome-domain.com\n" +
|
Signal: Connected
|
||||||
"NetBird IP: 192.168.178.100/16\n" +
|
Relays: 1/2 Available
|
||||||
"Interface type: Kernel\n" +
|
Nameservers: 1/2 Available
|
||||||
"Peers count: 2/2 Connected\n"
|
FQDN: some-localhost.awesome-domain.com
|
||||||
|
NetBird IP: 192.168.178.100/16
|
||||||
|
Interface type: Kernel
|
||||||
|
Quantum resistance: false
|
||||||
|
Routes: 10.10.0.0/24
|
||||||
|
Peers count: 2/2 Connected
|
||||||
|
`
|
||||||
|
|
||||||
assert.Equal(t, expectedString, shortVersion)
|
assert.Equal(t, expectedString, shortVersion)
|
||||||
}
|
}
|
||||||
@@ -308,3 +581,31 @@ func TestParsingOfIP(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "192.168.178.123\n", parsedIP)
|
assert.Equal(t, "192.168.178.123\n", parsedIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTimeAgo(t *testing.T) {
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
input time.Time
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"Now", now, "Now"},
|
||||||
|
{"Seconds ago", now.Add(-10 * time.Second), "10 seconds ago"},
|
||||||
|
{"One minute ago", now.Add(-1 * time.Minute), "1 minute ago"},
|
||||||
|
{"Minutes and seconds ago", now.Add(-(1*time.Minute + 30*time.Second)), "1 minute, 30 seconds ago"},
|
||||||
|
{"One hour ago", now.Add(-1 * time.Hour), "1 hour ago"},
|
||||||
|
{"Hours and minutes ago", now.Add(-(2*time.Hour + 15*time.Minute)), "2 hours, 15 minutes ago"},
|
||||||
|
{"One day ago", now.Add(-24 * time.Hour), "1 day ago"},
|
||||||
|
{"Multiple days ago", now.Add(-(72*time.Hour + 20*time.Minute)), "3 days ago"},
|
||||||
|
{"Zero time", time.Time{}, "-"},
|
||||||
|
{"Unix zero time", time.Unix(0, 0), "-"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := timeAgo(tc.input)
|
||||||
|
assert.Equal(t, tc.expected, result, "Failed %s", tc.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/management/server/activity"
|
"github.com/netbirdio/netbird/management/server/activity"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
|
"github.com/netbirdio/management-integrations/integrations"
|
||||||
|
|
||||||
clientProto "github.com/netbirdio/netbird/client/proto"
|
clientProto "github.com/netbirdio/netbird/client/proto"
|
||||||
client "github.com/netbirdio/netbird/client/server"
|
client "github.com/netbirdio/netbird/client/server"
|
||||||
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
mgmtProto "github.com/netbirdio/netbird/management/proto"
|
||||||
@@ -22,6 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func startTestingServices(t *testing.T) string {
|
func startTestingServices(t *testing.T) string {
|
||||||
|
t.Helper()
|
||||||
config := &mgmt.Config{}
|
config := &mgmt.Config{}
|
||||||
_, err := util.ReadJson("../testdata/management.json", config)
|
_, err := util.ReadJson("../testdata/management.json", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -44,12 +50,16 @@ func startTestingServices(t *testing.T) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
|
func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
|
||||||
|
t.Helper()
|
||||||
lis, err := net.Listen("tcp", ":0")
|
lis, err := net.Listen("tcp", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
sigProto.RegisterSignalExchangeServer(s, sig.NewServer())
|
srv, err := sig.NewServer(otel.Meter(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sigProto.RegisterSignalExchangeServer(s, srv)
|
||||||
go func() {
|
go func() {
|
||||||
if err := s.Serve(lis); err != nil {
|
if err := s.Serve(lis); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -60,28 +70,30 @@ func startSignal(t *testing.T) (*grpc.Server, net.Listener) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Listener) {
|
func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Listener) {
|
||||||
|
t.Helper()
|
||||||
lis, err := net.Listen("tcp", ":0")
|
lis, err := net.Listen("tcp", ":0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
store, err := mgmt.NewFileStore(config.Datadir, nil)
|
store, cleanUp, err := mgmt.NewTestStoreFromJson(context.Background(), config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
t.Cleanup(cleanUp)
|
||||||
|
|
||||||
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
peersUpdateManager := mgmt.NewPeersUpdateManager(nil)
|
||||||
eventStore := &activity.InMemoryEventStore{}
|
eventStore := &activity.InMemoryEventStore{}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
accountManager, err := mgmt.BuildManager(store, peersUpdateManager, nil, "", "",
|
iv, _ := integrations.NewIntegratedValidator(context.Background(), eventStore)
|
||||||
eventStore)
|
accountManager, err := mgmt.BuildManager(context.Background(), store, peersUpdateManager, nil, "", "netbird.selfhosted", eventStore, nil, false, iv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||||
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager, nil)
|
mgmtServer, err := mgmt.NewServer(context.Background(), config, accountManager, peersUpdateManager, turnManager, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -96,8 +108,9 @@ func startManagement(t *testing.T, config *mgmt.Config) (*grpc.Server, net.Liste
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startClientDaemon(
|
func startClientDaemon(
|
||||||
t *testing.T, ctx context.Context, managementURL, configPath string,
|
t *testing.T, ctx context.Context, _, configPath string,
|
||||||
) (*grpc.Server, net.Listener) {
|
) (*grpc.Server, net.Listener) {
|
||||||
|
t.Helper()
|
||||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
133
client/cmd/up.go
133
client/cmd/up.go
@@ -5,17 +5,21 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/protobuf/types/known/durationpb"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,6 +40,14 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
|
upCmd.PersistentFlags().BoolVarP(&foregroundMode, "foreground-mode", "F", false, "start service in foreground")
|
||||||
|
upCmd.PersistentFlags().StringVar(&interfaceName, interfaceNameFlag, iface.WgInterfaceDefault, "Wireguard interface name")
|
||||||
|
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port")
|
||||||
|
upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", networkMonitor,
|
||||||
|
`Manage network monitoring. Defaults to true on Windows and macOS, false on Linux. `+
|
||||||
|
`E.g. --network-monitor=false to disable or --network-monitor=true to enable.`,
|
||||||
|
)
|
||||||
|
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
|
||||||
|
upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
|
||||||
}
|
}
|
||||||
|
|
||||||
func upFunc(cmd *cobra.Command, args []string) error {
|
func upFunc(cmd *cobra.Command, args []string) error {
|
||||||
@@ -84,17 +96,63 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
NATExternalIPs: natExternalIPs,
|
NATExternalIPs: natExternalIPs,
|
||||||
CustomDNSAddress: customDNSAddressConverted,
|
CustomDNSAddress: customDNSAddressConverted,
|
||||||
|
ExtraIFaceBlackList: extraIFaceBlackList,
|
||||||
}
|
}
|
||||||
if preSharedKey != "" {
|
|
||||||
|
if cmd.Flag(enableRosenpassFlag).Changed {
|
||||||
|
ic.RosenpassEnabled = &rosenpassEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(rosenpassPermissiveFlag).Changed {
|
||||||
|
ic.RosenpassPermissive = &rosenpassPermissive
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
||||||
|
ic.ServerSSHAllowed = &serverSSHAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(interfaceNameFlag).Changed {
|
||||||
|
if err := parseInterfaceName(interfaceName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ic.InterfaceName = &interfaceName
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(wireguardPortFlag).Changed {
|
||||||
|
p := int(wireguardPort)
|
||||||
|
ic.WireguardPort = &p
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
|
ic.NetworkMonitor = &networkMonitor
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
ic.PreSharedKey = &preSharedKey
|
ic.PreSharedKey = &preSharedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(disableAutoConnectFlag).Changed {
|
||||||
|
ic.DisableAutoConnect = &autoConnectDisabled
|
||||||
|
|
||||||
|
if autoConnectDisabled {
|
||||||
|
cmd.Println("Autoconnect has been disabled. The client won't connect automatically when the service starts.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoConnectDisabled {
|
||||||
|
cmd.Println("Autoconnect has been enabled. The client will connect automatically when the service starts.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
||||||
|
ic.DNSRouteInterval = &dnsRouteInterval
|
||||||
|
}
|
||||||
|
|
||||||
config, err := internal.UpdateOrCreateConfig(ic)
|
config, err := internal.UpdateOrCreateConfig(ic)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get config file: %v", err)
|
return fmt.Errorf("get config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config, _ = internal.UpdateOldManagementPort(ctx, config, configPath)
|
config, _ = internal.UpdateOldManagementURL(ctx, config, configPath)
|
||||||
|
|
||||||
err = foregroundLogin(ctx, cmd, config, setupKey)
|
err = foregroundLogin(ctx, cmd, config, setupKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,11 +162,12 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
ctx, cancel = context.WithCancel(ctx)
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
SetupCloseHandler(ctx, cancel)
|
SetupCloseHandler(ctx, cancel)
|
||||||
return internal.RunClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()))
|
|
||||||
|
connectClient := internal.NewConnectClient(ctx, config, peer.NewRecorder(config.ManagementURL.String()))
|
||||||
|
return connectClient.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
||||||
|
|
||||||
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
|
customDNSAddressConverted, err := parseCustomDNSAddress(cmd.Flag(dnsResolverAddress).Changed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -123,7 +182,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
defer func() {
|
defer func() {
|
||||||
err := conn.Close()
|
err := conn.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed closing dameon gRPC client connection %v", err)
|
log.Warnf("failed closing daemon gRPC client connection %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -142,12 +201,54 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
|
|
||||||
loginRequest := proto.LoginRequest{
|
loginRequest := proto.LoginRequest{
|
||||||
SetupKey: setupKey,
|
SetupKey: setupKey,
|
||||||
PreSharedKey: preSharedKey,
|
|
||||||
ManagementUrl: managementURL,
|
ManagementUrl: managementURL,
|
||||||
AdminURL: adminURL,
|
AdminURL: adminURL,
|
||||||
NatExternalIPs: natExternalIPs,
|
NatExternalIPs: natExternalIPs,
|
||||||
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
|
CleanNATExternalIPs: natExternalIPs != nil && len(natExternalIPs) == 0,
|
||||||
CustomDNSAddress: customDNSAddressConverted,
|
CustomDNSAddress: customDNSAddressConverted,
|
||||||
|
IsLinuxDesktopClient: isLinuxRunningDesktop(),
|
||||||
|
Hostname: hostName,
|
||||||
|
ExtraIFaceBlacklist: extraIFaceBlackList,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootCmd.PersistentFlags().Changed(preSharedKeyFlag) {
|
||||||
|
loginRequest.OptionalPreSharedKey = &preSharedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(enableRosenpassFlag).Changed {
|
||||||
|
loginRequest.RosenpassEnabled = &rosenpassEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(rosenpassPermissiveFlag).Changed {
|
||||||
|
loginRequest.RosenpassPermissive = &rosenpassPermissive
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(serverSSHAllowedFlag).Changed {
|
||||||
|
loginRequest.ServerSSHAllowed = &serverSSHAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(disableAutoConnectFlag).Changed {
|
||||||
|
loginRequest.DisableAutoConnect = &autoConnectDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(interfaceNameFlag).Changed {
|
||||||
|
if err := parseInterfaceName(interfaceName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
loginRequest.InterfaceName = &interfaceName
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(wireguardPortFlag).Changed {
|
||||||
|
wp := int64(wireguardPort)
|
||||||
|
loginRequest.WireguardPort = &wp
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(networkMonitorFlag).Changed {
|
||||||
|
loginRequest.NetworkMonitor = &networkMonitor
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Flag(dnsRouteIntervalFlag).Changed {
|
||||||
|
loginRequest.DnsRouteInterval = durationpb.New(dnsRouteInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginErr error
|
var loginErr error
|
||||||
@@ -178,7 +279,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
|
|
||||||
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode)
|
openURL(cmd, loginResp.VerificationURIComplete, loginResp.UserCode)
|
||||||
|
|
||||||
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode})
|
_, err = client.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{UserCode: loginResp.UserCode, Hostname: hostName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("waiting sso login failed with: %v", err)
|
return fmt.Errorf("waiting sso login failed with: %v", err)
|
||||||
}
|
}
|
||||||
@@ -199,11 +300,11 @@ func validateNATExternalIPs(list []string) error {
|
|||||||
|
|
||||||
subElements := strings.Split(element, "/")
|
subElements := strings.Split(element, "/")
|
||||||
if len(subElements) > 2 {
|
if len(subElements) > 2 {
|
||||||
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"String\" or \"String/String\"", element, externalIPMapFlag)
|
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"String\" or \"String/String\"", element, externalIPMapFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(subElements) == 1 && !isValidIP(subElements[0]) {
|
if len(subElements) == 1 && !isValidIP(subElements[0]) {
|
||||||
return fmt.Errorf("%s is not a valid input for %s. it should be formated as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
|
return fmt.Errorf("%s is not a valid input for %s. it should be formatted as \"IP\" or \"IP/IP\", or \"IP/Interface Name\"", element, externalIPMapFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
last := 0
|
last := 0
|
||||||
@@ -221,6 +322,18 @@ func validateNATExternalIPs(list []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseInterfaceName(name string) error {
|
||||||
|
if runtime.GOOS != "darwin" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(name, "utun") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("invalid interface name %s. Please use the prefix utun followed by a number on MacOS. e.g., utun1 or utun199", name)
|
||||||
|
}
|
||||||
|
|
||||||
func validateElement(element string) (int, error) {
|
func validateElement(element string) (int, error) {
|
||||||
if isValidIP(element) {
|
if isValidIP(element) {
|
||||||
return ipInputType, nil
|
return ipInputType, nil
|
||||||
@@ -258,7 +371,7 @@ func parseCustomDNSAddress(modified bool) ([]byte, error) {
|
|||||||
var parsed []byte
|
var parsed []byte
|
||||||
if modified {
|
if modified {
|
||||||
if !isValidAddrPort(customDNSAddress) {
|
if !isValidAddrPort(customDNSAddress) {
|
||||||
return nil, fmt.Errorf("%s is invalid, it should be formated as IP:Port string or as an empty string like \"\"", customDNSAddress)
|
return nil, fmt.Errorf("%s is invalid, it should be formatted as IP:Port string or as an empty string like \"\"", customDNSAddress)
|
||||||
}
|
}
|
||||||
if customDNSAddress == "" && logFile != "console" {
|
if customDNSAddress == "" && logFile != "console" {
|
||||||
parsed = []byte("empty")
|
parsed = []byte("empty")
|
||||||
|
|||||||
30
client/errors/errors.go
Normal file
30
client/errors/errors.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
func formatError(es []error) string {
|
||||||
|
if len(es) == 0 {
|
||||||
|
return fmt.Sprintf("0 error occurred:\n\t* %s", es[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
points := make([]string, len(es))
|
||||||
|
for i, err := range es {
|
||||||
|
points[i] = fmt.Sprintf("* %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"%d errors occurred:\n\t%s",
|
||||||
|
len(es), strings.Join(points, "\n\t"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatErrorOrNil(err *multierror.Error) error {
|
||||||
|
if err != nil {
|
||||||
|
err.ErrorFormat = formatError
|
||||||
|
}
|
||||||
|
return err.ErrorOrNil()
|
||||||
|
}
|
||||||
32
client/firewall/create.go
Normal file
32
client/firewall/create.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//go:build !linux || android
|
||||||
|
|
||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFirewall creates a firewall manager instance
|
||||||
|
func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager, error) {
|
||||||
|
if !iface.IsUserspaceBind() {
|
||||||
|
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use userspace packet filtering firewall
|
||||||
|
fm, err := uspfilter.Create(iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = fm.AllowNetbird()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to allow netbird interface traffic: %v", err)
|
||||||
|
}
|
||||||
|
return fm, nil
|
||||||
|
}
|
||||||
149
client/firewall/create_linux.go
Normal file
149
client/firewall/create_linux.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package firewall
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
"github.com/google/nftables"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nbiptables "github.com/netbirdio/netbird/client/firewall/iptables"
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
nbnftables "github.com/netbirdio/netbird/client/firewall/nftables"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UNKNOWN is the default value for the firewall type for unknown firewall type
|
||||||
|
UNKNOWN FWType = iota
|
||||||
|
// IPTABLES is the value for the iptables firewall type
|
||||||
|
IPTABLES
|
||||||
|
// NFTABLES is the value for the nftables firewall type
|
||||||
|
NFTABLES
|
||||||
|
)
|
||||||
|
|
||||||
|
// SKIP_NFTABLES_ENV is the environment variable to skip nftables check
|
||||||
|
const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK"
|
||||||
|
|
||||||
|
// FWType is the type for the firewall type
|
||||||
|
type FWType int
|
||||||
|
|
||||||
|
func NewFirewall(context context.Context, iface IFaceMapper) (firewall.Manager, error) {
|
||||||
|
// on the linux system we try to user nftables or iptables
|
||||||
|
// in any case, because we need to allow netbird interface traffic
|
||||||
|
// so we use AllowNetbird traffic from these firewall managers
|
||||||
|
// for the userspace packet filtering firewall
|
||||||
|
var fm firewall.Manager
|
||||||
|
var errFw error
|
||||||
|
|
||||||
|
switch check() {
|
||||||
|
case IPTABLES:
|
||||||
|
log.Info("creating an iptables firewall manager")
|
||||||
|
fm, errFw = nbiptables.Create(context, iface)
|
||||||
|
if errFw != nil {
|
||||||
|
log.Errorf("failed to create iptables manager: %s", errFw)
|
||||||
|
}
|
||||||
|
case NFTABLES:
|
||||||
|
log.Info("creating an nftables firewall manager")
|
||||||
|
fm, errFw = nbnftables.Create(context, iface)
|
||||||
|
if errFw != nil {
|
||||||
|
log.Errorf("failed to create nftables manager: %s", errFw)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
errFw = fmt.Errorf("no firewall manager found")
|
||||||
|
log.Info("no firewall manager found, trying to use userspace packet filtering firewall")
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface.IsUserspaceBind() {
|
||||||
|
var errUsp error
|
||||||
|
if errFw == nil {
|
||||||
|
fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm)
|
||||||
|
} else {
|
||||||
|
fm, errUsp = uspfilter.Create(iface)
|
||||||
|
}
|
||||||
|
if errUsp != nil {
|
||||||
|
log.Debugf("failed to create userspace filtering firewall: %s", errUsp)
|
||||||
|
return nil, errUsp
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fm.AllowNetbird(); err != nil {
|
||||||
|
log.Errorf("failed to allow netbird interface traffic: %v", err)
|
||||||
|
}
|
||||||
|
return fm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if errFw != nil {
|
||||||
|
return nil, errFw
|
||||||
|
}
|
||||||
|
|
||||||
|
return fm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check returns the firewall type based on common lib checks. It returns UNKNOWN if no firewall is found.
|
||||||
|
func check() FWType {
|
||||||
|
useIPTABLES := false
|
||||||
|
var iptablesChains []string
|
||||||
|
ip, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
if err == nil && isIptablesClientAvailable(ip) {
|
||||||
|
major, minor, _ := ip.GetIptablesVersion()
|
||||||
|
// use iptables when its version is lower than 1.8.0 which doesn't work well with our nftables manager
|
||||||
|
if major < 1 || (major == 1 && minor < 8) {
|
||||||
|
return IPTABLES
|
||||||
|
}
|
||||||
|
|
||||||
|
useIPTABLES = true
|
||||||
|
|
||||||
|
iptablesChains, err = ip.ListChains("filter")
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to list iptables chains: %s", err)
|
||||||
|
useIPTABLES = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nf := nftables.Conn{}
|
||||||
|
if chains, err := nf.ListChains(); err == nil && os.Getenv(SKIP_NFTABLES_ENV) != "true" {
|
||||||
|
if !useIPTABLES {
|
||||||
|
return NFTABLES
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for chains where table is filter
|
||||||
|
// if we find one, we assume that nftables manager can be used with iptables
|
||||||
|
for _, chain := range chains {
|
||||||
|
if chain.Table.Name == "filter" {
|
||||||
|
return NFTABLES
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check tables for the following constraints:
|
||||||
|
// 1. there is no chain in nftables for the filter table and there is at least one chain in iptables, we assume that nftables manager can not be used
|
||||||
|
// 2. there is no tables or more than one table, we assume that nftables manager can be used
|
||||||
|
// 3. there is only one table and its name is filter, we assume that nftables manager can not be used, since there was no chain in it
|
||||||
|
// 4. if we find an error we log and continue with iptables check
|
||||||
|
nbTablesList, err := nf.ListTables()
|
||||||
|
switch {
|
||||||
|
case err == nil && len(iptablesChains) > 0:
|
||||||
|
return IPTABLES
|
||||||
|
case err == nil && len(nbTablesList) != 1:
|
||||||
|
return NFTABLES
|
||||||
|
case err == nil && len(nbTablesList) == 1 && nbTablesList[0].Name == "filter":
|
||||||
|
return IPTABLES
|
||||||
|
case err != nil:
|
||||||
|
log.Errorf("failed to list nftables tables on fw manager discovery: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if useIPTABLES {
|
||||||
|
return IPTABLES
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIptablesClientAvailable(client *iptables.IPTables) bool {
|
||||||
|
_, err := client.ListChains("filter")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
11
client/firewall/iface.go
Normal file
11
client/firewall/iface.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package firewall
|
||||||
|
|
||||||
|
import "github.com/netbirdio/netbird/iface"
|
||||||
|
|
||||||
|
// IFaceMapper defines subset methods of interface required for manager
|
||||||
|
type IFaceMapper interface {
|
||||||
|
Name() string
|
||||||
|
Address() iface.WGAddress
|
||||||
|
IsUserspaceBind() bool
|
||||||
|
SetFilter(iface.PacketFilter) error
|
||||||
|
}
|
||||||
473
client/firewall/iptables/acl_linux.go
Normal file
473
client/firewall/iptables/acl_linux.go
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
package iptables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/nadoo/ipset"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tableName = "filter"
|
||||||
|
|
||||||
|
// rules chains contains the effective ACL rules
|
||||||
|
chainNameInputRules = "NETBIRD-ACL-INPUT"
|
||||||
|
chainNameOutputRules = "NETBIRD-ACL-OUTPUT"
|
||||||
|
|
||||||
|
postRoutingMark = "0x000007e4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type aclManager struct {
|
||||||
|
iptablesClient *iptables.IPTables
|
||||||
|
wgIface iFaceMapper
|
||||||
|
routeingFwChainName string
|
||||||
|
|
||||||
|
entries map[string][][]string
|
||||||
|
ipsetStore *ipsetStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAclManager(iptablesClient *iptables.IPTables, wgIface iFaceMapper, routeingFwChainName string) (*aclManager, error) {
|
||||||
|
m := &aclManager{
|
||||||
|
iptablesClient: iptablesClient,
|
||||||
|
wgIface: wgIface,
|
||||||
|
routeingFwChainName: routeingFwChainName,
|
||||||
|
|
||||||
|
entries: make(map[string][][]string),
|
||||||
|
ipsetStore: newIpsetStore(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ipset.Init()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to init ipset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.seedInitialEntries()
|
||||||
|
|
||||||
|
err = m.cleanChains()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.createDefaultChains()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *aclManager) AddFiltering(
|
||||||
|
ip net.IP,
|
||||||
|
protocol firewall.Protocol,
|
||||||
|
sPort *firewall.Port,
|
||||||
|
dPort *firewall.Port,
|
||||||
|
direction firewall.RuleDirection,
|
||||||
|
action firewall.Action,
|
||||||
|
ipsetName string,
|
||||||
|
) ([]firewall.Rule, error) {
|
||||||
|
var dPortVal, sPortVal string
|
||||||
|
if dPort != nil && dPort.Values != nil {
|
||||||
|
// TODO: we support only one port per rule in current implementation of ACLs
|
||||||
|
dPortVal = strconv.Itoa(dPort.Values[0])
|
||||||
|
}
|
||||||
|
if sPort != nil && sPort.Values != nil {
|
||||||
|
sPortVal = strconv.Itoa(sPort.Values[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
var chain string
|
||||||
|
if direction == firewall.RuleDirectionOUT {
|
||||||
|
chain = chainNameOutputRules
|
||||||
|
} else {
|
||||||
|
chain = chainNameInputRules
|
||||||
|
}
|
||||||
|
|
||||||
|
ipsetName = transformIPsetName(ipsetName, sPortVal, dPortVal)
|
||||||
|
specs := filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, direction, action, ipsetName)
|
||||||
|
if ipsetName != "" {
|
||||||
|
if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists {
|
||||||
|
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add IP to ipset: %w", err)
|
||||||
|
}
|
||||||
|
// if ruleset already exists it means we already have the firewall rule
|
||||||
|
// so we need to update IPs in the ruleset and return new fw.Rule object for ACL manager.
|
||||||
|
ipList.addIP(ip.String())
|
||||||
|
return []firewall.Rule{&Rule{
|
||||||
|
ruleID: uuid.New().String(),
|
||||||
|
ipsetName: ipsetName,
|
||||||
|
ip: ip.String(),
|
||||||
|
chain: chain,
|
||||||
|
specs: specs,
|
||||||
|
}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ipset.Flush(ipsetName); err != nil {
|
||||||
|
log.Errorf("flush ipset %s before use it: %s", ipsetName, err)
|
||||||
|
}
|
||||||
|
if err := ipset.Create(ipsetName); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create ipset: %w", err)
|
||||||
|
}
|
||||||
|
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add IP to ipset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipList := newIpList(ip.String())
|
||||||
|
m.ipsetStore.addIpList(ipsetName, ipList)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := m.iptablesClient.Exists("filter", chain, specs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check rule: %w", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return nil, fmt.Errorf("rule already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.iptablesClient.Insert("filter", chain, 1, specs...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := &Rule{
|
||||||
|
ruleID: uuid.New().String(),
|
||||||
|
specs: specs,
|
||||||
|
ipsetName: ipsetName,
|
||||||
|
ip: ip.String(),
|
||||||
|
chain: chain,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldAddToPrerouting(protocol, dPort, direction) {
|
||||||
|
return []firewall.Rule{rule}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rulePrerouting, err := m.addPreroutingFilter(ipsetName, string(protocol), dPortVal, ip)
|
||||||
|
if err != nil {
|
||||||
|
return []firewall.Rule{rule}, err
|
||||||
|
}
|
||||||
|
return []firewall.Rule{rule, rulePrerouting}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteRule from the firewall by rule definition
|
||||||
|
func (m *aclManager) DeleteRule(rule firewall.Rule) error {
|
||||||
|
r, ok := rule.(*Rule)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("invalid rule type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.chain == "PREROUTING" {
|
||||||
|
goto DELETERULE
|
||||||
|
}
|
||||||
|
|
||||||
|
if ipsetList, ok := m.ipsetStore.ipset(r.ipsetName); ok {
|
||||||
|
// delete IP from ruleset IPs list and ipset
|
||||||
|
if _, ok := ipsetList.ips[r.ip]; ok {
|
||||||
|
if err := ipset.Del(r.ipsetName, r.ip); err != nil {
|
||||||
|
return fmt.Errorf("failed to delete ip from ipset: %w", err)
|
||||||
|
}
|
||||||
|
delete(ipsetList.ips, r.ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if after delete, set still contains other IPs,
|
||||||
|
// no need to delete firewall rule and we should exit here
|
||||||
|
if len(ipsetList.ips) != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// we delete last IP from the set, that means we need to delete
|
||||||
|
// set itself and associated firewall rule too
|
||||||
|
m.ipsetStore.deleteIpset(r.ipsetName)
|
||||||
|
|
||||||
|
if err := ipset.Destroy(r.ipsetName); err != nil {
|
||||||
|
log.Errorf("delete empty ipset: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DELETERULE:
|
||||||
|
var table string
|
||||||
|
if r.chain == "PREROUTING" {
|
||||||
|
table = "mangle"
|
||||||
|
} else {
|
||||||
|
table = "filter"
|
||||||
|
}
|
||||||
|
err := m.iptablesClient.Delete(table, r.chain, r.specs...)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to delete rule, %s, %v: %s", r.chain, r.specs, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *aclManager) Reset() error {
|
||||||
|
return m.cleanChains()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *aclManager) addPreroutingFilter(ipsetName string, protocol string, port string, ip net.IP) (*Rule, error) {
|
||||||
|
var src []string
|
||||||
|
if ipsetName != "" {
|
||||||
|
src = []string{"-m", "set", "--set", ipsetName, "src"}
|
||||||
|
} else {
|
||||||
|
src = []string{"-s", ip.String()}
|
||||||
|
}
|
||||||
|
specs := []string{
|
||||||
|
"-d", m.wgIface.Address().IP.String(),
|
||||||
|
"-p", protocol,
|
||||||
|
"--dport", port,
|
||||||
|
"-j", "MARK", "--set-mark", postRoutingMark,
|
||||||
|
}
|
||||||
|
|
||||||
|
specs = append(src, specs...)
|
||||||
|
|
||||||
|
ok, err := m.iptablesClient.Exists("mangle", "PREROUTING", specs...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check rule: %w", err)
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
return nil, fmt.Errorf("rule already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.iptablesClient.Insert("mangle", "PREROUTING", 1, specs...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rule := &Rule{
|
||||||
|
ruleID: uuid.New().String(),
|
||||||
|
specs: specs,
|
||||||
|
ipsetName: ipsetName,
|
||||||
|
ip: ip.String(),
|
||||||
|
chain: "PREROUTING",
|
||||||
|
}
|
||||||
|
return rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo write less destructive cleanup mechanism
|
||||||
|
func (m *aclManager) cleanChains() error {
|
||||||
|
ok, err := m.iptablesClient.ChainExists(tableName, chainNameOutputRules)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to list chains: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
rules := m.entries["OUTPUT"]
|
||||||
|
for _, rule := range rules {
|
||||||
|
err := m.iptablesClient.DeleteIfExists(tableName, "OUTPUT", rule...)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to delete rule: %v, %s", rule, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.iptablesClient.ClearAndDeleteChain(tableName, chainNameOutputRules)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to clear and delete %s chain: %s", chainNameOutputRules, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = m.iptablesClient.ChainExists(tableName, chainNameInputRules)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to list chains: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
for _, rule := range m.entries["INPUT"] {
|
||||||
|
err := m.iptablesClient.DeleteIfExists(tableName, "INPUT", rule...)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to delete rule: %v, %s", rule, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range m.entries["FORWARD"] {
|
||||||
|
err := m.iptablesClient.DeleteIfExists(tableName, "FORWARD", rule...)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to delete rule: %v, %s", rule, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.iptablesClient.ClearAndDeleteChain(tableName, chainNameInputRules)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to clear and delete %s chain: %s", chainNameInputRules, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = m.iptablesClient.ChainExists("mangle", "PREROUTING")
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to list chains: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
for _, rule := range m.entries["PREROUTING"] {
|
||||||
|
err := m.iptablesClient.DeleteIfExists("mangle", "PREROUTING", rule...)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to delete rule: %v, %s", rule, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = m.iptablesClient.ClearChain("mangle", "PREROUTING")
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to clear %s chain: %s", "PREROUTING", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ipsetName := range m.ipsetStore.ipsetNames() {
|
||||||
|
if err := ipset.Flush(ipsetName); err != nil {
|
||||||
|
log.Errorf("flush ipset %q during reset: %v", ipsetName, err)
|
||||||
|
}
|
||||||
|
if err := ipset.Destroy(ipsetName); err != nil {
|
||||||
|
log.Errorf("delete ipset %q during reset: %v", ipsetName, err)
|
||||||
|
}
|
||||||
|
m.ipsetStore.deleteIpset(ipsetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *aclManager) createDefaultChains() error {
|
||||||
|
// chain netbird-acl-input-rules
|
||||||
|
if err := m.iptablesClient.NewChain(tableName, chainNameInputRules); err != nil {
|
||||||
|
log.Debugf("failed to create '%s' chain: %s", chainNameInputRules, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// chain netbird-acl-output-rules
|
||||||
|
if err := m.iptablesClient.NewChain(tableName, chainNameOutputRules); err != nil {
|
||||||
|
log.Debugf("failed to create '%s' chain: %s", chainNameOutputRules, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for chainName, rules := range m.entries {
|
||||||
|
for _, rule := range rules {
|
||||||
|
if chainName == "FORWARD" {
|
||||||
|
// position 2 because we add it after router's, jump rule
|
||||||
|
if err := m.iptablesClient.InsertUnique(tableName, "FORWARD", 2, rule...); err != nil {
|
||||||
|
log.Debugf("failed to create input chain jump rule: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := m.iptablesClient.AppendUnique(tableName, chainName, rule...); err != nil {
|
||||||
|
log.Debugf("failed to create input chain jump rule: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *aclManager) seedInitialEntries() {
|
||||||
|
m.appendToEntries("INPUT",
|
||||||
|
[]string{"-i", m.wgIface.Name(), "!", "-s", m.wgIface.Address().String(), "-d", m.wgIface.Address().String(), "-j", "ACCEPT"})
|
||||||
|
|
||||||
|
m.appendToEntries("INPUT",
|
||||||
|
[]string{"-i", m.wgIface.Name(), "-s", m.wgIface.Address().String(), "!", "-d", m.wgIface.Address().String(), "-j", "ACCEPT"})
|
||||||
|
|
||||||
|
m.appendToEntries("INPUT",
|
||||||
|
[]string{"-i", m.wgIface.Name(), "-s", m.wgIface.Address().String(), "-d", m.wgIface.Address().String(), "-j", chainNameInputRules})
|
||||||
|
|
||||||
|
m.appendToEntries("INPUT", []string{"-i", m.wgIface.Name(), "-j", "DROP"})
|
||||||
|
|
||||||
|
m.appendToEntries("OUTPUT",
|
||||||
|
[]string{"-o", m.wgIface.Name(), "!", "-s", m.wgIface.Address().String(), "-d", m.wgIface.Address().String(), "-j", "ACCEPT"})
|
||||||
|
|
||||||
|
m.appendToEntries("OUTPUT",
|
||||||
|
[]string{"-o", m.wgIface.Name(), "-s", m.wgIface.Address().String(), "!", "-d", m.wgIface.Address().String(), "-j", "ACCEPT"})
|
||||||
|
|
||||||
|
m.appendToEntries("OUTPUT",
|
||||||
|
[]string{"-o", m.wgIface.Name(), "-s", m.wgIface.Address().String(), "-d", m.wgIface.Address().String(), "-j", chainNameOutputRules})
|
||||||
|
|
||||||
|
m.appendToEntries("OUTPUT", []string{"-o", m.wgIface.Name(), "-j", "DROP"})
|
||||||
|
|
||||||
|
m.appendToEntries("FORWARD", []string{"-i", m.wgIface.Name(), "-j", "DROP"})
|
||||||
|
m.appendToEntries("FORWARD", []string{"-i", m.wgIface.Name(), "-j", chainNameInputRules})
|
||||||
|
m.appendToEntries("FORWARD",
|
||||||
|
[]string{"-o", m.wgIface.Name(), "-m", "mark", "--mark", postRoutingMark, "-j", "ACCEPT"})
|
||||||
|
m.appendToEntries("FORWARD",
|
||||||
|
[]string{"-i", m.wgIface.Name(), "-m", "mark", "--mark", postRoutingMark, "-j", "ACCEPT"})
|
||||||
|
m.appendToEntries("FORWARD", []string{"-o", m.wgIface.Name(), "-j", m.routeingFwChainName})
|
||||||
|
m.appendToEntries("FORWARD", []string{"-i", m.wgIface.Name(), "-j", m.routeingFwChainName})
|
||||||
|
|
||||||
|
m.appendToEntries("PREROUTING",
|
||||||
|
[]string{"-t", "mangle", "-i", m.wgIface.Name(), "!", "-s", m.wgIface.Address().String(), "-d", m.wgIface.Address().IP.String(), "-m", "mark", "--mark", postRoutingMark})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *aclManager) appendToEntries(chainName string, spec []string) {
|
||||||
|
m.entries[chainName] = append(m.entries[chainName], spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterRuleSpecs returns the specs of a filtering rule
|
||||||
|
func filterRuleSpecs(
|
||||||
|
ip net.IP, protocol string, sPort, dPort string, direction firewall.RuleDirection, action firewall.Action, ipsetName string,
|
||||||
|
) (specs []string) {
|
||||||
|
matchByIP := true
|
||||||
|
// don't use IP matching if IP is ip 0.0.0.0
|
||||||
|
if ip.String() == "0.0.0.0" {
|
||||||
|
matchByIP = false
|
||||||
|
}
|
||||||
|
switch direction {
|
||||||
|
case firewall.RuleDirectionIN:
|
||||||
|
if matchByIP {
|
||||||
|
if ipsetName != "" {
|
||||||
|
specs = append(specs, "-m", "set", "--set", ipsetName, "src")
|
||||||
|
} else {
|
||||||
|
specs = append(specs, "-s", ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case firewall.RuleDirectionOUT:
|
||||||
|
if matchByIP {
|
||||||
|
if ipsetName != "" {
|
||||||
|
specs = append(specs, "-m", "set", "--set", ipsetName, "dst")
|
||||||
|
} else {
|
||||||
|
specs = append(specs, "-d", ip.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if protocol != "all" {
|
||||||
|
specs = append(specs, "-p", protocol)
|
||||||
|
}
|
||||||
|
if sPort != "" {
|
||||||
|
specs = append(specs, "--sport", sPort)
|
||||||
|
}
|
||||||
|
if dPort != "" {
|
||||||
|
specs = append(specs, "--dport", dPort)
|
||||||
|
}
|
||||||
|
return append(specs, "-j", actionToStr(action))
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionToStr(action firewall.Action) string {
|
||||||
|
if action == firewall.ActionAccept {
|
||||||
|
return "ACCEPT"
|
||||||
|
}
|
||||||
|
return "DROP"
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformIPsetName(ipsetName string, sPort, dPort string) string {
|
||||||
|
switch {
|
||||||
|
case ipsetName == "":
|
||||||
|
return ""
|
||||||
|
case sPort != "" && dPort != "":
|
||||||
|
return ipsetName + "-sport-dport"
|
||||||
|
case sPort != "":
|
||||||
|
return ipsetName + "-sport"
|
||||||
|
case dPort != "":
|
||||||
|
return ipsetName + "-dport"
|
||||||
|
default:
|
||||||
|
return ipsetName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldAddToPrerouting(proto firewall.Protocol, dPort *firewall.Port, direction firewall.RuleDirection) bool {
|
||||||
|
if proto == "all" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if direction != firewall.RuleDirectionIN {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if dPort == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -1,262 +1,105 @@
|
|||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/nadoo/ipset"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// ChainInputFilterName is the name of the chain that is used for filtering incoming packets
|
|
||||||
ChainInputFilterName = "NETBIRD-ACL-INPUT"
|
|
||||||
|
|
||||||
// ChainOutputFilterName is the name of the chain that is used for filtering outgoing packets
|
|
||||||
ChainOutputFilterName = "NETBIRD-ACL-OUTPUT"
|
|
||||||
)
|
|
||||||
|
|
||||||
// dropAllDefaultRule in the Netbird chain
|
|
||||||
var dropAllDefaultRule = []string{"-j", "DROP"}
|
|
||||||
|
|
||||||
// Manager of iptables firewall
|
// Manager of iptables firewall
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
|
||||||
ipv4Client *iptables.IPTables
|
|
||||||
ipv6Client *iptables.IPTables
|
|
||||||
|
|
||||||
inputDefaultRuleSpecs []string
|
|
||||||
outputDefaultRuleSpecs []string
|
|
||||||
wgIface iFaceMapper
|
wgIface iFaceMapper
|
||||||
|
|
||||||
rulesets map[string]ruleset
|
ipv4Client *iptables.IPTables
|
||||||
|
aclMgr *aclManager
|
||||||
|
router *routerManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// iFaceMapper defines subset methods of interface required for manager
|
// iFaceMapper defines subset methods of interface required for manager
|
||||||
type iFaceMapper interface {
|
type iFaceMapper interface {
|
||||||
Name() string
|
Name() string
|
||||||
Address() iface.WGAddress
|
Address() iface.WGAddress
|
||||||
}
|
IsUserspaceBind() bool
|
||||||
|
|
||||||
type ruleset struct {
|
|
||||||
rule *Rule
|
|
||||||
ips map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create iptables firewall manager
|
// Create iptables firewall manager
|
||||||
func Create(wgIface iFaceMapper) (*Manager, error) {
|
func Create(context context.Context, wgIface iFaceMapper) (*Manager, error) {
|
||||||
m := &Manager{
|
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
wgIface: wgIface,
|
|
||||||
inputDefaultRuleSpecs: []string{
|
|
||||||
"-i", wgIface.Name(), "-j", ChainInputFilterName, "-s", wgIface.Address().String()},
|
|
||||||
outputDefaultRuleSpecs: []string{
|
|
||||||
"-o", wgIface.Name(), "-j", ChainOutputFilterName, "-d", wgIface.Address().String()},
|
|
||||||
rulesets: make(map[string]ruleset),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ipset.Init(); err != nil {
|
|
||||||
return nil, fmt.Errorf("init ipset: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// init clients for booth ipv4 and ipv6
|
|
||||||
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("iptables is not installed in the system or not supported")
|
return nil, fmt.Errorf("iptables is not installed in the system or not supported")
|
||||||
}
|
}
|
||||||
if isIptablesClientAvailable(ipv4Client) {
|
|
||||||
m.ipv4Client = ipv4Client
|
m := &Manager{
|
||||||
|
wgIface: wgIface,
|
||||||
|
ipv4Client: iptablesClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
m.router, err = newRouterManager(context, iptablesClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ip6tables is not installed in the system or not supported: %v", err)
|
log.Debugf("failed to initialize route related chains: %s", err)
|
||||||
} else {
|
return nil, err
|
||||||
if isIptablesClientAvailable(ipv6Client) {
|
|
||||||
m.ipv6Client = ipv6Client
|
|
||||||
}
|
}
|
||||||
|
m.aclMgr, err = newAclManager(iptablesClient, wgIface, m.router.RouteingFwChainName())
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to initialize ACL manager: %s", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Reset(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to reset firewall: %v", err)
|
|
||||||
}
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isIptablesClientAvailable(client *iptables.IPTables) bool {
|
|
||||||
_, err := client.ListChains("filter")
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddFiltering rule to the firewall
|
// AddFiltering rule to the firewall
|
||||||
//
|
//
|
||||||
// If comment is empty rule ID is used as comment
|
// Comment will be ignored because some system this feature is not supported
|
||||||
func (m *Manager) AddFiltering(
|
func (m *Manager) AddFiltering(
|
||||||
ip net.IP,
|
ip net.IP,
|
||||||
protocol fw.Protocol,
|
protocol firewall.Protocol,
|
||||||
sPort *fw.Port,
|
sPort *firewall.Port,
|
||||||
dPort *fw.Port,
|
dPort *firewall.Port,
|
||||||
direction fw.RuleDirection,
|
direction firewall.RuleDirection,
|
||||||
action fw.Action,
|
action firewall.Action,
|
||||||
ipsetName string,
|
ipsetName string,
|
||||||
comment string,
|
comment string,
|
||||||
) (fw.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
client, err := m.client(ip)
|
return m.aclMgr.AddFiltering(ip, protocol, sPort, dPort, direction, action, ipsetName)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var dPortVal, sPortVal string
|
|
||||||
if dPort != nil && dPort.Values != nil {
|
|
||||||
// TODO: we support only one port per rule in current implementation of ACLs
|
|
||||||
dPortVal = strconv.Itoa(dPort.Values[0])
|
|
||||||
}
|
|
||||||
if sPort != nil && sPort.Values != nil {
|
|
||||||
sPortVal = strconv.Itoa(sPort.Values[0])
|
|
||||||
}
|
|
||||||
ipsetName = m.transformIPsetName(ipsetName, sPortVal, dPortVal)
|
|
||||||
|
|
||||||
ruleID := uuid.New().String()
|
|
||||||
if comment == "" {
|
|
||||||
comment = ruleID
|
|
||||||
}
|
|
||||||
|
|
||||||
if ipsetName != "" {
|
|
||||||
rs, rsExists := m.rulesets[ipsetName]
|
|
||||||
if !rsExists {
|
|
||||||
if err := ipset.Flush(ipsetName); err != nil {
|
|
||||||
log.Errorf("flush ipset %q before use it: %v", ipsetName, err)
|
|
||||||
}
|
|
||||||
if err := ipset.Create(ipsetName); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create ipset: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to add IP to ipset: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if rsExists {
|
|
||||||
// if ruleset already exists it means we already have the firewall rule
|
|
||||||
// so we need to update IPs in the ruleset and return new fw.Rule object for ACL manager.
|
|
||||||
rs.ips[ip.String()] = ruleID
|
|
||||||
return &Rule{
|
|
||||||
ruleID: ruleID,
|
|
||||||
ipsetName: ipsetName,
|
|
||||||
ip: ip.String(),
|
|
||||||
dst: direction == fw.RuleDirectionOUT,
|
|
||||||
v6: ip.To4() == nil,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
// this is new ipset so we need to create firewall rule for it
|
|
||||||
}
|
|
||||||
|
|
||||||
specs := m.filterRuleSpecs("filter", ip, string(protocol), sPortVal, dPortVal,
|
|
||||||
direction, action, comment, ipsetName)
|
|
||||||
|
|
||||||
if direction == fw.RuleDirectionOUT {
|
|
||||||
ok, err := client.Exists("filter", ChainOutputFilterName, specs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("check is output rule already exists: %w", err)
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
return nil, fmt.Errorf("input rule already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Insert("filter", ChainOutputFilterName, 1, specs...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ok, err := client.Exists("filter", ChainInputFilterName, specs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("check is input rule already exists: %w", err)
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
return nil, fmt.Errorf("input rule already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.Insert("filter", ChainInputFilterName, 1, specs...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rule := &Rule{
|
|
||||||
ruleID: ruleID,
|
|
||||||
specs: specs,
|
|
||||||
ipsetName: ipsetName,
|
|
||||||
ip: ip.String(),
|
|
||||||
dst: direction == fw.RuleDirectionOUT,
|
|
||||||
v6: ip.To4() == nil,
|
|
||||||
}
|
|
||||||
if ipsetName != "" {
|
|
||||||
// ipset name is defined and it means that this rule was created
|
|
||||||
// for it, need to assosiate it with ruleset
|
|
||||||
m.rulesets[ipsetName] = ruleset{
|
|
||||||
rule: rule,
|
|
||||||
ips: map[string]string{rule.ip: ruleID},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rule, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRule from the firewall by rule definition
|
// DeleteRule from the firewall by rule definition
|
||||||
func (m *Manager) DeleteRule(rule fw.Rule) error {
|
func (m *Manager) DeleteRule(rule firewall.Rule) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
r, ok := rule.(*Rule)
|
return m.aclMgr.DeleteRule(rule)
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid rule type")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client := m.ipv4Client
|
func (m *Manager) IsServerRouteSupported() bool {
|
||||||
if r.v6 {
|
return true
|
||||||
if m.ipv6Client == nil {
|
|
||||||
return fmt.Errorf("ipv6 is not supported")
|
|
||||||
}
|
|
||||||
client = m.ipv6Client
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rs, ok := m.rulesets[r.ipsetName]; ok {
|
func (m *Manager) InsertRoutingRules(pair firewall.RouterPair) error {
|
||||||
// delete IP from ruleset IPs list and ipset
|
m.mutex.Lock()
|
||||||
if _, ok := rs.ips[r.ip]; ok {
|
defer m.mutex.Unlock()
|
||||||
if err := ipset.Del(r.ipsetName, r.ip); err != nil {
|
|
||||||
return fmt.Errorf("failed to delete ip from ipset: %w", err)
|
return m.router.InsertRoutingRules(pair)
|
||||||
}
|
|
||||||
delete(rs.ips, r.ip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if after delete, set still contains other IPs,
|
func (m *Manager) RemoveRoutingRules(pair firewall.RouterPair) error {
|
||||||
// no need to delete firewall rule and we should exit here
|
m.mutex.Lock()
|
||||||
if len(rs.ips) != 0 {
|
defer m.mutex.Unlock()
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// we delete last IP from the set, that means we need to delete
|
return m.router.RemoveRoutingRules(pair)
|
||||||
// set itself and assosiated firewall rule too
|
|
||||||
delete(m.rulesets, r.ipsetName)
|
|
||||||
|
|
||||||
if err := ipset.Destroy(r.ipsetName); err != nil {
|
|
||||||
log.Errorf("delete empty ipset: %v", err)
|
|
||||||
}
|
|
||||||
r = rs.rule
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.dst {
|
|
||||||
return client.Delete("filter", ChainOutputFilterName, r.specs...)
|
|
||||||
}
|
|
||||||
return client.Delete("filter", ChainInputFilterName, r.specs...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset firewall to the default state
|
// Reset firewall to the default state
|
||||||
@@ -264,192 +107,49 @@ func (m *Manager) Reset() error {
|
|||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
if err := m.reset(m.ipv4Client, "filter"); err != nil {
|
errAcl := m.aclMgr.Reset()
|
||||||
return fmt.Errorf("clean ipv4 firewall ACL input chain: %w", err)
|
if errAcl != nil {
|
||||||
|
log.Errorf("failed to clean up ACL rules from firewall: %s", errAcl)
|
||||||
}
|
}
|
||||||
if m.ipv6Client != nil {
|
errMgr := m.router.Reset()
|
||||||
if err := m.reset(m.ipv6Client, "filter"); err != nil {
|
if errMgr != nil {
|
||||||
return fmt.Errorf("clean ipv6 firewall ACL input chain: %w", err)
|
log.Errorf("failed to clean up router rules from firewall: %s", errMgr)
|
||||||
|
return errMgr
|
||||||
}
|
}
|
||||||
|
return errAcl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AllowNetbird allows netbird interface traffic
|
||||||
|
func (m *Manager) AllowNetbird() error {
|
||||||
|
if !m.wgIface.IsUserspaceBind() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err := m.AddFiltering(
|
||||||
|
net.ParseIP("0.0.0.0"),
|
||||||
|
"all",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
firewall.RuleDirectionIN,
|
||||||
|
firewall.ActionAccept,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to allow netbird interface traffic: %w", err)
|
||||||
|
}
|
||||||
|
_, err = m.AddFiltering(
|
||||||
|
net.ParseIP("0.0.0.0"),
|
||||||
|
"all",
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
firewall.RuleDirectionOUT,
|
||||||
|
firewall.ActionAccept,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Flush doesn't need to be implemented for this manager
|
// Flush doesn't need to be implemented for this manager
|
||||||
func (m *Manager) Flush() error { return nil }
|
func (m *Manager) Flush() error { return nil }
|
||||||
|
|
||||||
// reset firewall chain, clear it and drop it
|
|
||||||
func (m *Manager) reset(client *iptables.IPTables, table string) error {
|
|
||||||
ok, err := client.ChainExists(table, ChainInputFilterName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check if input chain exists: %w", err)
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
if ok, err := client.Exists("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
|
|
||||||
return err
|
|
||||||
} else if ok {
|
|
||||||
if err := client.Delete("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
|
|
||||||
log.WithError(err).Errorf("failed to delete default input rule: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err = client.ChainExists(table, ChainOutputFilterName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check if output chain exists: %w", err)
|
|
||||||
}
|
|
||||||
if ok {
|
|
||||||
if ok, err := client.Exists("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
|
|
||||||
return err
|
|
||||||
} else if ok {
|
|
||||||
if err := client.Delete("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
|
|
||||||
log.WithError(err).Errorf("failed to delete default output rule: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.ClearAndDeleteChain(table, ChainInputFilterName); err != nil {
|
|
||||||
log.Errorf("failed to clear and delete input chain: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.ClearAndDeleteChain(table, ChainOutputFilterName); err != nil {
|
|
||||||
log.Errorf("failed to clear and delete input chain: %v", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for ipsetName := range m.rulesets {
|
|
||||||
if err := ipset.Flush(ipsetName); err != nil {
|
|
||||||
log.Errorf("flush ipset %q during reset: %v", ipsetName, err)
|
|
||||||
}
|
|
||||||
if err := ipset.Destroy(ipsetName); err != nil {
|
|
||||||
log.Errorf("delete ipset %q during reset: %v", ipsetName, err)
|
|
||||||
}
|
|
||||||
delete(m.rulesets, ipsetName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterRuleSpecs returns the specs of a filtering rule
|
|
||||||
func (m *Manager) filterRuleSpecs(
|
|
||||||
table string, ip net.IP, protocol string, sPort, dPort string,
|
|
||||||
direction fw.RuleDirection, action fw.Action, comment string,
|
|
||||||
ipsetName string,
|
|
||||||
) (specs []string) {
|
|
||||||
matchByIP := true
|
|
||||||
// don't use IP matching if IP is ip 0.0.0.0
|
|
||||||
if s := ip.String(); s == "0.0.0.0" || s == "::" {
|
|
||||||
matchByIP = false
|
|
||||||
}
|
|
||||||
switch direction {
|
|
||||||
case fw.RuleDirectionIN:
|
|
||||||
if matchByIP {
|
|
||||||
if ipsetName != "" {
|
|
||||||
specs = append(specs, "-m", "set", "--set", ipsetName, "src")
|
|
||||||
} else {
|
|
||||||
specs = append(specs, "-s", ip.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case fw.RuleDirectionOUT:
|
|
||||||
if matchByIP {
|
|
||||||
if ipsetName != "" {
|
|
||||||
specs = append(specs, "-m", "set", "--set", ipsetName, "dst")
|
|
||||||
} else {
|
|
||||||
specs = append(specs, "-d", ip.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if protocol != "all" {
|
|
||||||
specs = append(specs, "-p", protocol)
|
|
||||||
}
|
|
||||||
if sPort != "" {
|
|
||||||
specs = append(specs, "--sport", sPort)
|
|
||||||
}
|
|
||||||
if dPort != "" {
|
|
||||||
specs = append(specs, "--dport", dPort)
|
|
||||||
}
|
|
||||||
specs = append(specs, "-j", m.actionToStr(action))
|
|
||||||
return append(specs, "-m", "comment", "--comment", comment)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rawClient returns corresponding iptables client for the given ip
|
|
||||||
func (m *Manager) rawClient(ip net.IP) (*iptables.IPTables, error) {
|
|
||||||
if ip.To4() != nil {
|
|
||||||
return m.ipv4Client, nil
|
|
||||||
}
|
|
||||||
if m.ipv6Client == nil {
|
|
||||||
return nil, fmt.Errorf("ipv6 is not supported")
|
|
||||||
}
|
|
||||||
return m.ipv6Client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// client returns client with initialized chain and default rules
|
|
||||||
func (m *Manager) client(ip net.IP) (*iptables.IPTables, error) {
|
|
||||||
client, err := m.rawClient(ip)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := client.ChainExists("filter", ChainInputFilterName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to check if chain exists: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
if err := client.NewChain("filter", ChainInputFilterName); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create input chain: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.AppendUnique("filter", ChainInputFilterName, dropAllDefaultRule...); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create default drop all in netbird input chain: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.AppendUnique("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create input chain jump rule: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err = client.ChainExists("filter", ChainOutputFilterName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to check if chain exists: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
if err := client.NewChain("filter", ChainOutputFilterName); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create output chain: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.AppendUnique("filter", ChainOutputFilterName, dropAllDefaultRule...); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create default drop all in netbird output chain: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := client.AppendUnique("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create output chain jump rule: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) actionToStr(action fw.Action) string {
|
|
||||||
if action == fw.ActionAccept {
|
|
||||||
return "ACCEPT"
|
|
||||||
}
|
|
||||||
return "DROP"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) transformIPsetName(ipsetName string, sPort, dPort string) string {
|
|
||||||
if ipsetName == "" {
|
|
||||||
return ""
|
|
||||||
} else if sPort != "" && dPort != "" {
|
|
||||||
return ipsetName + "-sport-dport"
|
|
||||||
} else if sPort != "" {
|
|
||||||
return ipsetName + "-sport"
|
|
||||||
} else if dPort != "" {
|
|
||||||
return ipsetName + "-dport"
|
|
||||||
}
|
|
||||||
return ipsetName
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package iptables
|
package iptables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -9,7 +10,7 @@ import (
|
|||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +34,8 @@ func (i *iFaceMock) Address() iface.WGAddress {
|
|||||||
panic("AddressFunc is not set")
|
panic("AddressFunc is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *iFaceMock) IsUserspaceBind() bool { return false }
|
||||||
|
|
||||||
func TestIptablesManager(t *testing.T) {
|
func TestIptablesManager(t *testing.T) {
|
||||||
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -53,7 +56,7 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock)
|
manager, err := Create(context.Background(), mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -65,17 +68,20 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var rule1 fw.Rule
|
var rule1 []fw.Rule
|
||||||
t.Run("add first rule", func(t *testing.T) {
|
t.Run("add first rule", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.2")
|
ip := net.ParseIP("10.20.0.2")
|
||||||
port := &fw.Port{Values: []int{8080}}
|
port := &fw.Port{Values: []int{8080}}
|
||||||
rule1, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic")
|
rule1, err = manager.AddFiltering(ip, "tcp", nil, port, fw.RuleDirectionOUT, fw.ActionAccept, "", "accept HTTP traffic")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, true, rule1.(*Rule).specs...)
|
for _, r := range rule1 {
|
||||||
|
checkRuleSpecs(t, ipv4Client, chainNameOutputRules, true, r.(*Rule).specs...)
|
||||||
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
var rule2 fw.Rule
|
var rule2 []fw.Rule
|
||||||
t.Run("add second rule", func(t *testing.T) {
|
t.Run("add second rule", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := net.ParseIP("10.20.0.3")
|
||||||
port := &fw.Port{
|
port := &fw.Port{
|
||||||
@@ -85,21 +91,28 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTPS traffic from ports range")
|
ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept, "", "accept HTTPS traffic from ports range")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
checkRuleSpecs(t, ipv4Client, ChainInputFilterName, true, rule2.(*Rule).specs...)
|
for _, r := range rule2 {
|
||||||
|
rr := r.(*Rule)
|
||||||
|
checkRuleSpecs(t, ipv4Client, rr.chain, true, rr.specs...)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("delete first rule", func(t *testing.T) {
|
t.Run("delete first rule", func(t *testing.T) {
|
||||||
err := manager.DeleteRule(rule1)
|
for _, r := range rule1 {
|
||||||
|
err := manager.DeleteRule(r)
|
||||||
require.NoError(t, err, "failed to delete rule")
|
require.NoError(t, err, "failed to delete rule")
|
||||||
|
|
||||||
checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, false, rule1.(*Rule).specs...)
|
checkRuleSpecs(t, ipv4Client, chainNameOutputRules, false, r.(*Rule).specs...)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("delete second rule", func(t *testing.T) {
|
t.Run("delete second rule", func(t *testing.T) {
|
||||||
err := manager.DeleteRule(rule2)
|
for _, r := range rule2 {
|
||||||
|
err := manager.DeleteRule(r)
|
||||||
require.NoError(t, err, "failed to delete rule")
|
require.NoError(t, err, "failed to delete rule")
|
||||||
|
}
|
||||||
|
|
||||||
require.Empty(t, manager.rulesets, "rulesets index after removed second rule must be empty")
|
require.Empty(t, manager.aclMgr.ipsetStore.ipsets, "rulesets index after removed second rule must be empty")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("reset check", func(t *testing.T) {
|
t.Run("reset check", func(t *testing.T) {
|
||||||
@@ -112,11 +125,11 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
err = manager.Reset()
|
err = manager.Reset()
|
||||||
require.NoError(t, err, "failed to reset")
|
require.NoError(t, err, "failed to reset")
|
||||||
|
|
||||||
ok, err := ipv4Client.ChainExists("filter", ChainInputFilterName)
|
ok, err := ipv4Client.ChainExists("filter", chainNameInputRules)
|
||||||
require.NoError(t, err, "failed check chain exists")
|
require.NoError(t, err, "failed check chain exists")
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
require.NoErrorf(t, err, "chain '%v' still exists after Reset", ChainInputFilterName)
|
require.NoErrorf(t, err, "chain '%v' still exists after Reset", chainNameInputRules)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -141,7 +154,7 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock)
|
manager, err := Create(context.Background(), mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -153,7 +166,7 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var rule1 fw.Rule
|
var rule1 []fw.Rule
|
||||||
t.Run("add first rule with set", func(t *testing.T) {
|
t.Run("add first rule with set", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.2")
|
ip := net.ParseIP("10.20.0.2")
|
||||||
port := &fw.Port{Values: []int{8080}}
|
port := &fw.Port{Values: []int{8080}}
|
||||||
@@ -163,12 +176,14 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
checkRuleSpecs(t, ipv4Client, ChainOutputFilterName, true, rule1.(*Rule).specs...)
|
for _, r := range rule1 {
|
||||||
require.Equal(t, rule1.(*Rule).ipsetName, "default-dport", "ipset name must be set")
|
checkRuleSpecs(t, ipv4Client, chainNameOutputRules, true, r.(*Rule).specs...)
|
||||||
require.Equal(t, rule1.(*Rule).ip, "10.20.0.2", "ipset IP must be set")
|
require.Equal(t, r.(*Rule).ipsetName, "default-dport", "ipset name must be set")
|
||||||
|
require.Equal(t, r.(*Rule).ip, "10.20.0.2", "ipset IP must be set")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var rule2 fw.Rule
|
var rule2 []fw.Rule
|
||||||
t.Run("add second rule", func(t *testing.T) {
|
t.Run("add second rule", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := net.ParseIP("10.20.0.3")
|
||||||
port := &fw.Port{
|
port := &fw.Port{
|
||||||
@@ -178,23 +193,29 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept,
|
ip, "tcp", port, nil, fw.RuleDirectionIN, fw.ActionAccept,
|
||||||
"default", "accept HTTPS traffic from ports range",
|
"default", "accept HTTPS traffic from ports range",
|
||||||
)
|
)
|
||||||
|
for _, r := range rule2 {
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
require.Equal(t, rule2.(*Rule).ipsetName, "default-sport", "ipset name must be set")
|
require.Equal(t, r.(*Rule).ipsetName, "default-sport", "ipset name must be set")
|
||||||
require.Equal(t, rule2.(*Rule).ip, "10.20.0.3", "ipset IP must be set")
|
require.Equal(t, r.(*Rule).ip, "10.20.0.3", "ipset IP must be set")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("delete first rule", func(t *testing.T) {
|
t.Run("delete first rule", func(t *testing.T) {
|
||||||
err := manager.DeleteRule(rule1)
|
for _, r := range rule1 {
|
||||||
|
err := manager.DeleteRule(r)
|
||||||
require.NoError(t, err, "failed to delete rule")
|
require.NoError(t, err, "failed to delete rule")
|
||||||
|
|
||||||
require.NotContains(t, manager.rulesets, rule1.(*Rule).ruleID, "rule must be removed form the ruleset index")
|
require.NotContains(t, manager.aclMgr.ipsetStore.ipsets, r.(*Rule).ruleID, "rule must be removed form the ruleset index")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("delete second rule", func(t *testing.T) {
|
t.Run("delete second rule", func(t *testing.T) {
|
||||||
err := manager.DeleteRule(rule2)
|
for _, r := range rule2 {
|
||||||
|
err := manager.DeleteRule(r)
|
||||||
require.NoError(t, err, "failed to delete rule")
|
require.NoError(t, err, "failed to delete rule")
|
||||||
|
|
||||||
require.Empty(t, manager.rulesets, "rulesets index after removed second rule must be empty")
|
require.Empty(t, manager.aclMgr.ipsetStore.ipsets, "rulesets index after removed second rule must be empty")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("reset check", func(t *testing.T) {
|
t.Run("reset check", func(t *testing.T) {
|
||||||
@@ -204,6 +225,7 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName string, mustExists bool, rulespec ...string) {
|
func checkRuleSpecs(t *testing.T, ipv4Client *iptables.IPTables, chainName string, mustExists bool, rulespec ...string) {
|
||||||
|
t.Helper()
|
||||||
exists, err := ipv4Client.Exists("filter", chainName, rulespec...)
|
exists, err := ipv4Client.Exists("filter", chainName, rulespec...)
|
||||||
require.NoError(t, err, "failed to check rule")
|
require.NoError(t, err, "failed to check rule")
|
||||||
require.Falsef(t, !exists && mustExists, "rule '%v' does not exist", rulespec)
|
require.Falsef(t, !exists && mustExists, "rule '%v' does not exist", rulespec)
|
||||||
@@ -229,7 +251,7 @@ func TestIptablesCreatePerformance(t *testing.T) {
|
|||||||
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
||||||
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock)
|
manager, err := Create(context.Background(), mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
@@ -240,7 +262,6 @@ func TestIptablesCreatePerformance(t *testing.T) {
|
|||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
_, err = manager.client(net.ParseIP("10.20.0.100"))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ip := net.ParseIP("10.20.0.100")
|
ip := net.ParseIP("10.20.0.100")
|
||||||
|
|||||||
372
client/firewall/iptables/router_linux.go
Normal file
372
client/firewall/iptables/router_linux.go
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package iptables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Ipv4Forwarding = "netbird-rt-forwarding"
|
||||||
|
ipv4Nat = "netbird-rt-nat"
|
||||||
|
)
|
||||||
|
|
||||||
|
// constants needed to manage and create iptable rules
|
||||||
|
const (
|
||||||
|
tableFilter = "filter"
|
||||||
|
tableNat = "nat"
|
||||||
|
chainFORWARD = "FORWARD"
|
||||||
|
chainPOSTROUTING = "POSTROUTING"
|
||||||
|
chainRTNAT = "NETBIRD-RT-NAT"
|
||||||
|
chainRTFWD = "NETBIRD-RT-FWD"
|
||||||
|
routingFinalForwardJump = "ACCEPT"
|
||||||
|
routingFinalNatJump = "MASQUERADE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type routerManager struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
iptablesClient *iptables.IPTables
|
||||||
|
rules map[string][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouterManager(parentCtx context.Context, iptablesClient *iptables.IPTables) (*routerManager, error) {
|
||||||
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
|
m := &routerManager{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
iptablesClient: iptablesClient,
|
||||||
|
rules: make(map[string][]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := m.cleanUpDefaultForwardRules()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to cleanup routing rules: %s", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = m.createContainers()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create containers for route: %s", err)
|
||||||
|
}
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertRoutingRules inserts an iptables rule pair to the forwarding chain and if enabled, to the nat chain
|
||||||
|
func (i *routerManager) InsertRoutingRules(pair firewall.RouterPair) error {
|
||||||
|
err := i.insertRoutingRule(firewall.ForwardingFormat, tableFilter, chainRTFWD, routingFinalForwardJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.insertRoutingRule(firewall.InForwardingFormat, tableFilter, chainRTFWD, routingFinalForwardJump, firewall.GetInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.Masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.addNATRule(firewall.NatFormat, tableNat, chainRTNAT, routingFinalNatJump, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.addNATRule(firewall.InNatFormat, tableNat, chainRTNAT, routingFinalNatJump, firewall.GetInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertRoutingRule inserts an iptables rule
|
||||||
|
func (i *routerManager) insertRoutingRule(keyFormat, table, chain, jump string, pair firewall.RouterPair) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ruleKey := firewall.GenKey(keyFormat, pair.ID)
|
||||||
|
rule := genRuleSpec(jump, pair.Source, pair.Destination)
|
||||||
|
existingRule, found := i.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
err = i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
|
||||||
|
}
|
||||||
|
delete(i.rules, ruleKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.iptablesClient.Insert(table, chain, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while adding new %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.rules[ruleKey] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes an iptables rule pair from forwarding and nat chains
|
||||||
|
func (i *routerManager) RemoveRoutingRules(pair firewall.RouterPair) error {
|
||||||
|
err := i.removeRoutingRule(firewall.ForwardingFormat, tableFilter, chainRTFWD, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(firewall.InForwardingFormat, tableFilter, chainRTFWD, firewall.GetInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !pair.Masquerade {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(firewall.NatFormat, tableNat, chainRTNAT, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.removeRoutingRule(firewall.InNatFormat, tableNat, chainRTNAT, firewall.GetInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *routerManager) removeRoutingRule(keyFormat, table, chain string, pair firewall.RouterPair) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ruleKey := firewall.GenKey(keyFormat, pair.ID)
|
||||||
|
existingRule, found := i.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
err = i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while removing existing %s rule for %s: %v", getIptablesRuleType(table), pair.Destination, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(i.rules, ruleKey)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *routerManager) RouteingFwChainName() string {
|
||||||
|
return chainRTFWD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *routerManager) Reset() error {
|
||||||
|
err := i.cleanUpDefaultForwardRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules = make(map[string][]string)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *routerManager) cleanUpDefaultForwardRules() error {
|
||||||
|
err := i.cleanJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("flushing routing related tables")
|
||||||
|
ok, err := i.iptablesClient.ChainExists(tableFilter, chainRTFWD)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed check chain %s,error: %v", chainRTFWD, err)
|
||||||
|
return err
|
||||||
|
} else if ok {
|
||||||
|
err = i.iptablesClient.ClearAndDeleteChain(tableFilter, chainRTFWD)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed cleaning chain %s,error: %v", chainRTFWD, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err = i.iptablesClient.ChainExists(tableNat, chainRTNAT)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed check chain %s,error: %v", chainRTNAT, err)
|
||||||
|
return err
|
||||||
|
} else if ok {
|
||||||
|
err = i.iptablesClient.ClearAndDeleteChain(tableNat, chainRTNAT)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed cleaning chain %s,error: %v", chainRTNAT, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *routerManager) createContainers() error {
|
||||||
|
if i.rules[Ipv4Forwarding] != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errMSGFormat := "failed creating chain %s,error: %v"
|
||||||
|
err := i.createChain(tableFilter, chainRTFWD)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, chainRTFWD, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.createChain(tableNat, chainRTNAT)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, chainRTNAT, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.addJumpRules()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while creating jump rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addJumpRules create jump rules to send packets to NetBird chains
|
||||||
|
func (i *routerManager) addJumpRules() error {
|
||||||
|
rule := []string{"-j", chainRTFWD}
|
||||||
|
err := i.iptablesClient.Insert(tableFilter, chainFORWARD, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[Ipv4Forwarding] = rule
|
||||||
|
|
||||||
|
rule = []string{"-j", chainRTNAT}
|
||||||
|
err = i.iptablesClient.Insert(tableNat, chainPOSTROUTING, 1, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
i.rules[ipv4Nat] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanJumpRules cleans jump rules that was sending packets to NetBird chains
|
||||||
|
func (i *routerManager) cleanJumpRules() error {
|
||||||
|
var err error
|
||||||
|
errMSGFormat := "failed cleaning rule from chain %s,err: %v"
|
||||||
|
rule, found := i.rules[Ipv4Forwarding]
|
||||||
|
if found {
|
||||||
|
err = i.iptablesClient.DeleteIfExists(tableFilter, chainFORWARD, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, chainFORWARD, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule, found = i.rules[ipv4Nat]
|
||||||
|
if found {
|
||||||
|
err = i.iptablesClient.DeleteIfExists(tableNat, chainPOSTROUTING, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errMSGFormat, chainPOSTROUTING, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err := i.iptablesClient.List("nat", "POSTROUTING")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list rules: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ruleString := range rules {
|
||||||
|
if !strings.Contains(ruleString, "NETBIRD") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rule := strings.Fields(ruleString)
|
||||||
|
err := i.iptablesClient.DeleteIfExists("nat", "POSTROUTING", rule[2:]...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete postrouting jump rule: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err = i.iptablesClient.List(tableFilter, "FORWARD")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to list rules in FORWARD chain: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ruleString := range rules {
|
||||||
|
if !strings.Contains(ruleString, "NETBIRD") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rule := strings.Fields(ruleString)
|
||||||
|
err := i.iptablesClient.DeleteIfExists(tableFilter, "FORWARD", rule[2:]...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete FORWARD jump rule: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *routerManager) createChain(table, newChain string) error {
|
||||||
|
chains, err := i.iptablesClient.ListChains(table)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't get %s table chains, error: %v", table, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCreateChain := true
|
||||||
|
for _, chain := range chains {
|
||||||
|
if chain == newChain {
|
||||||
|
shouldCreateChain = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCreateChain {
|
||||||
|
err = i.iptablesClient.NewChain(table, newChain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create chain %s in %s table, error: %v", newChain, table, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the loopback return rule to the NAT chain
|
||||||
|
loopbackRule := []string{"-o", "lo", "-j", "RETURN"}
|
||||||
|
err = i.iptablesClient.Insert(table, newChain, 1, loopbackRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to add loopback return rule to %s: %v", chainRTNAT, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = i.iptablesClient.Append(table, newChain, "-j", "RETURN")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("couldn't create chain %s default rule, error: %v", newChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addNATRule appends an iptables rule pair to the nat chain
|
||||||
|
func (i *routerManager) addNATRule(keyFormat, table, chain, jump string, pair firewall.RouterPair) error {
|
||||||
|
ruleKey := firewall.GenKey(keyFormat, pair.ID)
|
||||||
|
rule := genRuleSpec(jump, pair.Source, pair.Destination)
|
||||||
|
existingRule, found := i.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
err := i.iptablesClient.DeleteIfExists(table, chain, existingRule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while removing existing NAT rule for %s: %v", pair.Destination, err)
|
||||||
|
}
|
||||||
|
delete(i.rules, ruleKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inserting after loopback ignore rule
|
||||||
|
err := i.iptablesClient.Insert(table, chain, 2, rule...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error while appending new NAT rule for %s: %v", pair.Destination, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.rules[ruleKey] = rule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// genRuleSpec generates rule specification
|
||||||
|
func genRuleSpec(jump, source, destination string) []string {
|
||||||
|
return []string{"-s", source, "-d", destination, "-j", jump}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIptablesRuleType(table string) string {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if table == tableNat {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
return ruleType
|
||||||
|
}
|
||||||
227
client/firewall/iptables/router_linux_test.go
Normal file
227
client/firewall/iptables/router_linux_test.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package iptables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isIptablesSupported() bool {
|
||||||
|
_, err4 := exec.LookPath("iptables")
|
||||||
|
return err4 == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptablesManager_RestoreOrCreateContainers(t *testing.T) {
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
require.NoError(t, err, "failed to init iptables client")
|
||||||
|
|
||||||
|
manager, err := newRouterManager(context.TODO(), iptablesClient)
|
||||||
|
require.NoError(t, err, "should return a valid iptables manager")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
_ = manager.Reset()
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.Len(t, manager.rules, 2, "should have created rules map")
|
||||||
|
|
||||||
|
exists, err := manager.iptablesClient.Exists(tableFilter, chainFORWARD, manager.rules[Ipv4Forwarding]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainFORWARD)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
exists, err = manager.iptablesClient.Exists(tableNat, chainPOSTROUTING, manager.rules[ipv4Nat]...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainPOSTROUTING)
|
||||||
|
require.True(t, exists, "postrouting rule should exist")
|
||||||
|
|
||||||
|
pair := firewall.RouterPair{
|
||||||
|
ID: "abc",
|
||||||
|
Source: "100.100.100.1/32",
|
||||||
|
Destination: "100.100.100.0/24",
|
||||||
|
Masquerade: true,
|
||||||
|
}
|
||||||
|
forward4Rule := genRuleSpec(routingFinalForwardJump, pair.Source, pair.Destination)
|
||||||
|
|
||||||
|
err = manager.iptablesClient.Insert(tableFilter, chainRTFWD, 1, forward4Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
nat4Rule := genRuleSpec(routingFinalNatJump, pair.Source, pair.Destination)
|
||||||
|
|
||||||
|
err = manager.iptablesClient.Insert(tableNat, chainRTNAT, 1, nat4Rule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
err = manager.Reset()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIptablesManager_InsertRoutingRules(t *testing.T) {
|
||||||
|
|
||||||
|
if !isIptablesSupported() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range test.InsertRuleTestCases {
|
||||||
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
|
iptablesClient, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
require.NoError(t, err, "failed to init iptables client")
|
||||||
|
|
||||||
|
manager, err := newRouterManager(context.TODO(), iptablesClient)
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := manager.Reset()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to reset iptables manager: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = manager.InsertRoutingRules(testCase.InputPair)
|
||||||
|
require.NoError(t, err, "forwarding pair should be inserted")
|
||||||
|
|
||||||
|
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||||
|
forwardRule := genRuleSpec(routingFinalForwardJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
|
exists, err := iptablesClient.Exists(tableFilter, chainRTFWD, forwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
||||||
|
require.True(t, exists, "forwarding rule should exist")
|
||||||
|
|
||||||
|
foundRule, found := manager.rules[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 := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(tableFilter, chainRTFWD, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
||||||
|
require.True(t, exists, "income forwarding rule should exist")
|
||||||
|
|
||||||
|
foundRule, found = manager.rules[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 := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||||
|
natRule := genRuleSpec(routingFinalNatJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, natRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
||||||
|
if testCase.InputPair.Masquerade {
|
||||||
|
require.True(t, exists, "nat rule should be created")
|
||||||
|
foundNatRule, foundNat := manager.rules[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[natRuleKey]
|
||||||
|
require.False(t, foundNat, "nat rule should not exist in the map")
|
||||||
|
}
|
||||||
|
|
||||||
|
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
||||||
|
if testCase.InputPair.Masquerade {
|
||||||
|
require.True(t, exists, "income nat rule should be created")
|
||||||
|
foundNatRule, foundNat := manager.rules[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[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 test.RemoveRuleTestCases {
|
||||||
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
|
iptablesClient, _ := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
|
||||||
|
manager, err := newRouterManager(context.TODO(), iptablesClient)
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
defer func() {
|
||||||
|
_ = manager.Reset()
|
||||||
|
}()
|
||||||
|
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||||
|
forwardRule := genRuleSpec(routingFinalForwardJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(tableFilter, chainRTFWD, 1, forwardRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||||
|
inForwardRule := genRuleSpec(routingFinalForwardJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(tableFilter, chainRTFWD, 1, inForwardRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||||
|
natRule := genRuleSpec(routingFinalNatJump, testCase.InputPair.Source, testCase.InputPair.Destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, natRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||||
|
inNatRule := genRuleSpec(routingFinalNatJump, firewall.GetInPair(testCase.InputPair).Source, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
|
err = iptablesClient.Insert(tableNat, chainRTNAT, 1, inNatRule...)
|
||||||
|
require.NoError(t, err, "inserting rule should not return error")
|
||||||
|
|
||||||
|
err = manager.Reset()
|
||||||
|
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(tableFilter, chainRTFWD, forwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
||||||
|
require.False(t, exists, "forwarding rule should not exist")
|
||||||
|
|
||||||
|
_, found := manager.rules[forwardRuleKey]
|
||||||
|
require.False(t, found, "forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(tableFilter, chainRTFWD, inForwardRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableFilter, chainRTFWD)
|
||||||
|
require.False(t, exists, "income forwarding rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[inForwardRuleKey]
|
||||||
|
require.False(t, found, "income forwarding rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, natRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
||||||
|
require.False(t, exists, "nat rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[natRuleKey]
|
||||||
|
require.False(t, found, "nat rule should exist in the manager map")
|
||||||
|
|
||||||
|
exists, err = iptablesClient.Exists(tableNat, chainRTNAT, inNatRule...)
|
||||||
|
require.NoError(t, err, "should be able to query the iptables %s table and %s chain", tableNat, chainRTNAT)
|
||||||
|
require.False(t, exists, "income nat rule should not exist")
|
||||||
|
|
||||||
|
_, found = manager.rules[inNatRuleKey]
|
||||||
|
require.False(t, found, "income nat rule should exist in the manager map")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,7 @@ type Rule struct {
|
|||||||
|
|
||||||
specs []string
|
specs []string
|
||||||
ip string
|
ip string
|
||||||
dst bool
|
chain string
|
||||||
v6 bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRuleID returns the rule id
|
// GetRuleID returns the rule id
|
||||||
|
|||||||
50
client/firewall/iptables/rulestore_linux.go
Normal file
50
client/firewall/iptables/rulestore_linux.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package iptables
|
||||||
|
|
||||||
|
type ipList struct {
|
||||||
|
ips map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIpList(ip string) ipList {
|
||||||
|
ips := make(map[string]struct{})
|
||||||
|
ips[ip] = struct{}{}
|
||||||
|
|
||||||
|
return ipList{
|
||||||
|
ips: ips,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipList) addIP(ip string) {
|
||||||
|
s.ips[ip] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipsetStore struct {
|
||||||
|
ipsets map[string]ipList // ipsetName -> ruleset
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIpsetStore() *ipsetStore {
|
||||||
|
return &ipsetStore{
|
||||||
|
ipsets: make(map[string]ipList),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) ipset(ipsetName string) (ipList, bool) {
|
||||||
|
r, ok := s.ipsets[ipsetName]
|
||||||
|
return r, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) addIpList(ipsetName string, list ipList) {
|
||||||
|
s.ipsets[ipsetName] = list
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) deleteIpset(ipsetName string) {
|
||||||
|
s.ipsets[ipsetName] = ipList{}
|
||||||
|
delete(s.ipsets, ipsetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) ipsetNames() []string {
|
||||||
|
names := make([]string, 0, len(s.ipsets))
|
||||||
|
for name := range s.ipsets {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
package firewall
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
NatFormat = "netbird-nat-%s"
|
||||||
|
ForwardingFormat = "netbird-fwd-%s"
|
||||||
|
InNatFormat = "netbird-nat-in-%s"
|
||||||
|
InForwardingFormat = "netbird-fwd-in-%s"
|
||||||
|
)
|
||||||
|
|
||||||
// Rule abstraction should be implemented by each firewall manager
|
// Rule abstraction should be implemented by each firewall manager
|
||||||
//
|
//
|
||||||
// Each firewall type for different OS can use different type
|
// Each firewall type for different OS can use different type
|
||||||
@@ -27,10 +35,8 @@ const (
|
|||||||
type Action int
|
type Action int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ActionUnknown is a unknown action
|
|
||||||
ActionUnknown Action = iota
|
|
||||||
// ActionAccept is the action to accept a packet
|
// ActionAccept is the action to accept a packet
|
||||||
ActionAccept
|
ActionAccept Action = iota
|
||||||
// ActionDrop is the action to drop a packet
|
// ActionDrop is the action to drop a packet
|
||||||
ActionDrop
|
ActionDrop
|
||||||
)
|
)
|
||||||
@@ -40,6 +46,9 @@ const (
|
|||||||
// It declares methods which handle actions required by the
|
// It declares methods which handle actions required by the
|
||||||
// Netbird client for ACL and routing functionality
|
// Netbird client for ACL and routing functionality
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
|
// AllowNetbird allows netbird interface traffic
|
||||||
|
AllowNetbird() error
|
||||||
|
|
||||||
// AddFiltering rule to the firewall
|
// AddFiltering rule to the firewall
|
||||||
//
|
//
|
||||||
// If comment argument is empty firewall manager should set
|
// If comment argument is empty firewall manager should set
|
||||||
@@ -53,16 +62,27 @@ type Manager interface {
|
|||||||
action Action,
|
action Action,
|
||||||
ipsetName string,
|
ipsetName string,
|
||||||
comment string,
|
comment string,
|
||||||
) (Rule, error)
|
) ([]Rule, error)
|
||||||
|
|
||||||
// DeleteRule from the firewall by rule definition
|
// DeleteRule from the firewall by rule definition
|
||||||
DeleteRule(rule Rule) error
|
DeleteRule(rule Rule) error
|
||||||
|
|
||||||
|
// IsServerRouteSupported returns true if the firewall supports server side routing operations
|
||||||
|
IsServerRouteSupported() bool
|
||||||
|
|
||||||
|
// InsertRoutingRules inserts a routing firewall rule
|
||||||
|
InsertRoutingRules(pair RouterPair) error
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes a routing firewall rule
|
||||||
|
RemoveRoutingRules(pair RouterPair) error
|
||||||
|
|
||||||
// Reset firewall to the default state
|
// Reset firewall to the default state
|
||||||
Reset() error
|
Reset() error
|
||||||
|
|
||||||
// Flush the changes to firewall controller
|
// Flush the changes to firewall controller
|
||||||
Flush() error
|
Flush() error
|
||||||
|
}
|
||||||
// TODO: migrate routemanager firewal actions to this interface
|
|
||||||
|
func GenKey(format string, input string) string {
|
||||||
|
return fmt.Sprintf(format, input)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package firewall
|
package manager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
18
client/firewall/manager/routerpair.go
Normal file
18
client/firewall/manager/routerpair.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package manager
|
||||||
|
|
||||||
|
type RouterPair struct {
|
||||||
|
ID string
|
||||||
|
Source string
|
||||||
|
Destination string
|
||||||
|
Masquerade bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInPair(pair RouterPair) RouterPair {
|
||||||
|
return RouterPair{
|
||||||
|
ID: pair.ID,
|
||||||
|
// invert Source/Destination
|
||||||
|
Source: pair.Destination,
|
||||||
|
Destination: pair.Source,
|
||||||
|
Masquerade: pair.Masquerade,
|
||||||
|
}
|
||||||
|
}
|
||||||
1196
client/firewall/nftables/acl_linux.go
Normal file
1196
client/firewall/nftables/acl_linux.go
Normal file
File diff suppressed because it is too large
Load Diff
85
client/firewall/nftables/ipsetstore_linux.go
Normal file
85
client/firewall/nftables/ipsetstore_linux.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package nftables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ipsetStore struct {
|
||||||
|
ipsetReference map[string]int
|
||||||
|
ipsets map[string]map[string]struct{} // ipsetName -> list of ips
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIpsetStore() *ipsetStore {
|
||||||
|
return &ipsetStore{
|
||||||
|
ipsetReference: make(map[string]int),
|
||||||
|
ipsets: make(map[string]map[string]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) ips(ipsetName string) (map[string]struct{}, bool) {
|
||||||
|
r, ok := s.ipsets[ipsetName]
|
||||||
|
return r, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) newIpset(ipsetName string) map[string]struct{} {
|
||||||
|
s.ipsetReference[ipsetName] = 0
|
||||||
|
ipList := make(map[string]struct{})
|
||||||
|
s.ipsets[ipsetName] = ipList
|
||||||
|
return ipList
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) deleteIpset(ipsetName string) {
|
||||||
|
delete(s.ipsetReference, ipsetName)
|
||||||
|
delete(s.ipsets, ipsetName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) DeleteIpFromSet(ipsetName string, ip net.IP) {
|
||||||
|
ipList, ok := s.ipsets[ipsetName]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(ipList, ip.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) AddIpToSet(ipsetName string, ip net.IP) {
|
||||||
|
ipList, ok := s.ipsets[ipsetName]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ipList[ip.String()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) IsIpInSet(ipsetName string, ip net.IP) bool {
|
||||||
|
ipList, ok := s.ipsets[ipsetName]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok = ipList[ip.String()]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) AddReferenceToIpset(ipsetName string) {
|
||||||
|
s.ipsetReference[ipsetName]++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) DeleteReferenceFromIpSet(ipsetName string) {
|
||||||
|
r, ok := s.ipsetReference[ipsetName]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.ipsetReference[ipsetName]--
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ipsetStore) HasReferenceToSet(ipsetName string) bool {
|
||||||
|
if _, ok := s.ipsetReference[ipsetName]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.ipsetReference[ipsetName] == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -2,88 +2,52 @@ package nftables
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
"github.com/google/nftables/expr"
|
"github.com/google/nftables/expr"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/iface"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// FilterTableName is the name of the table that is used for filtering by the Netbird client
|
// tableName is the name of the table that is used for filtering by the Netbird client
|
||||||
FilterTableName = "netbird-acl"
|
tableName = "netbird"
|
||||||
|
|
||||||
// FilterInputChainName is the name of the chain that is used for filtering incoming packets
|
|
||||||
FilterInputChainName = "netbird-acl-input-filter"
|
|
||||||
|
|
||||||
// FilterOutputChainName is the name of the chain that is used for filtering outgoing packets
|
|
||||||
FilterOutputChainName = "netbird-acl-output-filter"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var anyIP = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
|
||||||
|
|
||||||
// Manager of iptables firewall
|
// Manager of iptables firewall
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
|
|
||||||
rConn *nftables.Conn
|
rConn *nftables.Conn
|
||||||
sConn *nftables.Conn
|
|
||||||
tableIPv4 *nftables.Table
|
|
||||||
tableIPv6 *nftables.Table
|
|
||||||
|
|
||||||
filterInputChainIPv4 *nftables.Chain
|
|
||||||
filterOutputChainIPv4 *nftables.Chain
|
|
||||||
|
|
||||||
filterInputChainIPv6 *nftables.Chain
|
|
||||||
filterOutputChainIPv6 *nftables.Chain
|
|
||||||
|
|
||||||
rulesetManager *rulesetManager
|
|
||||||
setRemovedIPs map[string]struct{}
|
|
||||||
setRemoved map[string]*nftables.Set
|
|
||||||
|
|
||||||
wgIface iFaceMapper
|
wgIface iFaceMapper
|
||||||
}
|
|
||||||
|
|
||||||
// iFaceMapper defines subset methods of interface required for manager
|
router *router
|
||||||
type iFaceMapper interface {
|
aclManager *AclManager
|
||||||
Name() string
|
|
||||||
Address() iface.WGAddress
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create nftables firewall manager
|
// Create nftables firewall manager
|
||||||
func Create(wgIface iFaceMapper) (*Manager, error) {
|
func Create(context context.Context, wgIface iFaceMapper) (*Manager, error) {
|
||||||
// sConn is used for creating sets and adding/removing elements from them
|
m := &Manager{
|
||||||
// it's differ then rConn (which does create new conn for each flush operation)
|
rConn: &nftables.Conn{},
|
||||||
// and is permanent. Using same connection for booth type of operations
|
wgIface: wgIface,
|
||||||
// overloads netlink with high amount of rules ( > 10000)
|
}
|
||||||
sConn, err := nftables.New(nftables.AsLasting())
|
|
||||||
|
workTable, err := m.createWorkTable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := &Manager{
|
m.router, err = newRouter(context, workTable)
|
||||||
rConn: &nftables.Conn{},
|
if err != nil {
|
||||||
sConn: sConn,
|
return nil, err
|
||||||
|
|
||||||
rulesetManager: newRuleManager(),
|
|
||||||
setRemovedIPs: map[string]struct{}{},
|
|
||||||
setRemoved: map[string]*nftables.Set{},
|
|
||||||
|
|
||||||
wgIface: wgIface,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.Reset(); err != nil {
|
m.aclManager, err = newAclManager(workTable, wgIface, m.router.RouteingFwChainName())
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,533 +60,98 @@ func Create(wgIface iFaceMapper) (*Manager, error) {
|
|||||||
// rule ID as comment for the rule
|
// rule ID as comment for the rule
|
||||||
func (m *Manager) AddFiltering(
|
func (m *Manager) AddFiltering(
|
||||||
ip net.IP,
|
ip net.IP,
|
||||||
proto fw.Protocol,
|
proto firewall.Protocol,
|
||||||
sPort *fw.Port,
|
sPort *firewall.Port,
|
||||||
dPort *fw.Port,
|
dPort *firewall.Port,
|
||||||
direction fw.RuleDirection,
|
direction firewall.RuleDirection,
|
||||||
action fw.Action,
|
action firewall.Action,
|
||||||
ipsetName string,
|
ipsetName string,
|
||||||
comment string,
|
comment string,
|
||||||
) (fw.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
var (
|
|
||||||
err error
|
|
||||||
ipset *nftables.Set
|
|
||||||
table *nftables.Table
|
|
||||||
chain *nftables.Chain
|
|
||||||
)
|
|
||||||
|
|
||||||
if direction == fw.RuleDirectionOUT {
|
|
||||||
table, chain, err = m.chain(
|
|
||||||
ip,
|
|
||||||
FilterOutputChainName,
|
|
||||||
nftables.ChainHookOutput,
|
|
||||||
nftables.ChainPriorityFilter,
|
|
||||||
nftables.ChainTypeFilter)
|
|
||||||
} else {
|
|
||||||
table, chain, err = m.chain(
|
|
||||||
ip,
|
|
||||||
FilterInputChainName,
|
|
||||||
nftables.ChainHookInput,
|
|
||||||
nftables.ChainPriorityFilter,
|
|
||||||
nftables.ChainTypeFilter)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rawIP := ip.To4()
|
rawIP := ip.To4()
|
||||||
if rawIP == nil {
|
if rawIP == nil {
|
||||||
rawIP = ip.To16()
|
return nil, fmt.Errorf("unsupported IP version: %s", ip.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesetID := m.getRulesetID(ip, proto, sPort, dPort, direction, action, ipsetName)
|
return m.aclManager.AddFiltering(ip, proto, sPort, dPort, direction, action, ipsetName, comment)
|
||||||
|
|
||||||
if ipsetName != "" {
|
|
||||||
// if we already have set with given name, just add ip to the set
|
|
||||||
// and return rule with new ID in other case let's create rule
|
|
||||||
// with fresh created set and set element
|
|
||||||
|
|
||||||
var isSetNew bool
|
|
||||||
ipset, err = m.rConn.GetSetByName(table, ipsetName)
|
|
||||||
if err != nil {
|
|
||||||
if ipset, err = m.createSet(table, rawIP, ipsetName); err != nil {
|
|
||||||
return nil, fmt.Errorf("get set name: %v", err)
|
|
||||||
}
|
|
||||||
isSetNew = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.sConn.SetAddElements(ipset, []nftables.SetElement{{Key: rawIP}}); err != nil {
|
|
||||||
return nil, fmt.Errorf("add set element for the first time: %v", err)
|
|
||||||
}
|
|
||||||
if err := m.sConn.Flush(); err != nil {
|
|
||||||
return nil, fmt.Errorf("flush add elements: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isSetNew {
|
|
||||||
// if we already have nftables rules with set for given direction
|
|
||||||
// just add new rule to the ruleset and return new fw.Rule object
|
|
||||||
|
|
||||||
if ruleset, ok := m.rulesetManager.getRuleset(rulesetID); ok {
|
|
||||||
return m.rulesetManager.addRule(ruleset, rawIP)
|
|
||||||
}
|
|
||||||
// if ipset exists but it is not linked to rule for given direction
|
|
||||||
// create new rule for direction and bind ipset to it later
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ifaceKey := expr.MetaKeyIIFNAME
|
|
||||||
if direction == fw.RuleDirectionOUT {
|
|
||||||
ifaceKey = expr.MetaKeyOIFNAME
|
|
||||||
}
|
|
||||||
expressions := []expr.Any{
|
|
||||||
&expr.Meta{Key: ifaceKey, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname(m.wgIface.Name()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if proto != "all" {
|
|
||||||
expressions = append(expressions, &expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
|
||||||
Offset: uint32(9),
|
|
||||||
Len: uint32(1),
|
|
||||||
})
|
|
||||||
|
|
||||||
var protoData []byte
|
|
||||||
switch proto {
|
|
||||||
case fw.ProtocolTCP:
|
|
||||||
protoData = []byte{unix.IPPROTO_TCP}
|
|
||||||
case fw.ProtocolUDP:
|
|
||||||
protoData = []byte{unix.IPPROTO_UDP}
|
|
||||||
case fw.ProtocolICMP:
|
|
||||||
protoData = []byte{unix.IPPROTO_ICMP}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported protocol: %s", proto)
|
|
||||||
}
|
|
||||||
expressions = append(expressions, &expr.Cmp{
|
|
||||||
Register: 1,
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Data: protoData,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if rawIP contains zeroed IPv4 0.0.0.0 or same IPv6 value
|
|
||||||
// in that case not add IP match expression into the rule definition
|
|
||||||
if !bytes.HasPrefix(anyIP, rawIP) {
|
|
||||||
// source address position
|
|
||||||
addrLen := uint32(len(rawIP))
|
|
||||||
addrOffset := uint32(12)
|
|
||||||
if addrLen == 16 {
|
|
||||||
addrOffset = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
// change to destination address position if need
|
|
||||||
if direction == fw.RuleDirectionOUT {
|
|
||||||
addrOffset += addrLen
|
|
||||||
}
|
|
||||||
|
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
|
||||||
Offset: addrOffset,
|
|
||||||
Len: addrLen,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
// add individual IP for match if no ipset defined
|
|
||||||
if ipset == nil {
|
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: rawIP,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Lookup{
|
|
||||||
SourceRegister: 1,
|
|
||||||
SetName: ipsetName,
|
|
||||||
SetID: ipset.ID,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sPort != nil && len(sPort.Values) != 0 {
|
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 0,
|
|
||||||
Len: 2,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: encodePort(*sPort),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dPort != nil && len(dPort.Values) != 0 {
|
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 2,
|
|
||||||
Len: 2,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: encodePort(*dPort),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if action == fw.ActionAccept {
|
|
||||||
expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictAccept})
|
|
||||||
} else {
|
|
||||||
expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictDrop})
|
|
||||||
}
|
|
||||||
|
|
||||||
userData := []byte(strings.Join([]string{rulesetID, comment}, " "))
|
|
||||||
|
|
||||||
rule := m.rConn.InsertRule(&nftables.Rule{
|
|
||||||
Table: table,
|
|
||||||
Chain: chain,
|
|
||||||
Position: 0,
|
|
||||||
Exprs: expressions,
|
|
||||||
UserData: userData,
|
|
||||||
})
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
|
||||||
return nil, fmt.Errorf("flush insert rule: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleset := m.rulesetManager.createRuleset(rulesetID, rule, ipset)
|
|
||||||
return m.rulesetManager.addRule(ruleset, rawIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getRulesetID returns ruleset ID based on given parameters
|
|
||||||
func (m *Manager) getRulesetID(
|
|
||||||
ip net.IP,
|
|
||||||
proto fw.Protocol,
|
|
||||||
sPort *fw.Port,
|
|
||||||
dPort *fw.Port,
|
|
||||||
direction fw.RuleDirection,
|
|
||||||
action fw.Action,
|
|
||||||
ipsetName string,
|
|
||||||
) string {
|
|
||||||
rulesetID := ":" + strconv.Itoa(int(direction)) + ":"
|
|
||||||
if sPort != nil {
|
|
||||||
rulesetID += sPort.String()
|
|
||||||
}
|
|
||||||
rulesetID += ":"
|
|
||||||
if dPort != nil {
|
|
||||||
rulesetID += dPort.String()
|
|
||||||
}
|
|
||||||
rulesetID += ":"
|
|
||||||
rulesetID += strconv.Itoa(int(action))
|
|
||||||
if ipsetName == "" {
|
|
||||||
return "ip:" + ip.String() + rulesetID
|
|
||||||
}
|
|
||||||
return "set:" + ipsetName + rulesetID
|
|
||||||
}
|
|
||||||
|
|
||||||
// createSet in given table by name
|
|
||||||
func (m *Manager) createSet(
|
|
||||||
table *nftables.Table,
|
|
||||||
rawIP []byte,
|
|
||||||
name string,
|
|
||||||
) (*nftables.Set, error) {
|
|
||||||
keyType := nftables.TypeIPAddr
|
|
||||||
if len(rawIP) == 16 {
|
|
||||||
keyType = nftables.TypeIP6Addr
|
|
||||||
}
|
|
||||||
// else we create new ipset and continue creating rule
|
|
||||||
ipset := &nftables.Set{
|
|
||||||
Name: name,
|
|
||||||
Table: table,
|
|
||||||
Dynamic: true,
|
|
||||||
KeyType: keyType,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.rConn.AddSet(ipset, nil); err != nil {
|
|
||||||
return nil, fmt.Errorf("create set: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
|
||||||
return nil, fmt.Errorf("flush created set: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ipset, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// chain returns the chain for the given IP address with specific settings
|
|
||||||
func (m *Manager) chain(
|
|
||||||
ip net.IP,
|
|
||||||
name string,
|
|
||||||
hook nftables.ChainHook,
|
|
||||||
priority nftables.ChainPriority,
|
|
||||||
cType nftables.ChainType,
|
|
||||||
) (*nftables.Table, *nftables.Chain, error) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
getChain := func(c *nftables.Chain, tf nftables.TableFamily) (*nftables.Chain, error) {
|
|
||||||
if c != nil {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
return m.createChainIfNotExists(tf, name, hook, priority, cType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip.To4() != nil {
|
|
||||||
if name == FilterInputChainName {
|
|
||||||
m.filterInputChainIPv4, err = getChain(m.filterInputChainIPv4, nftables.TableFamilyIPv4)
|
|
||||||
return m.tableIPv4, m.filterInputChainIPv4, err
|
|
||||||
}
|
|
||||||
m.filterOutputChainIPv4, err = getChain(m.filterOutputChainIPv4, nftables.TableFamilyIPv4)
|
|
||||||
return m.tableIPv4, m.filterOutputChainIPv4, err
|
|
||||||
}
|
|
||||||
if name == FilterInputChainName {
|
|
||||||
m.filterInputChainIPv6, err = getChain(m.filterInputChainIPv6, nftables.TableFamilyIPv6)
|
|
||||||
return m.tableIPv4, m.filterInputChainIPv6, err
|
|
||||||
}
|
|
||||||
m.filterOutputChainIPv6, err = getChain(m.filterOutputChainIPv6, nftables.TableFamilyIPv6)
|
|
||||||
return m.tableIPv4, m.filterOutputChainIPv6, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// table returns the table for the given family of the IP address
|
|
||||||
func (m *Manager) table(family nftables.TableFamily) (*nftables.Table, error) {
|
|
||||||
if family == nftables.TableFamilyIPv4 {
|
|
||||||
if m.tableIPv4 != nil {
|
|
||||||
return m.tableIPv4, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
table, err := m.createTableIfNotExists(nftables.TableFamilyIPv4)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m.tableIPv4 = table
|
|
||||||
return m.tableIPv4, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.tableIPv6 != nil {
|
|
||||||
return m.tableIPv6, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
table, err := m.createTableIfNotExists(nftables.TableFamilyIPv6)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m.tableIPv6 = table
|
|
||||||
return m.tableIPv6, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) createTableIfNotExists(family nftables.TableFamily) (*nftables.Table, error) {
|
|
||||||
tables, err := m.rConn.ListTablesOfFamily(family)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("list of tables: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range tables {
|
|
||||||
if t.Name == FilterTableName {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table := m.rConn.AddTable(&nftables.Table{Name: FilterTableName, Family: nftables.TableFamilyIPv4})
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return table, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) createChainIfNotExists(
|
|
||||||
family nftables.TableFamily,
|
|
||||||
name string,
|
|
||||||
hooknum nftables.ChainHook,
|
|
||||||
priority nftables.ChainPriority,
|
|
||||||
chainType nftables.ChainType,
|
|
||||||
) (*nftables.Chain, error) {
|
|
||||||
table, err := m.table(family)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
chains, err := m.rConn.ListChainsOfTableFamily(family)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("list of chains: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range chains {
|
|
||||||
if c.Name == name && c.Table.Name == table.Name {
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
polAccept := nftables.ChainPolicyAccept
|
|
||||||
chain := &nftables.Chain{
|
|
||||||
Name: name,
|
|
||||||
Table: table,
|
|
||||||
Hooknum: hooknum,
|
|
||||||
Priority: priority,
|
|
||||||
Type: chainType,
|
|
||||||
Policy: &polAccept,
|
|
||||||
}
|
|
||||||
|
|
||||||
chain = m.rConn.AddChain(chain)
|
|
||||||
|
|
||||||
ifaceKey := expr.MetaKeyIIFNAME
|
|
||||||
shiftDSTAddr := 0
|
|
||||||
if name == FilterOutputChainName {
|
|
||||||
ifaceKey = expr.MetaKeyOIFNAME
|
|
||||||
shiftDSTAddr = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
expressions := []expr.Any{
|
|
||||||
&expr.Meta{Key: ifaceKey, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname(m.wgIface.Name()),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mask, _ := netip.AddrFromSlice(m.wgIface.Address().Network.Mask)
|
|
||||||
if m.wgIface.Address().IP.To4() == nil {
|
|
||||||
ip, _ := netip.AddrFromSlice(m.wgIface.Address().Network.IP.To16())
|
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 2,
|
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
|
||||||
Offset: uint32(8 + (16 * shiftDSTAddr)),
|
|
||||||
Len: 16,
|
|
||||||
},
|
|
||||||
&expr.Bitwise{
|
|
||||||
SourceRegister: 2,
|
|
||||||
DestRegister: 2,
|
|
||||||
Len: 16,
|
|
||||||
Xor: []byte{0x0, 0x0, 0x0, 0x0},
|
|
||||||
Mask: mask.Unmap().AsSlice(),
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpNeq,
|
|
||||||
Register: 2,
|
|
||||||
Data: ip.Unmap().AsSlice(),
|
|
||||||
},
|
|
||||||
&expr.Verdict{Kind: expr.VerdictAccept},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ip, _ := netip.AddrFromSlice(m.wgIface.Address().Network.IP.To4())
|
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 2,
|
|
||||||
Base: expr.PayloadBaseNetworkHeader,
|
|
||||||
Offset: uint32(12 + (4 * shiftDSTAddr)),
|
|
||||||
Len: 4,
|
|
||||||
},
|
|
||||||
&expr.Bitwise{
|
|
||||||
SourceRegister: 2,
|
|
||||||
DestRegister: 2,
|
|
||||||
Len: 4,
|
|
||||||
Xor: []byte{0x0, 0x0, 0x0, 0x0},
|
|
||||||
Mask: m.wgIface.Address().Network.Mask,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpNeq,
|
|
||||||
Register: 2,
|
|
||||||
Data: ip.Unmap().AsSlice(),
|
|
||||||
},
|
|
||||||
&expr.Verdict{Kind: expr.VerdictAccept},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = m.rConn.AddRule(&nftables.Rule{
|
|
||||||
Table: table,
|
|
||||||
Chain: chain,
|
|
||||||
Exprs: expressions,
|
|
||||||
})
|
|
||||||
|
|
||||||
expressions = []expr.Any{
|
|
||||||
&expr.Meta{Key: ifaceKey, Register: 1},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname(m.wgIface.Name()),
|
|
||||||
},
|
|
||||||
&expr.Verdict{Kind: expr.VerdictDrop},
|
|
||||||
}
|
|
||||||
_ = m.rConn.AddRule(&nftables.Rule{
|
|
||||||
Table: table,
|
|
||||||
Chain: chain,
|
|
||||||
Exprs: expressions,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return chain, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRule from the firewall by rule definition
|
// DeleteRule from the firewall by rule definition
|
||||||
func (m *Manager) DeleteRule(rule fw.Rule) error {
|
func (m *Manager) DeleteRule(rule firewall.Rule) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
nativeRule, ok := rule.(*Rule)
|
return m.aclManager.DeleteRule(rule)
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("invalid rule type")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if nativeRule.nftRule == nil {
|
func (m *Manager) IsServerRouteSupported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) InsertRoutingRules(pair firewall.RouterPair) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.AddRoutingRules(pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) RemoveRoutingRules(pair firewall.RouterPair) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.RemoveRoutingRules(pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowNetbird allows netbird interface traffic
|
||||||
|
func (m *Manager) AllowNetbird() error {
|
||||||
|
if !m.wgIface.IsUserspaceBind() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if nativeRule.nftSet != nil {
|
m.mutex.Lock()
|
||||||
// call twice of delete set element raises error
|
defer m.mutex.Unlock()
|
||||||
// so we need to check if element is already removed
|
|
||||||
key := fmt.Sprintf("%s:%v", nativeRule.nftSet.Name, nativeRule.ip)
|
err := m.aclManager.createDefaultAllowRules()
|
||||||
if _, ok := m.setRemovedIPs[key]; !ok {
|
|
||||||
err := m.sConn.SetDeleteElements(nativeRule.nftSet, []nftables.SetElement{{Key: nativeRule.ip}})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("delete elements for set %q: %v", nativeRule.nftSet.Name, err)
|
return fmt.Errorf("failed to create default allow rules: %v", err)
|
||||||
}
|
}
|
||||||
if err := m.sConn.Flush(); err != nil {
|
|
||||||
return err
|
chains, err := m.rConn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("list of chains: %w", err)
|
||||||
}
|
}
|
||||||
m.setRemovedIPs[key] = struct{}{}
|
|
||||||
|
var chain *nftables.Chain
|
||||||
|
for _, c := range chains {
|
||||||
|
if c.Table.Name == "filter" && c.Name == "INPUT" {
|
||||||
|
chain = c
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.rulesetManager.deleteRule(nativeRule) {
|
if chain == nil {
|
||||||
// deleteRule indicates that we still have IP in the ruleset
|
log.Debugf("chain INPUT not found. Skipping add allow netbird rule")
|
||||||
// it means we should not remove the nftables rule but need to update set
|
|
||||||
// so we prepare IP to be removed from set on the next flush call
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ruleset doesn't contain IP anymore (or contains only one), remove nft rule
|
rules, err := m.rConn.GetRules(chain.Table, chain)
|
||||||
if err := m.rConn.DelRule(nativeRule.nftRule); err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to delete rule: %v", err)
|
return fmt.Errorf("failed to get rules for the INPUT chain: %v", err)
|
||||||
}
|
}
|
||||||
if err := m.rConn.Flush(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nativeRule.nftRule = nil
|
|
||||||
|
|
||||||
if nativeRule.nftSet != nil {
|
if rule := m.detectAllowNetbirdRule(rules); rule != nil {
|
||||||
if _, ok := m.setRemoved[nativeRule.nftSet.Name]; !ok {
|
log.Debugf("allow netbird rule already exists: %v", rule)
|
||||||
m.setRemoved[nativeRule.nftSet.Name] = nativeRule.nftSet
|
return nil
|
||||||
}
|
}
|
||||||
nativeRule.nftSet = nil
|
|
||||||
|
m.applyAllowNetbirdRules(chain)
|
||||||
|
|
||||||
|
err = m.rConn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to flush allow input netbird rules: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -637,18 +166,33 @@ func (m *Manager) Reset() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list of chains: %w", err)
|
return fmt.Errorf("list of chains: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range chains {
|
for _, c := range chains {
|
||||||
if c.Name == FilterInputChainName || c.Name == FilterOutputChainName {
|
// delete Netbird allow input traffic rule if it exists
|
||||||
m.rConn.DelChain(c)
|
if c.Table.Name == "filter" && c.Name == "INPUT" {
|
||||||
|
rules, err := m.rConn.GetRules(c.Table, c)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("get rules for chain %q: %v", c.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, r := range rules {
|
||||||
|
if bytes.Equal(r.UserData, []byte(allowNetbirdInputRuleID)) {
|
||||||
|
if err := m.rConn.DelRule(r); err != nil {
|
||||||
|
log.Errorf("delete rule: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.router.ResetForwardRules()
|
||||||
|
|
||||||
tables, err := m.rConn.ListTables()
|
tables, err := m.rConn.ListTables()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("list of tables: %w", err)
|
return fmt.Errorf("list of tables: %w", err)
|
||||||
}
|
}
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
if t.Name == FilterTableName {
|
if t.Name == tableName {
|
||||||
m.rConn.DelTable(t)
|
m.rConn.DelTable(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,100 +203,65 @@ func (m *Manager) Reset() error {
|
|||||||
// Flush rule/chain/set operations from the buffer
|
// Flush rule/chain/set operations from the buffer
|
||||||
//
|
//
|
||||||
// Method also get all rules after flush and refreshes handle values in the rulesets
|
// Method also get all rules after flush and refreshes handle values in the rulesets
|
||||||
|
// todo review this method usage
|
||||||
func (m *Manager) Flush() error {
|
func (m *Manager) Flush() error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
if err := m.flushWithBackoff(); err != nil {
|
return m.aclManager.Flush()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// set must be removed after flush rule changes
|
func (m *Manager) createWorkTable() (*nftables.Table, error) {
|
||||||
// otherwise we will get error
|
tables, err := m.rConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||||
for _, s := range m.setRemoved {
|
|
||||||
m.rConn.FlushSet(s)
|
|
||||||
m.rConn.DelSet(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.setRemoved) > 0 {
|
|
||||||
if err := m.flushWithBackoff(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
m.setRemovedIPs = map[string]struct{}{}
|
|
||||||
m.setRemoved = map[string]*nftables.Set{}
|
|
||||||
|
|
||||||
if err := m.refreshRuleHandles(m.tableIPv4, m.filterInputChainIPv4); err != nil {
|
|
||||||
log.Errorf("failed to refresh rule handles ipv4 input chain: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.refreshRuleHandles(m.tableIPv4, m.filterOutputChainIPv4); err != nil {
|
|
||||||
log.Errorf("failed to refresh rule handles IPv4 output chain: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.refreshRuleHandles(m.tableIPv6, m.filterInputChainIPv6); err != nil {
|
|
||||||
log.Errorf("failed to refresh rule handles IPv6 input chain: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.refreshRuleHandles(m.tableIPv6, m.filterOutputChainIPv6); err != nil {
|
|
||||||
log.Errorf("failed to refresh rule handles IPv6 output chain: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) flushWithBackoff() (err error) {
|
|
||||||
backoff := 4
|
|
||||||
backoffTime := 1000 * time.Millisecond
|
|
||||||
for i := 0; ; i++ {
|
|
||||||
err = m.rConn.Flush()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !strings.Contains(err.Error(), "busy") {
|
return nil, fmt.Errorf("list of tables: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
log.Error("failed to flush nftables, retrying...")
|
|
||||||
if i == backoff-1 {
|
for _, t := range tables {
|
||||||
return err
|
if t.Name == tableName {
|
||||||
|
m.rConn.DelTable(t)
|
||||||
}
|
}
|
||||||
time.Sleep(backoffTime)
|
}
|
||||||
backoffTime = backoffTime * 2
|
|
||||||
|
table := m.rConn.AddTable(&nftables.Table{Name: tableName, Family: nftables.TableFamilyIPv4})
|
||||||
|
err = m.rConn.Flush()
|
||||||
|
return table, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) applyAllowNetbirdRules(chain *nftables.Chain) {
|
||||||
|
rule := &nftables.Rule{
|
||||||
|
Table: chain.Table,
|
||||||
|
Chain: chain,
|
||||||
|
Exprs: []expr.Any{
|
||||||
|
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: ifname(m.wgIface.Name()),
|
||||||
|
},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
UserData: []byte(allowNetbirdInputRuleID),
|
||||||
|
}
|
||||||
|
_ = m.rConn.InsertRule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) detectAllowNetbirdRule(existedRules []*nftables.Rule) *nftables.Rule {
|
||||||
|
ifName := ifname(m.wgIface.Name())
|
||||||
|
for _, rule := range existedRules {
|
||||||
|
if rule.Table.Name == "filter" && rule.Chain.Name == "INPUT" {
|
||||||
|
if len(rule.Exprs) < 4 {
|
||||||
|
if e, ok := rule.Exprs[0].(*expr.Meta); !ok || e.Key != expr.MetaKeyIIFNAME {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
break
|
if e, ok := rule.Exprs[1].(*expr.Cmp); !ok || e.Op != expr.CmpOpEq || !bytes.Equal(e.Data, ifName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return rule
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) refreshRuleHandles(table *nftables.Table, chain *nftables.Chain) error {
|
|
||||||
if table == nil || chain == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := m.rConn.GetRules(table, chain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rule := range list {
|
|
||||||
if len(rule.UserData) != 0 {
|
|
||||||
if err := m.rulesetManager.setNftRuleHandle(rule); err != nil {
|
|
||||||
log.Errorf("failed to set rule handle: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodePort(port fw.Port) []byte {
|
|
||||||
bs := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(bs, uint16(port.Values[0]))
|
|
||||||
return bs
|
|
||||||
}
|
|
||||||
|
|
||||||
func ifname(n string) []byte {
|
|
||||||
b := make([]byte, 16)
|
|
||||||
copy(b, []byte(n+"\x00"))
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -12,7 +13,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ func (i *iFaceMock) Address() iface.WGAddress {
|
|||||||
panic("AddressFunc is not set")
|
panic("AddressFunc is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *iFaceMock) IsUserspaceBind() bool { return false }
|
||||||
|
|
||||||
func TestNftablesManager(t *testing.T) {
|
func TestNftablesManager(t *testing.T) {
|
||||||
mock := &iFaceMock{
|
mock := &iFaceMock{
|
||||||
NameFunc: func() string {
|
NameFunc: func() string {
|
||||||
@@ -53,7 +56,7 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock)
|
manager, err := Create(context.Background(), mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
time.Sleep(time.Second * 3)
|
time.Sleep(time.Second * 3)
|
||||||
|
|
||||||
@@ -82,14 +85,10 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
err = manager.Flush()
|
err = manager.Flush()
|
||||||
require.NoError(t, err, "failed to flush")
|
require.NoError(t, err, "failed to flush")
|
||||||
|
|
||||||
rules, err := testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4)
|
rules, err := testClient.GetRules(manager.aclManager.workTable, manager.aclManager.chainInputRules)
|
||||||
require.NoError(t, err, "failed to get rules")
|
require.NoError(t, err, "failed to get rules")
|
||||||
|
|
||||||
// test expectations:
|
require.Len(t, rules, 1, "expected 1 rules")
|
||||||
// 1) regular rule
|
|
||||||
// 2) "accept extra routed traffic rule" for the interface
|
|
||||||
// 3) "drop all rule" for the interface
|
|
||||||
require.Len(t, rules, 3, "expected 3 rules")
|
|
||||||
|
|
||||||
ipToAdd, _ := netip.AddrFromSlice(ip)
|
ipToAdd, _ := netip.AddrFromSlice(ip)
|
||||||
add := ipToAdd.Unmap()
|
add := ipToAdd.Unmap()
|
||||||
@@ -137,18 +136,17 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.ElementsMatch(t, rules[0].Exprs, expectedExprs, "expected the same expressions")
|
require.ElementsMatch(t, rules[0].Exprs, expectedExprs, "expected the same expressions")
|
||||||
|
|
||||||
err = manager.DeleteRule(rule)
|
for _, r := range rule {
|
||||||
|
err = manager.DeleteRule(r)
|
||||||
require.NoError(t, err, "failed to delete rule")
|
require.NoError(t, err, "failed to delete rule")
|
||||||
|
}
|
||||||
|
|
||||||
err = manager.Flush()
|
err = manager.Flush()
|
||||||
require.NoError(t, err, "failed to flush")
|
require.NoError(t, err, "failed to flush")
|
||||||
|
|
||||||
rules, err = testClient.GetRules(manager.tableIPv4, manager.filterInputChainIPv4)
|
rules, err = testClient.GetRules(manager.aclManager.workTable, manager.aclManager.chainInputRules)
|
||||||
require.NoError(t, err, "failed to get rules")
|
require.NoError(t, err, "failed to get rules")
|
||||||
// test expectations:
|
require.Len(t, rules, 0, "expected 0 rules after deletion")
|
||||||
// 1) "accept extra routed traffic rule" for the interface
|
|
||||||
// 2) "drop all rule" for the interface
|
|
||||||
require.Len(t, rules, 2, "expected 2 rules after deleteion")
|
|
||||||
|
|
||||||
err = manager.Reset()
|
err = manager.Reset()
|
||||||
require.NoError(t, err, "failed to reset")
|
require.NoError(t, err, "failed to reset")
|
||||||
@@ -173,7 +171,7 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
|||||||
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
for _, testMax := range []int{10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000} {
|
||||||
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
t.Run(fmt.Sprintf("Testing %d rules", testMax), func(t *testing.T) {
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
manager, err := Create(mock)
|
manager, err := Create(context.Background(), mock)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
time.Sleep(time.Second * 3)
|
time.Sleep(time.Second * 3)
|
||||||
|
|
||||||
|
|||||||
431
client/firewall/nftables/route_linux.go
Normal file
431
client/firewall/nftables/route_linux.go
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
package nftables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/google/nftables"
|
||||||
|
"github.com/google/nftables/binaryutil"
|
||||||
|
"github.com/google/nftables/expr"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
chainNameRouteingFw = "netbird-rt-fwd"
|
||||||
|
chainNameRoutingNat = "netbird-rt-nat"
|
||||||
|
|
||||||
|
userDataAcceptForwardRuleSrc = "frwacceptsrc"
|
||||||
|
userDataAcceptForwardRuleDst = "frwacceptdst"
|
||||||
|
|
||||||
|
loopbackInterface = "lo\x00"
|
||||||
|
)
|
||||||
|
|
||||||
|
// some presets for building nftable rules
|
||||||
|
var (
|
||||||
|
zeroXor = binaryutil.NativeEndian.PutUint32(0)
|
||||||
|
|
||||||
|
exprCounterAccept = []expr.Any{
|
||||||
|
&expr.Counter{},
|
||||||
|
&expr.Verdict{
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
errFilterTableNotFound = fmt.Errorf("nftables: 'filter' table not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type router struct {
|
||||||
|
ctx context.Context
|
||||||
|
stop context.CancelFunc
|
||||||
|
conn *nftables.Conn
|
||||||
|
workTable *nftables.Table
|
||||||
|
filterTable *nftables.Table
|
||||||
|
chains map[string]*nftables.Chain
|
||||||
|
// rules is useful to avoid duplicates and to get missing attributes that we don't have when adding new rules
|
||||||
|
rules map[string]*nftables.Rule
|
||||||
|
isDefaultFwdRulesEnabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRouter(parentCtx context.Context, workTable *nftables.Table) (*router, error) {
|
||||||
|
ctx, cancel := context.WithCancel(parentCtx)
|
||||||
|
|
||||||
|
r := &router{
|
||||||
|
ctx: ctx,
|
||||||
|
stop: cancel,
|
||||||
|
conn: &nftables.Conn{},
|
||||||
|
workTable: workTable,
|
||||||
|
chains: make(map[string]*nftables.Chain),
|
||||||
|
rules: make(map[string]*nftables.Rule),
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
r.filterTable, err = r.loadFilterTable()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errFilterTableNotFound) {
|
||||||
|
log.Warnf("table 'filter' not found for forward rules")
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.cleanUpDefaultForwardRules()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.createContainers()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to create containers for route: %s", err)
|
||||||
|
}
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) RouteingFwChainName() string {
|
||||||
|
return chainNameRouteingFw
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetForwardRules cleans existing nftables default forward rules from the system
|
||||||
|
func (r *router) ResetForwardRules() {
|
||||||
|
err := r.cleanUpDefaultForwardRules()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to reset forward rules: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) loadFilterTable() (*nftables.Table, error) {
|
||||||
|
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range tables {
|
||||||
|
if table.Name == "filter" {
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errFilterTableNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) createContainers() error {
|
||||||
|
|
||||||
|
r.chains[chainNameRouteingFw] = r.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: chainNameRouteingFw,
|
||||||
|
Table: r.workTable,
|
||||||
|
})
|
||||||
|
|
||||||
|
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: chainNameRoutingNat,
|
||||||
|
Table: r.workTable,
|
||||||
|
Hooknum: nftables.ChainHookPostrouting,
|
||||||
|
Priority: nftables.ChainPriorityNATSource - 1,
|
||||||
|
Type: nftables.ChainTypeNAT,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add RETURN rule for loopback interface
|
||||||
|
loRule := &nftables.Rule{
|
||||||
|
Table: r.workTable,
|
||||||
|
Chain: r.chains[chainNameRoutingNat],
|
||||||
|
Exprs: []expr.Any{
|
||||||
|
&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: []byte(loopbackInterface),
|
||||||
|
},
|
||||||
|
&expr.Verdict{Kind: expr.VerdictReturn},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
r.conn.InsertRule(loRule)
|
||||||
|
|
||||||
|
err := r.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to initialize table: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRoutingRules appends a nftable rule pair to the forwarding chain and if enabled, to the nat chain
|
||||||
|
func (r *router) AddRoutingRules(pair manager.RouterPair) error {
|
||||||
|
err := r.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.addRoutingRule(manager.ForwardingFormat, chainNameRouteingFw, pair, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.addRoutingRule(manager.InForwardingFormat, chainNameRouteingFw, manager.GetInPair(pair), false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pair.Masquerade {
|
||||||
|
err = r.addRoutingRule(manager.NatFormat, chainNameRoutingNat, pair, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = r.addRoutingRule(manager.InNatFormat, chainNameRoutingNat, manager.GetInPair(pair), true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.filterTable != nil && !r.isDefaultFwdRulesEnabled {
|
||||||
|
log.Debugf("add default accept forward rule")
|
||||||
|
r.acceptForwardRule(pair.Source)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.conn.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("nftables: unable to insert rules for %s: %v", pair.Destination, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addRoutingRule inserts a nftable rule to the conn client flush queue
|
||||||
|
func (r *router) addRoutingRule(format, chainName string, pair manager.RouterPair, isNat bool) error {
|
||||||
|
sourceExp := generateCIDRMatcherExpressions(true, pair.Source)
|
||||||
|
destExp := generateCIDRMatcherExpressions(false, pair.Destination)
|
||||||
|
|
||||||
|
var expression []expr.Any
|
||||||
|
if isNat {
|
||||||
|
expression = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) // nolint:gocritic
|
||||||
|
} else {
|
||||||
|
expression = append(sourceExp, append(destExp, exprCounterAccept...)...) // nolint:gocritic
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleKey := manager.GenKey(format, pair.ID)
|
||||||
|
|
||||||
|
_, exists := r.rules[ruleKey]
|
||||||
|
if exists {
|
||||||
|
err := r.removeRoutingRule(format, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rules[ruleKey] = r.conn.AddRule(&nftables.Rule{
|
||||||
|
Table: r.workTable,
|
||||||
|
Chain: r.chains[chainName],
|
||||||
|
Exprs: expression,
|
||||||
|
UserData: []byte(ruleKey),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||||
|
src := generateCIDRMatcherExpressions(true, sourceNetwork)
|
||||||
|
dst := generateCIDRMatcherExpressions(false, "0.0.0.0/0")
|
||||||
|
|
||||||
|
var exprs []expr.Any
|
||||||
|
exprs = append(src, append(dst, &expr.Verdict{ // nolint:gocritic
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
})...)
|
||||||
|
|
||||||
|
rule := &nftables.Rule{
|
||||||
|
Table: r.filterTable,
|
||||||
|
Chain: &nftables.Chain{
|
||||||
|
Name: "FORWARD",
|
||||||
|
Table: r.filterTable,
|
||||||
|
Type: nftables.ChainTypeFilter,
|
||||||
|
Hooknum: nftables.ChainHookForward,
|
||||||
|
Priority: nftables.ChainPriorityFilter,
|
||||||
|
},
|
||||||
|
Exprs: exprs,
|
||||||
|
UserData: []byte(userDataAcceptForwardRuleSrc),
|
||||||
|
}
|
||||||
|
|
||||||
|
r.conn.AddRule(rule)
|
||||||
|
|
||||||
|
src = generateCIDRMatcherExpressions(true, "0.0.0.0/0")
|
||||||
|
dst = generateCIDRMatcherExpressions(false, sourceNetwork)
|
||||||
|
|
||||||
|
exprs = append(src, append(dst, &expr.Verdict{ //nolint:gocritic
|
||||||
|
Kind: expr.VerdictAccept,
|
||||||
|
})...)
|
||||||
|
|
||||||
|
rule = &nftables.Rule{
|
||||||
|
Table: r.filterTable,
|
||||||
|
Chain: &nftables.Chain{
|
||||||
|
Name: "FORWARD",
|
||||||
|
Table: r.filterTable,
|
||||||
|
Type: nftables.ChainTypeFilter,
|
||||||
|
Hooknum: nftables.ChainHookForward,
|
||||||
|
Priority: nftables.ChainPriorityFilter,
|
||||||
|
},
|
||||||
|
Exprs: exprs,
|
||||||
|
UserData: []byte(userDataAcceptForwardRuleDst),
|
||||||
|
}
|
||||||
|
r.conn.AddRule(rule)
|
||||||
|
r.isDefaultFwdRulesEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes a nftable rule pair from forwarding and nat chains
|
||||||
|
func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
|
||||||
|
err := r.refreshRulesMap()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.removeRoutingRule(manager.ForwardingFormat, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.removeRoutingRule(manager.InForwardingFormat, manager.GetInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.removeRoutingRule(manager.NatFormat, pair)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.removeRoutingRule(manager.InNatFormat, manager.GetInPair(pair))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.rules) == 0 {
|
||||||
|
err := r.cleanUpDefaultForwardRules()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = r.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 (r *router) removeRoutingRule(format string, pair manager.RouterPair) error {
|
||||||
|
ruleKey := manager.GenKey(format, pair.ID)
|
||||||
|
|
||||||
|
rule, found := r.rules[ruleKey]
|
||||||
|
if found {
|
||||||
|
ruleType := "forwarding"
|
||||||
|
if rule.Chain.Type == nftables.ChainTypeNAT {
|
||||||
|
ruleType = "nat"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.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(r.rules, ruleKey)
|
||||||
|
}
|
||||||
|
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 (r *router) refreshRulesMap() error {
|
||||||
|
for _, chain := range r.chains {
|
||||||
|
rules, err := r.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 {
|
||||||
|
r.rules[string(rule.UserData)] = rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *router) cleanUpDefaultForwardRules() error {
|
||||||
|
if r.filterTable == nil {
|
||||||
|
r.isDefaultFwdRulesEnabled = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chains, err := r.conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []*nftables.Rule
|
||||||
|
for _, chain := range chains {
|
||||||
|
if chain.Table.Name != r.filterTable.Name {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if chain.Name != "FORWARD" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rules, err = r.conn.GetRules(r.filterTable, chain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rule := range rules {
|
||||||
|
if bytes.Equal(rule.UserData, []byte(userDataAcceptForwardRuleSrc)) || bytes.Equal(rule.UserData, []byte(userDataAcceptForwardRuleDst)) {
|
||||||
|
err := r.conn.DelRule(rule)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r.isDefaultFwdRulesEnabled = false
|
||||||
|
return r.conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCIDRMatcherExpressions generates nftables expressions that matches a CIDR
|
||||||
|
func generateCIDRMatcherExpressions(source bool, cidr string) []expr.Any {
|
||||||
|
ip, network, _ := net.ParseCIDR(cidr)
|
||||||
|
ipToAdd, _ := netip.AddrFromSlice(ip)
|
||||||
|
add := ipToAdd.Unmap()
|
||||||
|
|
||||||
|
var offSet uint32
|
||||||
|
if source {
|
||||||
|
offSet = 12 // src offset
|
||||||
|
} else {
|
||||||
|
offSet = 16 // dst offset
|
||||||
|
}
|
||||||
|
|
||||||
|
return []expr.Any{
|
||||||
|
// fetch src add
|
||||||
|
&expr.Payload{
|
||||||
|
DestRegister: 1,
|
||||||
|
Base: expr.PayloadBaseNetworkHeader,
|
||||||
|
Offset: offSet,
|
||||||
|
Len: 4,
|
||||||
|
},
|
||||||
|
// net mask
|
||||||
|
&expr.Bitwise{
|
||||||
|
DestRegister: 1,
|
||||||
|
SourceRegister: 1,
|
||||||
|
Len: 4,
|
||||||
|
Mask: network.Mask,
|
||||||
|
Xor: zeroXor,
|
||||||
|
},
|
||||||
|
// net address
|
||||||
|
&expr.Cmp{
|
||||||
|
Register: 1,
|
||||||
|
Data: add.AsSlice(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
280
client/firewall/nftables/router_linux_test.go
Normal file
280
client/firewall/nftables/router_linux_test.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package nftables
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/coreos/go-iptables/iptables"
|
||||||
|
"github.com/google/nftables"
|
||||||
|
"github.com/google/nftables/expr"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/test"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UNKNOWN is the default value for the firewall type for unknown firewall type
|
||||||
|
UNKNOWN = iota
|
||||||
|
// IPTABLES is the value for the iptables firewall type
|
||||||
|
IPTABLES
|
||||||
|
// NFTABLES is the value for the nftables firewall type
|
||||||
|
NFTABLES
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNftablesManager_InsertRoutingRules(t *testing.T) {
|
||||||
|
if check() != NFTABLES {
|
||||||
|
t.Skip("nftables not supported on this OS")
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := createWorkTable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer deleteWorkTable()
|
||||||
|
|
||||||
|
for _, testCase := range test.InsertRuleTestCases {
|
||||||
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
|
manager, err := newRouter(context.TODO(), table)
|
||||||
|
require.NoError(t, err, "failed to create router")
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.ResetForwardRules()
|
||||||
|
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
err = manager.AddRoutingRules(testCase.InputPair)
|
||||||
|
defer func() {
|
||||||
|
_ = manager.RemoveRoutingRules(testCase.InputPair)
|
||||||
|
}()
|
||||||
|
require.NoError(t, err, "forwarding pair should be inserted")
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
||||||
|
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
||||||
|
testingExpression := append(sourceExp, destExp...) //nolint:gocritic
|
||||||
|
fwdRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||||
|
|
||||||
|
found := 0
|
||||||
|
for _, chain := range manager.chains {
|
||||||
|
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 := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||||
|
found := 0
|
||||||
|
for _, chain := range manager.chains {
|
||||||
|
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(true, firewall.GetInPair(testCase.InputPair).Source)
|
||||||
|
destExp = generateCIDRMatcherExpressions(false, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
testingExpression = append(sourceExp, destExp...) //nolint:gocritic
|
||||||
|
inFwdRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||||
|
|
||||||
|
found = 0
|
||||||
|
for _, chain := range manager.chains {
|
||||||
|
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 := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||||
|
found := 0
|
||||||
|
for _, chain := range manager.chains {
|
||||||
|
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) {
|
||||||
|
if check() != NFTABLES {
|
||||||
|
t.Skip("nftables not supported on this OS")
|
||||||
|
}
|
||||||
|
|
||||||
|
table, err := createWorkTable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer deleteWorkTable()
|
||||||
|
|
||||||
|
for _, testCase := range test.RemoveRuleTestCases {
|
||||||
|
t.Run(testCase.Name, func(t *testing.T) {
|
||||||
|
manager, err := newRouter(context.TODO(), table)
|
||||||
|
require.NoError(t, err, "failed to create router")
|
||||||
|
|
||||||
|
nftablesTestingClient := &nftables.Conn{}
|
||||||
|
|
||||||
|
defer manager.ResetForwardRules()
|
||||||
|
|
||||||
|
sourceExp := generateCIDRMatcherExpressions(true, testCase.InputPair.Source)
|
||||||
|
destExp := generateCIDRMatcherExpressions(false, testCase.InputPair.Destination)
|
||||||
|
|
||||||
|
forwardExp := append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic
|
||||||
|
forwardRuleKey := firewall.GenKey(firewall.ForwardingFormat, testCase.InputPair.ID)
|
||||||
|
insertedForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.workTable,
|
||||||
|
Chain: manager.chains[chainNameRouteingFw],
|
||||||
|
Exprs: forwardExp,
|
||||||
|
UserData: []byte(forwardRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
natExp := append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic
|
||||||
|
natRuleKey := firewall.GenKey(firewall.NatFormat, testCase.InputPair.ID)
|
||||||
|
|
||||||
|
insertedNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.workTable,
|
||||||
|
Chain: manager.chains[chainNameRoutingNat],
|
||||||
|
Exprs: natExp,
|
||||||
|
UserData: []byte(natRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
sourceExp = generateCIDRMatcherExpressions(true, firewall.GetInPair(testCase.InputPair).Source)
|
||||||
|
destExp = generateCIDRMatcherExpressions(false, firewall.GetInPair(testCase.InputPair).Destination)
|
||||||
|
|
||||||
|
forwardExp = append(sourceExp, append(destExp, exprCounterAccept...)...) //nolint:gocritic
|
||||||
|
inForwardRuleKey := firewall.GenKey(firewall.InForwardingFormat, testCase.InputPair.ID)
|
||||||
|
insertedInForwarding := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.workTable,
|
||||||
|
Chain: manager.chains[chainNameRouteingFw],
|
||||||
|
Exprs: forwardExp,
|
||||||
|
UserData: []byte(inForwardRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
natExp = append(sourceExp, append(destExp, &expr.Counter{}, &expr.Masq{})...) //nolint:gocritic
|
||||||
|
inNatRuleKey := firewall.GenKey(firewall.InNatFormat, testCase.InputPair.ID)
|
||||||
|
|
||||||
|
insertedInNat := nftablesTestingClient.InsertRule(&nftables.Rule{
|
||||||
|
Table: manager.workTable,
|
||||||
|
Chain: manager.chains[chainNameRoutingNat],
|
||||||
|
Exprs: natExp,
|
||||||
|
UserData: []byte(inNatRuleKey),
|
||||||
|
})
|
||||||
|
|
||||||
|
err = nftablesTestingClient.Flush()
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
manager.ResetForwardRules()
|
||||||
|
|
||||||
|
err = manager.RemoveRoutingRules(testCase.InputPair)
|
||||||
|
require.NoError(t, err, "shouldn't return error")
|
||||||
|
|
||||||
|
for _, chain := range manager.chains {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check returns the firewall type based on common lib checks. It returns UNKNOWN if no firewall is found.
|
||||||
|
func check() int {
|
||||||
|
nf := nftables.Conn{}
|
||||||
|
if _, err := nf.ListChains(); err == nil {
|
||||||
|
return NFTABLES
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
|
if err != nil {
|
||||||
|
return UNKNOWN
|
||||||
|
}
|
||||||
|
if isIptablesClientAvailable(ip) {
|
||||||
|
return IPTABLES
|
||||||
|
}
|
||||||
|
|
||||||
|
return UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIptablesClientAvailable(client *iptables.IPTables) bool {
|
||||||
|
_, err := client.ListChains("filter")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createWorkTable() (*nftables.Table, error) {
|
||||||
|
sConn, err := nftables.New(nftables.AsLasting())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tables, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range tables {
|
||||||
|
if t.Name == tableName {
|
||||||
|
sConn.DelTable(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table := sConn.AddTable(&nftables.Table{Name: tableName, Family: nftables.TableFamilyIPv4})
|
||||||
|
err = sConn.Flush()
|
||||||
|
|
||||||
|
return table, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteWorkTable() {
|
||||||
|
sConn, err := nftables.New(nftables.AsLasting())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tables, err := sConn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range tables {
|
||||||
|
if t.Name == tableName {
|
||||||
|
sConn.DelTable(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package nftables
|
package nftables
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -8,9 +10,8 @@ import (
|
|||||||
type Rule struct {
|
type Rule struct {
|
||||||
nftRule *nftables.Rule
|
nftRule *nftables.Rule
|
||||||
nftSet *nftables.Set
|
nftSet *nftables.Set
|
||||||
|
|
||||||
ruleID string
|
ruleID string
|
||||||
ip []byte
|
ip net.IP
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRuleID returns the rule id
|
// GetRuleID returns the rule id
|
||||||
|
|||||||
@@ -1,115 +0,0 @@
|
|||||||
package nftables
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/google/nftables"
|
|
||||||
"github.com/rs/xid"
|
|
||||||
)
|
|
||||||
|
|
||||||
// nftRuleset links native firewall rule and ipset to ACL generated rules
|
|
||||||
type nftRuleset struct {
|
|
||||||
nftRule *nftables.Rule
|
|
||||||
nftSet *nftables.Set
|
|
||||||
issuedRules map[string]*Rule
|
|
||||||
rulesetID string
|
|
||||||
}
|
|
||||||
|
|
||||||
type rulesetManager struct {
|
|
||||||
rulesets map[string]*nftRuleset
|
|
||||||
|
|
||||||
nftSetName2rulesetID map[string]string
|
|
||||||
issuedRuleID2rulesetID map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRuleManager() *rulesetManager {
|
|
||||||
return &rulesetManager{
|
|
||||||
rulesets: map[string]*nftRuleset{},
|
|
||||||
|
|
||||||
nftSetName2rulesetID: map[string]string{},
|
|
||||||
issuedRuleID2rulesetID: map[string]string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetManager) getRuleset(rulesetID string) (*nftRuleset, bool) {
|
|
||||||
ruleset, ok := r.rulesets[rulesetID]
|
|
||||||
return ruleset, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetManager) createRuleset(
|
|
||||||
rulesetID string,
|
|
||||||
nftRule *nftables.Rule,
|
|
||||||
nftSet *nftables.Set,
|
|
||||||
) *nftRuleset {
|
|
||||||
ruleset := nftRuleset{
|
|
||||||
rulesetID: rulesetID,
|
|
||||||
nftRule: nftRule,
|
|
||||||
nftSet: nftSet,
|
|
||||||
issuedRules: map[string]*Rule{},
|
|
||||||
}
|
|
||||||
r.rulesets[ruleset.rulesetID] = &ruleset
|
|
||||||
if nftSet != nil {
|
|
||||||
r.nftSetName2rulesetID[nftSet.Name] = ruleset.rulesetID
|
|
||||||
}
|
|
||||||
return &ruleset
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *rulesetManager) addRule(
|
|
||||||
ruleset *nftRuleset,
|
|
||||||
ip []byte,
|
|
||||||
) (*Rule, error) {
|
|
||||||
if _, ok := r.rulesets[ruleset.rulesetID]; !ok {
|
|
||||||
return nil, fmt.Errorf("ruleset not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
rule := Rule{
|
|
||||||
nftRule: ruleset.nftRule,
|
|
||||||
nftSet: ruleset.nftSet,
|
|
||||||
ruleID: xid.New().String(),
|
|
||||||
ip: ip,
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleset.issuedRules[rule.ruleID] = &rule
|
|
||||||
r.issuedRuleID2rulesetID[rule.ruleID] = ruleset.rulesetID
|
|
||||||
|
|
||||||
return &rule, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// deleteRule from ruleset and returns true if contains other rules
|
|
||||||
func (r *rulesetManager) deleteRule(rule *Rule) bool {
|
|
||||||
rulesetID, ok := r.issuedRuleID2rulesetID[rule.ruleID]
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ruleset := r.rulesets[rulesetID]
|
|
||||||
if ruleset.nftRule == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
delete(r.issuedRuleID2rulesetID, rule.ruleID)
|
|
||||||
delete(ruleset.issuedRules, rule.ruleID)
|
|
||||||
|
|
||||||
if len(ruleset.issuedRules) == 0 {
|
|
||||||
delete(r.rulesets, ruleset.rulesetID)
|
|
||||||
if rule.nftSet != nil {
|
|
||||||
delete(r.nftSetName2rulesetID, rule.nftSet.Name)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// setNftRuleHandle finds rule by userdata which contains rulesetID and updates it's handle number
|
|
||||||
//
|
|
||||||
// This is important to do, because after we add rule to the nftables we can't update it until
|
|
||||||
// we set correct handle value to it.
|
|
||||||
func (r *rulesetManager) setNftRuleHandle(nftRule *nftables.Rule) error {
|
|
||||||
split := bytes.Split(nftRule.UserData, []byte(" "))
|
|
||||||
ruleset, ok := r.rulesets[string(split[0])]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("ruleset not found")
|
|
||||||
}
|
|
||||||
*ruleset.nftRule = *nftRule
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package nftables
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/nftables"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRulesetManager_createRuleset(t *testing.T) {
|
|
||||||
// Create a ruleset manager.
|
|
||||||
rulesetManager := newRuleManager()
|
|
||||||
|
|
||||||
// Create a ruleset.
|
|
||||||
rulesetID := "ruleset-1"
|
|
||||||
nftRule := nftables.Rule{
|
|
||||||
UserData: []byte(rulesetID),
|
|
||||||
}
|
|
||||||
ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil)
|
|
||||||
require.NotNil(t, ruleset, "createRuleset() failed")
|
|
||||||
require.Equal(t, ruleset.rulesetID, rulesetID, "rulesetID is incorrect")
|
|
||||||
require.Equal(t, ruleset.nftRule, &nftRule, "nftRule is incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRulesetManager_addRule(t *testing.T) {
|
|
||||||
// Create a ruleset manager.
|
|
||||||
rulesetManager := newRuleManager()
|
|
||||||
|
|
||||||
// Create a ruleset.
|
|
||||||
rulesetID := "ruleset-1"
|
|
||||||
nftRule := nftables.Rule{}
|
|
||||||
ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil)
|
|
||||||
|
|
||||||
// Add a rule to the ruleset.
|
|
||||||
ip := []byte("192.168.1.1")
|
|
||||||
rule, err := rulesetManager.addRule(ruleset, ip)
|
|
||||||
require.NoError(t, err, "addRule() failed")
|
|
||||||
require.NotNil(t, rule, "rule should not be nil")
|
|
||||||
require.NotEqual(t, rule.ruleID, "ruleID is empty")
|
|
||||||
require.EqualValues(t, rule.ip, ip, "ip is incorrect")
|
|
||||||
require.Contains(t, ruleset.issuedRules, rule.ruleID, "ruleID already exists in ruleset")
|
|
||||||
require.Contains(t, rulesetManager.issuedRuleID2rulesetID, rule.ruleID, "ruleID already exists in ruleset manager")
|
|
||||||
|
|
||||||
ruleset2 := &nftRuleset{
|
|
||||||
rulesetID: "ruleset-2",
|
|
||||||
}
|
|
||||||
_, err = rulesetManager.addRule(ruleset2, ip)
|
|
||||||
require.Error(t, err, "addRule() should have failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRulesetManager_deleteRule(t *testing.T) {
|
|
||||||
// Create a ruleset manager.
|
|
||||||
rulesetManager := newRuleManager()
|
|
||||||
|
|
||||||
// Create a ruleset.
|
|
||||||
rulesetID := "ruleset-1"
|
|
||||||
nftRule := nftables.Rule{}
|
|
||||||
ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil)
|
|
||||||
|
|
||||||
// Add a rule to the ruleset.
|
|
||||||
ip := []byte("192.168.1.1")
|
|
||||||
rule, err := rulesetManager.addRule(ruleset, ip)
|
|
||||||
require.NoError(t, err, "addRule() failed")
|
|
||||||
require.NotNil(t, rule, "rule should not be nil")
|
|
||||||
|
|
||||||
ip2 := []byte("192.168.1.1")
|
|
||||||
rule2, err := rulesetManager.addRule(ruleset, ip2)
|
|
||||||
require.NoError(t, err, "addRule() failed")
|
|
||||||
require.NotNil(t, rule2, "rule should not be nil")
|
|
||||||
|
|
||||||
hasNext := rulesetManager.deleteRule(rule)
|
|
||||||
require.True(t, hasNext, "deleteRule() should have returned true")
|
|
||||||
|
|
||||||
// Check that the rule is no longer in the manager.
|
|
||||||
require.NotContains(t, rulesetManager.issuedRuleID2rulesetID, rule.ruleID, "rule should have been deleted")
|
|
||||||
|
|
||||||
hasNext = rulesetManager.deleteRule(rule2)
|
|
||||||
require.False(t, hasNext, "deleteRule() should have returned false")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRulesetManager_setNftRuleHandle(t *testing.T) {
|
|
||||||
// Create a ruleset manager.
|
|
||||||
rulesetManager := newRuleManager()
|
|
||||||
// Create a ruleset.
|
|
||||||
rulesetID := "ruleset-1"
|
|
||||||
nftRule := nftables.Rule{}
|
|
||||||
ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, nil)
|
|
||||||
// Add a rule to the ruleset.
|
|
||||||
ip := []byte("192.168.0.1")
|
|
||||||
|
|
||||||
rule, err := rulesetManager.addRule(ruleset, ip)
|
|
||||||
require.NoError(t, err, "addRule() failed")
|
|
||||||
require.NotNil(t, rule, "rule should not be nil")
|
|
||||||
|
|
||||||
nftRuleCopy := nftRule
|
|
||||||
nftRuleCopy.Handle = 2
|
|
||||||
nftRuleCopy.UserData = []byte(rulesetID)
|
|
||||||
err = rulesetManager.setNftRuleHandle(&nftRuleCopy)
|
|
||||||
require.NoError(t, err, "setNftRuleHandle() failed")
|
|
||||||
// check correct work with references
|
|
||||||
require.Equal(t, nftRule.Handle, uint64(2), "nftRule.Handle is incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRulesetManager_getRuleset(t *testing.T) {
|
|
||||||
// Create a ruleset manager.
|
|
||||||
rulesetManager := newRuleManager()
|
|
||||||
// Create a ruleset.
|
|
||||||
rulesetID := "ruleset-1"
|
|
||||||
nftRule := nftables.Rule{}
|
|
||||||
nftSet := nftables.Set{
|
|
||||||
ID: 2,
|
|
||||||
}
|
|
||||||
ruleset := rulesetManager.createRuleset(rulesetID, &nftRule, &nftSet)
|
|
||||||
require.NotNil(t, ruleset, "createRuleset() failed")
|
|
||||||
|
|
||||||
find, ok := rulesetManager.getRuleset(rulesetID)
|
|
||||||
require.True(t, ok, "getRuleset() failed")
|
|
||||||
require.Equal(t, ruleset, find, "getRulesetBySetID() failed")
|
|
||||||
|
|
||||||
_, ok = rulesetManager.getRuleset("does-not-exist")
|
|
||||||
require.False(t, ok, "getRuleset() failed")
|
|
||||||
}
|
|
||||||
47
client/firewall/test/cases_linux.go
Normal file
47
client/firewall/test/cases_linux.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package test
|
||||||
|
|
||||||
|
import firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
|
||||||
|
var (
|
||||||
|
InsertRuleTestCases = []struct {
|
||||||
|
Name string
|
||||||
|
InputPair firewall.RouterPair
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "Insert Forwarding IPV4 Rule",
|
||||||
|
InputPair: firewall.RouterPair{
|
||||||
|
ID: "zxa",
|
||||||
|
Source: "100.100.100.1/32",
|
||||||
|
Destination: "100.100.200.0/24",
|
||||||
|
Masquerade: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Insert Forwarding And Nat IPV4 Rules",
|
||||||
|
InputPair: firewall.RouterPair{
|
||||||
|
ID: "zxa",
|
||||||
|
Source: "100.100.100.1/32",
|
||||||
|
Destination: "100.100.200.0/24",
|
||||||
|
Masquerade: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoveRuleTestCases = []struct {
|
||||||
|
Name string
|
||||||
|
InputPair firewall.RouterPair
|
||||||
|
IpVersion string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "Remove Forwarding And Nat IPV4 Rules",
|
||||||
|
InputPair: firewall.RouterPair{
|
||||||
|
ID: "zxa",
|
||||||
|
Source: "100.100.100.1/32",
|
||||||
|
Destination: "100.100.200.0/24",
|
||||||
|
Masquerade: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
25
client/firewall/uspfilter/allow_netbird.go
Normal file
25
client/firewall/uspfilter/allow_netbird.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package uspfilter
|
||||||
|
|
||||||
|
// Reset firewall to the default state
|
||||||
|
func (m *Manager) Reset() error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
m.outgoingRules = make(map[string]RuleSet)
|
||||||
|
m.incomingRules = make(map[string]RuleSet)
|
||||||
|
|
||||||
|
if m.nativeFirewall != nil {
|
||||||
|
return m.nativeFirewall.Reset()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowNetbird allows netbird interface traffic
|
||||||
|
func (m *Manager) AllowNetbird() error {
|
||||||
|
if m.nativeFirewall != nil {
|
||||||
|
return m.nativeFirewall.AllowNetbird()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
112
client/firewall/uspfilter/allow_netbird_windows.go
Normal file
112
client/firewall/uspfilter/allow_netbird_windows.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package uspfilter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type action string
|
||||||
|
|
||||||
|
const (
|
||||||
|
addRule action = "add"
|
||||||
|
deleteRule action = "delete"
|
||||||
|
firewallRuleName = "Netbird"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reset firewall to the default state
|
||||||
|
func (m *Manager) Reset() error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
m.outgoingRules = make(map[string]RuleSet)
|
||||||
|
m.incomingRules = make(map[string]RuleSet)
|
||||||
|
|
||||||
|
if !isWindowsFirewallReachable() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isFirewallRuleActive(firewallRuleName) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := manageFirewallRule(firewallRuleName, deleteRule); err != nil {
|
||||||
|
return fmt.Errorf("couldn't remove windows firewall: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowNetbird allows netbird interface traffic
|
||||||
|
func (m *Manager) AllowNetbird() error {
|
||||||
|
if !isWindowsFirewallReachable() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isFirewallRuleActive(firewallRuleName) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return manageFirewallRule(firewallRuleName,
|
||||||
|
addRule,
|
||||||
|
"dir=in",
|
||||||
|
"enable=yes",
|
||||||
|
"action=allow",
|
||||||
|
"profile=any",
|
||||||
|
"localip="+m.wgIface.Address().IP.String(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func manageFirewallRule(ruleName string, action action, extraArgs ...string) error {
|
||||||
|
|
||||||
|
args := []string{"advfirewall", "firewall", string(action), "rule", "name=" + ruleName}
|
||||||
|
if action == addRule {
|
||||||
|
args = append(args, extraArgs...)
|
||||||
|
}
|
||||||
|
netshCmd := GetSystem32Command("netsh")
|
||||||
|
cmd := exec.Command(netshCmd, args...)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWindowsFirewallReachable() bool {
|
||||||
|
args := []string{"advfirewall", "show", "allprofiles", "state"}
|
||||||
|
|
||||||
|
netshCmd := GetSystem32Command("netsh")
|
||||||
|
|
||||||
|
cmd := exec.Command(netshCmd, args...)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
|
||||||
|
_, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("Windows firewall is not reachable, skipping default rule management. Using only user space rules. Error: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFirewallRuleActive(ruleName string) bool {
|
||||||
|
args := []string{"advfirewall", "firewall", "show", "rule", "name=" + ruleName}
|
||||||
|
|
||||||
|
netshCmd := GetSystem32Command("netsh")
|
||||||
|
|
||||||
|
cmd := exec.Command(netshCmd, args...)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||||
|
_, err := cmd.Output()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSystem32Command checks if a command can be found in the system path and returns it. In case it can't find it
|
||||||
|
// in the path it will return the full path of a command assuming C:\windows\system32 as the base path.
|
||||||
|
func GetSystem32Command(command string) string {
|
||||||
|
_, err := exec.LookPath(command)
|
||||||
|
if err == nil {
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Command %s not found in PATH, using C:\\windows\\system32\\%s.exe path", command, command)
|
||||||
|
|
||||||
|
return "C:\\windows\\system32\\" + command + ".exe"
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rule to handle management of rules
|
// Rule to handle management of rules
|
||||||
@@ -15,7 +15,7 @@ type Rule struct {
|
|||||||
ipLayer gopacket.LayerType
|
ipLayer gopacket.LayerType
|
||||||
matchByIP bool
|
matchByIP bool
|
||||||
protoLayer gopacket.LayerType
|
protoLayer gopacket.LayerType
|
||||||
direction fw.RuleDirection
|
direction firewall.RuleDirection
|
||||||
sPort uint16
|
sPort uint16
|
||||||
dPort uint16
|
dPort uint16
|
||||||
drop bool
|
drop bool
|
||||||
|
|||||||
@@ -10,15 +10,20 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
const layerTypeAll = 0
|
const layerTypeAll = 0
|
||||||
|
|
||||||
|
var (
|
||||||
|
errRouteNotSupported = fmt.Errorf("route not supported with userspace firewall")
|
||||||
|
)
|
||||||
|
|
||||||
// IFaceMapper defines subset methods of interface required for manager
|
// IFaceMapper defines subset methods of interface required for manager
|
||||||
type IFaceMapper interface {
|
type IFaceMapper interface {
|
||||||
SetFilter(iface.PacketFilter) error
|
SetFilter(iface.PacketFilter) error
|
||||||
|
Address() iface.WGAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuleSet is a set of rules grouped by a string key
|
// RuleSet is a set of rules grouped by a string key
|
||||||
@@ -30,6 +35,8 @@ type Manager struct {
|
|||||||
incomingRules map[string]RuleSet
|
incomingRules map[string]RuleSet
|
||||||
wgNetwork *net.IPNet
|
wgNetwork *net.IPNet
|
||||||
decoders sync.Pool
|
decoders sync.Pool
|
||||||
|
wgIface IFaceMapper
|
||||||
|
nativeFirewall firewall.Manager
|
||||||
|
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
}
|
}
|
||||||
@@ -49,6 +56,20 @@ type decoder struct {
|
|||||||
|
|
||||||
// Create userspace firewall manager constructor
|
// Create userspace firewall manager constructor
|
||||||
func Create(iface IFaceMapper) (*Manager, error) {
|
func Create(iface IFaceMapper) (*Manager, error) {
|
||||||
|
return create(iface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateWithNativeFirewall(iface IFaceMapper, nativeFirewall firewall.Manager) (*Manager, error) {
|
||||||
|
mgr, err := create(iface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr.nativeFirewall = nativeFirewall
|
||||||
|
return mgr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(iface IFaceMapper) (*Manager, error) {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
decoders: sync.Pool{
|
decoders: sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
@@ -65,6 +86,7 @@ func Create(iface IFaceMapper) (*Manager, error) {
|
|||||||
},
|
},
|
||||||
outgoingRules: make(map[string]RuleSet),
|
outgoingRules: make(map[string]RuleSet),
|
||||||
incomingRules: make(map[string]RuleSet),
|
incomingRules: make(map[string]RuleSet),
|
||||||
|
wgIface: iface,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := iface.SetFilter(m); err != nil {
|
if err := iface.SetFilter(m); err != nil {
|
||||||
@@ -73,27 +95,50 @@ func Create(iface IFaceMapper) (*Manager, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) IsServerRouteSupported() bool {
|
||||||
|
if m.nativeFirewall == nil {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) InsertRoutingRules(pair firewall.RouterPair) error {
|
||||||
|
if m.nativeFirewall == nil {
|
||||||
|
return errRouteNotSupported
|
||||||
|
}
|
||||||
|
return m.nativeFirewall.InsertRoutingRules(pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveRoutingRules removes a routing firewall rule
|
||||||
|
func (m *Manager) RemoveRoutingRules(pair firewall.RouterPair) error {
|
||||||
|
if m.nativeFirewall == nil {
|
||||||
|
return errRouteNotSupported
|
||||||
|
}
|
||||||
|
return m.nativeFirewall.RemoveRoutingRules(pair)
|
||||||
|
}
|
||||||
|
|
||||||
// AddFiltering rule to the firewall
|
// AddFiltering rule to the firewall
|
||||||
//
|
//
|
||||||
// If comment argument is empty firewall manager should set
|
// If comment argument is empty firewall manager should set
|
||||||
// rule ID as comment for the rule
|
// rule ID as comment for the rule
|
||||||
func (m *Manager) AddFiltering(
|
func (m *Manager) AddFiltering(
|
||||||
ip net.IP,
|
ip net.IP,
|
||||||
proto fw.Protocol,
|
proto firewall.Protocol,
|
||||||
sPort *fw.Port,
|
sPort *firewall.Port,
|
||||||
dPort *fw.Port,
|
dPort *firewall.Port,
|
||||||
direction fw.RuleDirection,
|
direction firewall.RuleDirection,
|
||||||
action fw.Action,
|
action firewall.Action,
|
||||||
ipsetName string,
|
ipsetName string,
|
||||||
comment string,
|
comment string,
|
||||||
) (fw.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
r := Rule{
|
r := Rule{
|
||||||
id: uuid.New().String(),
|
id: uuid.New().String(),
|
||||||
ip: ip,
|
ip: ip,
|
||||||
ipLayer: layers.LayerTypeIPv6,
|
ipLayer: layers.LayerTypeIPv6,
|
||||||
matchByIP: true,
|
matchByIP: true,
|
||||||
direction: direction,
|
direction: direction,
|
||||||
drop: action == fw.ActionDrop,
|
drop: action == firewall.ActionDrop,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
}
|
}
|
||||||
if ipNormalized := ip.To4(); ipNormalized != nil {
|
if ipNormalized := ip.To4(); ipNormalized != nil {
|
||||||
@@ -114,21 +159,21 @@ func (m *Manager) AddFiltering(
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch proto {
|
switch proto {
|
||||||
case fw.ProtocolTCP:
|
case firewall.ProtocolTCP:
|
||||||
r.protoLayer = layers.LayerTypeTCP
|
r.protoLayer = layers.LayerTypeTCP
|
||||||
case fw.ProtocolUDP:
|
case firewall.ProtocolUDP:
|
||||||
r.protoLayer = layers.LayerTypeUDP
|
r.protoLayer = layers.LayerTypeUDP
|
||||||
case fw.ProtocolICMP:
|
case firewall.ProtocolICMP:
|
||||||
r.protoLayer = layers.LayerTypeICMPv4
|
r.protoLayer = layers.LayerTypeICMPv4
|
||||||
if r.ipLayer == layers.LayerTypeIPv6 {
|
if r.ipLayer == layers.LayerTypeIPv6 {
|
||||||
r.protoLayer = layers.LayerTypeICMPv6
|
r.protoLayer = layers.LayerTypeICMPv6
|
||||||
}
|
}
|
||||||
case fw.ProtocolALL:
|
case firewall.ProtocolALL:
|
||||||
r.protoLayer = layerTypeAll
|
r.protoLayer = layerTypeAll
|
||||||
}
|
}
|
||||||
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
if direction == fw.RuleDirectionIN {
|
if direction == firewall.RuleDirectionIN {
|
||||||
if _, ok := m.incomingRules[r.ip.String()]; !ok {
|
if _, ok := m.incomingRules[r.ip.String()]; !ok {
|
||||||
m.incomingRules[r.ip.String()] = make(RuleSet)
|
m.incomingRules[r.ip.String()] = make(RuleSet)
|
||||||
}
|
}
|
||||||
@@ -140,12 +185,11 @@ func (m *Manager) AddFiltering(
|
|||||||
m.outgoingRules[r.ip.String()][r.id] = r
|
m.outgoingRules[r.ip.String()][r.id] = r
|
||||||
}
|
}
|
||||||
m.mutex.Unlock()
|
m.mutex.Unlock()
|
||||||
|
return []firewall.Rule{&r}, nil
|
||||||
return &r, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteRule from the firewall by rule definition
|
// DeleteRule from the firewall by rule definition
|
||||||
func (m *Manager) DeleteRule(rule fw.Rule) error {
|
func (m *Manager) DeleteRule(rule firewall.Rule) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
@@ -154,7 +198,7 @@ func (m *Manager) DeleteRule(rule fw.Rule) error {
|
|||||||
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
|
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.direction == fw.RuleDirectionIN {
|
if r.direction == firewall.RuleDirectionIN {
|
||||||
_, ok := m.incomingRules[r.ip.String()][r.id]
|
_, ok := m.incomingRules[r.ip.String()][r.id]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
|
return fmt.Errorf("delete rule: no rule with such id: %v", r.id)
|
||||||
@@ -171,17 +215,6 @@ func (m *Manager) DeleteRule(rule fw.Rule) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset firewall to the default state
|
|
||||||
func (m *Manager) Reset() error {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
m.outgoingRules = make(map[string]RuleSet)
|
|
||||||
m.incomingRules = make(map[string]RuleSet)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush doesn't need to be implemented for this manager
|
// Flush doesn't need to be implemented for this manager
|
||||||
func (m *Manager) Flush() error { return nil }
|
func (m *Manager) Flush() error { return nil }
|
||||||
|
|
||||||
@@ -195,7 +228,7 @@ func (m *Manager) DropIncoming(packetData []byte) bool {
|
|||||||
return m.dropFilter(packetData, m.incomingRules, true)
|
return m.dropFilter(packetData, m.incomingRules, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// dropFilter imlements same logic for booth direction of the traffic
|
// dropFilter implements same logic for booth direction of the traffic
|
||||||
func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet, isIncomingPacket bool) bool {
|
func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet, isIncomingPacket bool) bool {
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
defer m.mutex.RUnlock()
|
||||||
@@ -329,7 +362,7 @@ func (m *Manager) AddUDPPacketHook(
|
|||||||
protoLayer: layers.LayerTypeUDP,
|
protoLayer: layers.LayerTypeUDP,
|
||||||
dPort: dPort,
|
dPort: dPort,
|
||||||
ipLayer: layers.LayerTypeIPv6,
|
ipLayer: layers.LayerTypeIPv6,
|
||||||
direction: fw.RuleDirectionOUT,
|
direction: firewall.RuleDirectionOUT,
|
||||||
comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort),
|
comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort),
|
||||||
udpHook: hook,
|
udpHook: hook,
|
||||||
}
|
}
|
||||||
@@ -340,7 +373,7 @@ func (m *Manager) AddUDPPacketHook(
|
|||||||
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
if in {
|
if in {
|
||||||
r.direction = fw.RuleDirectionIN
|
r.direction = firewall.RuleDirectionIN
|
||||||
if _, ok := m.incomingRules[r.ip.String()]; !ok {
|
if _, ok := m.incomingRules[r.ip.String()]; !ok {
|
||||||
m.incomingRules[r.ip.String()] = make(map[string]Rule)
|
m.incomingRules[r.ip.String()] = make(map[string]Rule)
|
||||||
}
|
}
|
||||||
@@ -362,14 +395,16 @@ func (m *Manager) RemovePacketHook(hookID string) error {
|
|||||||
for _, arr := range m.incomingRules {
|
for _, arr := range m.incomingRules {
|
||||||
for _, r := range arr {
|
for _, r := range arr {
|
||||||
if r.id == hookID {
|
if r.id == hookID {
|
||||||
return m.DeleteRule(&r)
|
rule := r
|
||||||
|
return m.DeleteRule(&rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, arr := range m.outgoingRules {
|
for _, arr := range m.outgoingRules {
|
||||||
for _, r := range arr {
|
for _, r := range arr {
|
||||||
if r.id == hookID {
|
if r.id == hookID {
|
||||||
return m.DeleteRule(&r)
|
rule := r
|
||||||
|
return m.DeleteRule(&rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import (
|
|||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IFaceMock struct {
|
type IFaceMock struct {
|
||||||
SetFilterFunc func(iface.PacketFilter) error
|
SetFilterFunc func(iface.PacketFilter) error
|
||||||
|
AddressFunc func() iface.WGAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *IFaceMock) SetFilter(iface iface.PacketFilter) error {
|
func (i *IFaceMock) SetFilter(iface iface.PacketFilter) error {
|
||||||
@@ -25,6 +26,13 @@ func (i *IFaceMock) SetFilter(iface iface.PacketFilter) error {
|
|||||||
return i.SetFilterFunc(iface)
|
return i.SetFilterFunc(iface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *IFaceMock) Address() iface.WGAddress {
|
||||||
|
if i.AddressFunc == nil {
|
||||||
|
return iface.WGAddress{}
|
||||||
|
}
|
||||||
|
return i.AddressFunc()
|
||||||
|
}
|
||||||
|
|
||||||
func TestManagerCreate(t *testing.T) {
|
func TestManagerCreate(t *testing.T) {
|
||||||
ifaceMock := &IFaceMock{
|
ifaceMock := &IFaceMock{
|
||||||
SetFilterFunc: func(iface.PacketFilter) error { return nil },
|
SetFilterFunc: func(iface.PacketFilter) error { return nil },
|
||||||
@@ -117,26 +125,34 @@ func TestManagerDeleteRule(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.DeleteRule(rule)
|
for _, r := range rule {
|
||||||
|
err = m.DeleteRule(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to delete rule: %v", err)
|
t.Errorf("failed to delete rule: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := m.incomingRules[ip.String()][rule2.GetRuleID()]; !ok {
|
|
||||||
t.Errorf("rule2 is not in the incomingRules")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.DeleteRule(rule2)
|
for _, r := range rule2 {
|
||||||
|
if _, ok := m.incomingRules[ip.String()][r.GetRuleID()]; !ok {
|
||||||
|
t.Errorf("rule2 is not in the incomingRules")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rule2 {
|
||||||
|
err = m.DeleteRule(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to delete rule: %v", err)
|
t.Errorf("failed to delete rule: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, ok := m.incomingRules[ip.String()][rule2.GetRuleID()]; ok {
|
for _, r := range rule2 {
|
||||||
|
if _, ok := m.incomingRules[ip.String()][r.GetRuleID()]; ok {
|
||||||
t.Errorf("rule2 is not in the incomingRules")
|
t.Errorf("rule2 is not in the incomingRules")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAddUDPPacketHook(t *testing.T) {
|
func TestAddUDPPacketHook(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|||||||
@@ -166,10 +166,9 @@ WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
|
|||||||
EnVar::SetHKLM
|
EnVar::SetHKLM
|
||||||
EnVar::AddValueEx "path" "$INSTDIR"
|
EnVar::AddValueEx "path" "$INSTDIR"
|
||||||
|
|
||||||
SetShellVarContext current
|
SetShellVarContext all
|
||||||
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
CreateShortCut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${UI_APP_EXE}"
|
||||||
SetShellVarContext all
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
Section -Post
|
Section -Post
|
||||||
@@ -194,12 +193,12 @@ Sleep 3000
|
|||||||
Delete "$INSTDIR\${UI_APP_EXE}"
|
Delete "$INSTDIR\${UI_APP_EXE}"
|
||||||
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
Delete "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
Delete "$INSTDIR\wintun.dll"
|
Delete "$INSTDIR\wintun.dll"
|
||||||
|
Delete "$INSTDIR\opengl32.dll"
|
||||||
RmDir /r "$INSTDIR"
|
RmDir /r "$INSTDIR"
|
||||||
|
|
||||||
SetShellVarContext current
|
SetShellVarContext all
|
||||||
Delete "$DESKTOP\${APP_NAME}.lnk"
|
Delete "$DESKTOP\${APP_NAME}.lnk"
|
||||||
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
|
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
|
||||||
SetShellVarContext all
|
|
||||||
|
|
||||||
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
|
||||||
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
|
||||||
@@ -209,8 +208,7 @@ SectionEnd
|
|||||||
|
|
||||||
|
|
||||||
Function LaunchLink
|
Function LaunchLink
|
||||||
SetShellVarContext current
|
SetShellVarContext all
|
||||||
SetOutPath $INSTDIR
|
SetOutPath $INSTDIR
|
||||||
ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk"
|
ShellExecAsUser::ShellExecAsUser "" "$DESKTOP\${APP_NAME}.lnk"
|
||||||
SetShellVarContext all
|
|
||||||
FunctionEnd
|
FunctionEnd
|
||||||
|
|||||||
@@ -11,49 +11,34 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/ssh"
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/iface"
|
|
||||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IFaceMapper defines subset methods of interface required for manager
|
|
||||||
type IFaceMapper interface {
|
|
||||||
Name() string
|
|
||||||
Address() iface.WGAddress
|
|
||||||
IsUserspaceBind() bool
|
|
||||||
SetFilter(iface.PacketFilter) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manager is a ACL rules manager
|
// Manager is a ACL rules manager
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
ApplyFiltering(networkMap *mgmProto.NetworkMap)
|
ApplyFiltering(networkMap *mgmProto.NetworkMap)
|
||||||
Stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultManager uses firewall manager to handle
|
// DefaultManager uses firewall manager to handle
|
||||||
type DefaultManager struct {
|
type DefaultManager struct {
|
||||||
manager firewall.Manager
|
firewall firewall.Manager
|
||||||
ipsetCounter int
|
ipsetCounter int
|
||||||
rulesPairs map[string][]firewall.Rule
|
rulesPairs map[string][]firewall.Rule
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type ipsetInfo struct {
|
func NewDefaultManager(fm firewall.Manager) *DefaultManager {
|
||||||
name string
|
|
||||||
ipCount int
|
|
||||||
}
|
|
||||||
|
|
||||||
func newDefaultManager(fm firewall.Manager) *DefaultManager {
|
|
||||||
return &DefaultManager{
|
return &DefaultManager{
|
||||||
manager: fm,
|
firewall: fm,
|
||||||
rulesPairs: make(map[string][]firewall.Rule),
|
rulesPairs: make(map[string][]firewall.Rule),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
|
// ApplyFiltering firewall rules to the local firewall manager processed by ACL policy.
|
||||||
//
|
//
|
||||||
// If allowByDefault is ture it appends allow ALL traffic rules to input and output chains.
|
// If allowByDefault is true it appends allow ALL traffic rules to input and output chains.
|
||||||
func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
||||||
d.mutex.Lock()
|
d.mutex.Lock()
|
||||||
defer d.mutex.Unlock()
|
defer d.mutex.Unlock()
|
||||||
@@ -69,13 +54,13 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
|||||||
time.Since(start), total)
|
time.Since(start), total)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if d.manager == nil {
|
if d.firewall == nil {
|
||||||
log.Debug("firewall manager is not supported, skipping firewall rules")
|
log.Debug("firewall manager is not supported, skipping firewall rules")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := d.manager.Flush(); err != nil {
|
if err := d.firewall.Flush(); err != nil {
|
||||||
log.Error("failed to flush firewall rules: ", err)
|
log.Error("failed to flush firewall rules: ", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -125,58 +110,35 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
applyFailed := false
|
|
||||||
newRulePairs := make(map[string][]firewall.Rule)
|
newRulePairs := make(map[string][]firewall.Rule)
|
||||||
ipsetByRuleSelectors := make(map[string]*ipsetInfo)
|
ipsetByRuleSelectors := make(map[string]string)
|
||||||
|
|
||||||
// calculate which IP's can be grouped in by which ipset
|
|
||||||
// to do that we use rule selector (which is just rule properties without IP's)
|
|
||||||
for _, r := range rules {
|
|
||||||
selector := d.getRuleGroupingSelector(r)
|
|
||||||
ipset, ok := ipsetByRuleSelectors[selector]
|
|
||||||
if !ok {
|
|
||||||
ipset = &ipsetInfo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
ipset.ipCount++
|
|
||||||
ipsetByRuleSelectors[selector] = ipset
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range rules {
|
for _, r := range rules {
|
||||||
// if this rule is member of rule selection with more than DefaultIPsCountForSet
|
// if this rule is member of rule selection with more than DefaultIPsCountForSet
|
||||||
// it's IP address can be used in the ipset for firewall manager which supports it
|
// it's IP address can be used in the ipset for firewall manager which supports it
|
||||||
ipset := ipsetByRuleSelectors[d.getRuleGroupingSelector(r)]
|
selector := d.getRuleGroupingSelector(r)
|
||||||
ipsetName := ""
|
ipsetName, ok := ipsetByRuleSelectors[selector]
|
||||||
if ipset.name == "" {
|
if !ok {
|
||||||
d.ipsetCounter++
|
d.ipsetCounter++
|
||||||
ipset.name = fmt.Sprintf("nb%07d", d.ipsetCounter)
|
ipsetName = fmt.Sprintf("nb%07d", d.ipsetCounter)
|
||||||
|
ipsetByRuleSelectors[selector] = ipsetName
|
||||||
}
|
}
|
||||||
ipsetName = ipset.name
|
|
||||||
pairID, rulePair, err := d.protoRuleToFirewallRule(r, ipsetName)
|
pairID, rulePair, err := d.protoRuleToFirewallRule(r, ipsetName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to apply firewall rule: %+v, %v", r, err)
|
log.Errorf("failed to apply firewall rule: %+v, %v", r, err)
|
||||||
applyFailed = true
|
d.rollBack(newRulePairs)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
if len(rules) > 0 {
|
||||||
|
d.rulesPairs[pairID] = rulePair
|
||||||
newRulePairs[pairID] = rulePair
|
newRulePairs[pairID] = rulePair
|
||||||
}
|
}
|
||||||
if applyFailed {
|
|
||||||
log.Error("failed to apply firewall rules, rollback ACL to previous state")
|
|
||||||
for _, rules := range newRulePairs {
|
|
||||||
for _, rule := range rules {
|
|
||||||
if err := d.manager.DeleteRule(rule); err != nil {
|
|
||||||
log.Errorf("failed to delete new firewall rule (id: %v) during rollback: %v", rule.GetRuleID(), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for pairID, rules := range d.rulesPairs {
|
for pairID, rules := range d.rulesPairs {
|
||||||
if _, ok := newRulePairs[pairID]; !ok {
|
if _, ok := newRulePairs[pairID]; !ok {
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if err := d.manager.DeleteRule(rule); err != nil {
|
if err := d.firewall.DeleteRule(rule); err != nil {
|
||||||
log.Errorf("failed to delete firewall rule: %v", err)
|
log.Errorf("failed to delete firewall rule: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -187,16 +149,6 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap) {
|
|||||||
d.rulesPairs = newRulePairs
|
d.rulesPairs = newRulePairs
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop ACL controller and clear firewall state
|
|
||||||
func (d *DefaultManager) Stop() {
|
|
||||||
d.mutex.Lock()
|
|
||||||
defer d.mutex.Unlock()
|
|
||||||
|
|
||||||
if err := d.manager.Reset(); err != nil {
|
|
||||||
log.WithError(err).Error("reset firewall state")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DefaultManager) protoRuleToFirewallRule(
|
func (d *DefaultManager) protoRuleToFirewallRule(
|
||||||
r *mgmProto.FirewallRule,
|
r *mgmProto.FirewallRule,
|
||||||
ipsetName string,
|
ipsetName string,
|
||||||
@@ -206,14 +158,14 @@ func (d *DefaultManager) protoRuleToFirewallRule(
|
|||||||
return "", nil, fmt.Errorf("invalid IP address, skipping firewall rule")
|
return "", nil, fmt.Errorf("invalid IP address, skipping firewall rule")
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol := convertToFirewallProtocol(r.Protocol)
|
protocol, err := convertToFirewallProtocol(r.Protocol)
|
||||||
if protocol == firewall.ProtocolUnknown {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("invalid protocol type: %d, skipping firewall rule", r.Protocol)
|
return "", nil, fmt.Errorf("skipping firewall rule: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
action := convertFirewallAction(r.Action)
|
action, err := convertFirewallAction(r.Action)
|
||||||
if action == firewall.ActionUnknown {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("invalid action type: %d, skipping firewall rule", r.Action)
|
return "", nil, fmt.Errorf("skipping firewall rule: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var port *firewall.Port
|
var port *firewall.Port
|
||||||
@@ -233,7 +185,6 @@ func (d *DefaultManager) protoRuleToFirewallRule(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var rules []firewall.Rule
|
var rules []firewall.Rule
|
||||||
var err error
|
|
||||||
switch r.Direction {
|
switch r.Direction {
|
||||||
case mgmProto.FirewallRule_IN:
|
case mgmProto.FirewallRule_IN:
|
||||||
rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "")
|
rules, err = d.addInRules(ip, protocol, port, action, ipsetName, "")
|
||||||
@@ -247,7 +198,6 @@ func (d *DefaultManager) protoRuleToFirewallRule(
|
|||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
d.rulesPairs[ruleID] = rules
|
|
||||||
return ruleID, rules, nil
|
return ruleID, rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,24 +210,24 @@ func (d *DefaultManager) addInRules(
|
|||||||
comment string,
|
comment string,
|
||||||
) ([]firewall.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
var rules []firewall.Rule
|
var rules []firewall.Rule
|
||||||
rule, err := d.manager.AddFiltering(
|
rule, err := d.firewall.AddFiltering(
|
||||||
ip, protocol, nil, port, firewall.RuleDirectionIN, action, ipsetName, comment)
|
ip, protocol, nil, port, firewall.RuleDirectionIN, action, ipsetName, comment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
||||||
}
|
}
|
||||||
rules = append(rules, rule)
|
rules = append(rules, rule...)
|
||||||
|
|
||||||
if shouldSkipInvertedRule(protocol, port) {
|
if shouldSkipInvertedRule(protocol, port) {
|
||||||
return rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rule, err = d.manager.AddFiltering(
|
rule, err = d.firewall.AddFiltering(
|
||||||
ip, protocol, port, nil, firewall.RuleDirectionOUT, action, ipsetName, comment)
|
ip, protocol, port, nil, firewall.RuleDirectionOUT, action, ipsetName, comment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(rules, rule), nil
|
return append(rules, rule...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultManager) addOutRules(
|
func (d *DefaultManager) addOutRules(
|
||||||
@@ -289,24 +239,24 @@ func (d *DefaultManager) addOutRules(
|
|||||||
comment string,
|
comment string,
|
||||||
) ([]firewall.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
var rules []firewall.Rule
|
var rules []firewall.Rule
|
||||||
rule, err := d.manager.AddFiltering(
|
rule, err := d.firewall.AddFiltering(
|
||||||
ip, protocol, nil, port, firewall.RuleDirectionOUT, action, ipsetName, comment)
|
ip, protocol, nil, port, firewall.RuleDirectionOUT, action, ipsetName, comment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
||||||
}
|
}
|
||||||
rules = append(rules, rule)
|
rules = append(rules, rule...)
|
||||||
|
|
||||||
if shouldSkipInvertedRule(protocol, port) {
|
if shouldSkipInvertedRule(protocol, port) {
|
||||||
return rules, nil
|
return rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rule, err = d.manager.AddFiltering(
|
rule, err = d.firewall.AddFiltering(
|
||||||
ip, protocol, port, nil, firewall.RuleDirectionIN, action, ipsetName, comment)
|
ip, protocol, port, nil, firewall.RuleDirectionIN, action, ipsetName, comment)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
return nil, fmt.Errorf("failed to add firewall rule: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return append(rules, rule), nil
|
return append(rules, rule...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getRuleID() returns unique ID for the rule based on its parameters.
|
// getRuleID() returns unique ID for the rule based on its parameters.
|
||||||
@@ -367,7 +317,7 @@ func (d *DefaultManager) squashAcceptRules(
|
|||||||
protocols[r.Protocol] = map[string]int{}
|
protocols[r.Protocol] = map[string]int{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// special case, when we recieve this all network IP address
|
// special case, when we receive this all network IP address
|
||||||
// it means that rules for that protocol was already optimized on the
|
// it means that rules for that protocol was already optimized on the
|
||||||
// management side
|
// management side
|
||||||
if r.PeerIP == "0.0.0.0" {
|
if r.PeerIP == "0.0.0.0" {
|
||||||
@@ -394,7 +344,7 @@ func (d *DefaultManager) squashAcceptRules(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// order of squashing by protocol is important
|
// order of squashing by protocol is important
|
||||||
// only for ther first element ALL, it must be done first
|
// only for their first element ALL, it must be done first
|
||||||
protocolOrders := []mgmProto.FirewallRuleProtocol{
|
protocolOrders := []mgmProto.FirewallRuleProtocol{
|
||||||
mgmProto.FirewallRule_ALL,
|
mgmProto.FirewallRule_ALL,
|
||||||
mgmProto.FirewallRule_ICMP,
|
mgmProto.FirewallRule_ICMP,
|
||||||
@@ -462,18 +412,29 @@ func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) st
|
|||||||
return fmt.Sprintf("%v:%v:%v:%s", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port)
|
return fmt.Sprintf("%v:%v:%v:%s", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToFirewallProtocol(protocol mgmProto.FirewallRuleProtocol) firewall.Protocol {
|
func (d *DefaultManager) rollBack(newRulePairs map[string][]firewall.Rule) {
|
||||||
|
log.Debugf("rollback ACL to previous state")
|
||||||
|
for _, rules := range newRulePairs {
|
||||||
|
for _, rule := range rules {
|
||||||
|
if err := d.firewall.DeleteRule(rule); err != nil {
|
||||||
|
log.Errorf("failed to delete new firewall rule (id: %v) during rollback: %v", rule.GetRuleID(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToFirewallProtocol(protocol mgmProto.FirewallRuleProtocol) (firewall.Protocol, error) {
|
||||||
switch protocol {
|
switch protocol {
|
||||||
case mgmProto.FirewallRule_TCP:
|
case mgmProto.FirewallRule_TCP:
|
||||||
return firewall.ProtocolTCP
|
return firewall.ProtocolTCP, nil
|
||||||
case mgmProto.FirewallRule_UDP:
|
case mgmProto.FirewallRule_UDP:
|
||||||
return firewall.ProtocolUDP
|
return firewall.ProtocolUDP, nil
|
||||||
case mgmProto.FirewallRule_ICMP:
|
case mgmProto.FirewallRule_ICMP:
|
||||||
return firewall.ProtocolICMP
|
return firewall.ProtocolICMP, nil
|
||||||
case mgmProto.FirewallRule_ALL:
|
case mgmProto.FirewallRule_ALL:
|
||||||
return firewall.ProtocolALL
|
return firewall.ProtocolALL, nil
|
||||||
default:
|
default:
|
||||||
return firewall.ProtocolUnknown
|
return firewall.ProtocolALL, fmt.Errorf("invalid protocol type: %s", protocol.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -481,13 +442,13 @@ func shouldSkipInvertedRule(protocol firewall.Protocol, port *firewall.Port) boo
|
|||||||
return protocol == firewall.ProtocolALL || protocol == firewall.ProtocolICMP || port == nil
|
return protocol == firewall.ProtocolALL || protocol == firewall.ProtocolICMP || port == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertFirewallAction(action mgmProto.FirewallRuleAction) firewall.Action {
|
func convertFirewallAction(action mgmProto.FirewallRuleAction) (firewall.Action, error) {
|
||||||
switch action {
|
switch action {
|
||||||
case mgmProto.FirewallRule_ACCEPT:
|
case mgmProto.FirewallRule_ACCEPT:
|
||||||
return firewall.ActionAccept
|
return firewall.ActionAccept, nil
|
||||||
case mgmProto.FirewallRule_DROP:
|
case mgmProto.FirewallRule_DROP:
|
||||||
return firewall.ActionDrop
|
return firewall.ActionDrop, nil
|
||||||
default:
|
default:
|
||||||
return firewall.ActionUnknown
|
return firewall.ActionDrop, fmt.Errorf("invalid action type: %d", action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
//go:build !linux
|
|
||||||
|
|
||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create creates a firewall manager instance
|
|
||||||
func Create(iface IFaceMapper) (manager *DefaultManager, err error) {
|
|
||||||
if iface.IsUserspaceBind() {
|
|
||||||
// use userspace packet filtering firewall
|
|
||||||
fm, err := uspfilter.Create(iface)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return newDefaultManager(fm), nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package acl
|
|
||||||
|
|
||||||
import (
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall"
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/iptables"
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/nftables"
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create creates a firewall manager instance for the Linux
|
|
||||||
func Create(iface IFaceMapper) (manager *DefaultManager, err error) {
|
|
||||||
var fm firewall.Manager
|
|
||||||
if iface.IsUserspaceBind() {
|
|
||||||
// use userspace packet filtering firewall
|
|
||||||
if fm, err = uspfilter.Create(iface); err != nil {
|
|
||||||
log.Debugf("failed to create userspace filtering firewall: %s", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if fm, err = nftables.Create(iface); err != nil {
|
|
||||||
log.Debugf("failed to create nftables manager: %s", err)
|
|
||||||
// fallback to iptables
|
|
||||||
if fm, err = iptables.Create(iface); err != nil {
|
|
||||||
log.Errorf("failed to create iptables manager: %s", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newDefaultManager(fm), nil
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
package acl
|
package acl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/internal/acl/mocks"
|
"github.com/netbirdio/netbird/client/internal/acl/mocks"
|
||||||
|
"github.com/netbirdio/netbird/iface"
|
||||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,18 +37,30 @@ func TestDefaultManager(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
iface := mocks.NewMockIFaceMapper(ctrl)
|
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
||||||
iface.EXPECT().IsUserspaceBind().Return(true)
|
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
||||||
// iface.EXPECT().Name().Return("lo")
|
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
||||||
iface.EXPECT().SetFilter(gomock.Any())
|
ip, network, err := net.ParseCIDR("172.0.0.1/32")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse IP address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
||||||
|
ifaceMock.EXPECT().Address().Return(iface.WGAddress{
|
||||||
|
IP: ip,
|
||||||
|
Network: network,
|
||||||
|
}).AnyTimes()
|
||||||
|
|
||||||
// we receive one rule from the management so for testing purposes ignore it
|
// we receive one rule from the management so for testing purposes ignore it
|
||||||
acl, err := Create(iface)
|
fw, err := firewall.NewFirewall(context.Background(), ifaceMock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("create ACL manager: %v", err)
|
t.Errorf("create firewall: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer acl.Stop()
|
defer func(fw manager.Manager) {
|
||||||
|
_ = fw.Reset()
|
||||||
|
}(fw)
|
||||||
|
acl := NewDefaultManager(fw)
|
||||||
|
|
||||||
t.Run("apply firewall rules", func(t *testing.T) {
|
t.Run("apply firewall rules", func(t *testing.T) {
|
||||||
acl.ApplyFiltering(networkMap)
|
acl.ApplyFiltering(networkMap)
|
||||||
@@ -178,31 +195,33 @@ func TestDefaultManagerSquashRules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := rules[0]
|
r := rules[0]
|
||||||
if r.PeerIP != "0.0.0.0" {
|
switch {
|
||||||
|
case r.PeerIP != "0.0.0.0":
|
||||||
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
|
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
|
||||||
return
|
return
|
||||||
} else if r.Direction != mgmProto.FirewallRule_IN {
|
case r.Direction != mgmProto.FirewallRule_IN:
|
||||||
t.Errorf("direction should be IN, got: %v", r.Direction)
|
t.Errorf("direction should be IN, got: %v", r.Direction)
|
||||||
return
|
return
|
||||||
} else if r.Protocol != mgmProto.FirewallRule_ALL {
|
case r.Protocol != mgmProto.FirewallRule_ALL:
|
||||||
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
|
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
|
||||||
return
|
return
|
||||||
} else if r.Action != mgmProto.FirewallRule_ACCEPT {
|
case r.Action != mgmProto.FirewallRule_ACCEPT:
|
||||||
t.Errorf("action should be ACCEPT, got: %v", r.Action)
|
t.Errorf("action should be ACCEPT, got: %v", r.Action)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
r = rules[1]
|
r = rules[1]
|
||||||
if r.PeerIP != "0.0.0.0" {
|
switch {
|
||||||
|
case r.PeerIP != "0.0.0.0":
|
||||||
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
|
t.Errorf("IP should be 0.0.0.0, got: %v", r.PeerIP)
|
||||||
return
|
return
|
||||||
} else if r.Direction != mgmProto.FirewallRule_OUT {
|
case r.Direction != mgmProto.FirewallRule_OUT:
|
||||||
t.Errorf("direction should be OUT, got: %v", r.Direction)
|
t.Errorf("direction should be OUT, got: %v", r.Direction)
|
||||||
return
|
return
|
||||||
} else if r.Protocol != mgmProto.FirewallRule_ALL {
|
case r.Protocol != mgmProto.FirewallRule_ALL:
|
||||||
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
|
t.Errorf("protocol should be ALL, got: %v", r.Protocol)
|
||||||
return
|
return
|
||||||
} else if r.Action != mgmProto.FirewallRule_ACCEPT {
|
case r.Action != mgmProto.FirewallRule_ACCEPT:
|
||||||
t.Errorf("action should be ACCEPT, got: %v", r.Action)
|
t.Errorf("action should be ACCEPT, got: %v", r.Action)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -270,7 +289,7 @@ func TestDefaultManagerSquashRulesNoAffect(t *testing.T) {
|
|||||||
|
|
||||||
manager := &DefaultManager{}
|
manager := &DefaultManager{}
|
||||||
if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) {
|
if rules, _ := manager.squashAcceptRules(networkMap); len(rules) != len(networkMap.FirewallRules) {
|
||||||
t.Errorf("we should got same amount of rules as intput, got %v", len(rules))
|
t.Errorf("we should get the same amount of rules as output, got %v", len(rules))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,18 +330,30 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) {
|
|||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
iface := mocks.NewMockIFaceMapper(ctrl)
|
ifaceMock := mocks.NewMockIFaceMapper(ctrl)
|
||||||
iface.EXPECT().IsUserspaceBind().Return(true)
|
ifaceMock.EXPECT().IsUserspaceBind().Return(true).AnyTimes()
|
||||||
// iface.EXPECT().Name().Return("lo")
|
ifaceMock.EXPECT().SetFilter(gomock.Any())
|
||||||
iface.EXPECT().SetFilter(gomock.Any())
|
ip, network, err := net.ParseCIDR("172.0.0.1/32")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to parse IP address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ifaceMock.EXPECT().Name().Return("lo").AnyTimes()
|
||||||
|
ifaceMock.EXPECT().Address().Return(iface.WGAddress{
|
||||||
|
IP: ip,
|
||||||
|
Network: network,
|
||||||
|
}).AnyTimes()
|
||||||
|
|
||||||
// we receive one rule from the management so for testing purposes ignore it
|
// we receive one rule from the management so for testing purposes ignore it
|
||||||
acl, err := Create(iface)
|
fw, err := firewall.NewFirewall(context.Background(), ifaceMock)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("create ACL manager: %v", err)
|
t.Errorf("create firewall: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer acl.Stop()
|
defer func(fw manager.Manager) {
|
||||||
|
_ = fw.Reset()
|
||||||
|
}(fw)
|
||||||
|
acl := NewDefaultManager(fw)
|
||||||
|
|
||||||
acl.ApplyFiltering(networkMap)
|
acl.ApplyFiltering(networkMap)
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HostedGrantType grant type for device flow on Hosted
|
// HostedGrantType grant type for device flow on Hosted
|
||||||
@@ -174,7 +175,7 @@ func (d *DeviceAuthorizationFlow) WaitToken(ctx context.Context, info AuthFlowIn
|
|||||||
if tokenResponse.Error == "authorization_pending" {
|
if tokenResponse.Error == "authorization_pending" {
|
||||||
continue
|
continue
|
||||||
} else if tokenResponse.Error == "slow_down" {
|
} else if tokenResponse.Error == "slow_down" {
|
||||||
interval = interval + (3 * time.Second)
|
interval += (3 * time.Second)
|
||||||
ticker.Reset(interval)
|
ticker.Reset(interval)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -25,7 +26,7 @@ type HTTPClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AuthFlowInfo holds information for the OAuth 2.0 authorization flow
|
// AuthFlowInfo holds information for the OAuth 2.0 authorization flow
|
||||||
type AuthFlowInfo struct {
|
type AuthFlowInfo struct { //nolint:revive
|
||||||
DeviceCode string `json:"device_code"`
|
DeviceCode string `json:"device_code"`
|
||||||
UserCode string `json:"user_code"`
|
UserCode string `json:"user_code"`
|
||||||
VerificationURI string `json:"verification_uri"`
|
VerificationURI string `json:"verification_uri"`
|
||||||
@@ -57,34 +58,52 @@ func (t TokenInfo) GetTokenToUse() string {
|
|||||||
return t.AccessToken
|
return t.AccessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration.
|
// NewOAuthFlow initializes and returns the appropriate OAuth flow based on the management configuration
|
||||||
func NewOAuthFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
//
|
||||||
log.Debug("getting device authorization flow info")
|
// It starts by initializing the PKCE.If this process fails, it resorts to the Device Code Flow,
|
||||||
|
// and if that also fails, the authentication process is deemed unsuccessful
|
||||||
// Try to initialize the Device Authorization Flow
|
//
|
||||||
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
// On Linux distros without desktop environment support, it only tries to initialize the Device Code Flow
|
||||||
if err == nil {
|
func NewOAuthFlow(ctx context.Context, config *internal.Config, isLinuxDesktopClient bool) (OAuthFlow, error) {
|
||||||
return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig)
|
if runtime.GOOS == "linux" && !isLinuxDesktopClient {
|
||||||
|
return authenticateWithDeviceCodeFlow(ctx, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("getting device authorization flow info failed with error: %v", err)
|
pkceFlow, err := authenticateWithPKCEFlow(ctx, config)
|
||||||
log.Debugf("falling back to pkce authorization flow info")
|
if err != nil {
|
||||||
|
// fallback to device code flow
|
||||||
|
log.Debugf("failed to initialize pkce authentication with error: %v\n", err)
|
||||||
|
log.Debug("falling back to device code flow")
|
||||||
|
return authenticateWithDeviceCodeFlow(ctx, config)
|
||||||
|
}
|
||||||
|
return pkceFlow, nil
|
||||||
|
}
|
||||||
|
|
||||||
// If Device Authorization Flow failed, try the PKCE Authorization Flow
|
// authenticateWithPKCEFlow initializes the Proof Key for Code Exchange flow auth flow
|
||||||
|
func authenticateWithPKCEFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
||||||
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
pkceFlowInfo, err := internal.GetPKCEAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s, ok := gstatus.FromError(err)
|
return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err)
|
||||||
if ok && s.Code() == codes.NotFound {
|
}
|
||||||
|
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticateWithDeviceCodeFlow initializes the Device Code auth Flow
|
||||||
|
func authenticateWithDeviceCodeFlow(ctx context.Context, config *internal.Config) (OAuthFlow, error) {
|
||||||
|
deviceFlowInfo, err := internal.GetDeviceAuthorizationFlowInfo(ctx, config.PrivateKey, config.ManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
switch s, ok := gstatus.FromError(err); {
|
||||||
|
case ok && s.Code() == codes.NotFound:
|
||||||
return nil, fmt.Errorf("no SSO provider returned from management. " +
|
return nil, fmt.Errorf("no SSO provider returned from management. " +
|
||||||
"If you are using hosting Netbird see documentation at " +
|
"Please proceed with setting up this device using setup keys " +
|
||||||
"https://github.com/netbirdio/netbird/tree/main/management for details")
|
"https://docs.netbird.io/how-to/register-machines-using-setup-keys")
|
||||||
} else if ok && s.Code() == codes.Unimplemented {
|
case ok && s.Code() == codes.Unimplemented:
|
||||||
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
return nil, fmt.Errorf("the management server, %s, does not support SSO providers, "+
|
||||||
"please update your server or use Setup Keys to login", config.ManagementURL)
|
"please update your server or use Setup Keys to login", config.ManagementURL)
|
||||||
} else {
|
default:
|
||||||
return nil, fmt.Errorf("getting pkce authorization flow info failed with error: %v", err)
|
return nil, fmt.Errorf("getting device authorization flow info failed with error: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewPKCEAuthorizationFlow(pkceFlowInfo.ProviderConfig)
|
return NewDeviceAuthorizationFlow(deviceFlowInfo.ProviderConfig)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net"
|
"net"
|
||||||
@@ -25,6 +26,8 @@ var _ OAuthFlow = &PKCEAuthorizationFlow{}
|
|||||||
const (
|
const (
|
||||||
queryState = "state"
|
queryState = "state"
|
||||||
queryCode = "code"
|
queryCode = "code"
|
||||||
|
queryError = "error"
|
||||||
|
queryErrorDesc = "error_description"
|
||||||
defaultPKCETimeoutSeconds = 300
|
defaultPKCETimeoutSeconds = 300
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -76,7 +79,7 @@ func (p *PKCEAuthorizationFlow) GetClientID(_ context.Context) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RequestAuthInfo requests a authorization code login flow information.
|
// RequestAuthInfo requests a authorization code login flow information.
|
||||||
func (p *PKCEAuthorizationFlow) RequestAuthInfo(_ context.Context) (AuthFlowInfo, error) {
|
func (p *PKCEAuthorizationFlow) RequestAuthInfo(ctx context.Context) (AuthFlowInfo, error) {
|
||||||
state, err := randomBytesInHex(24)
|
state, err := randomBytesInHex(24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AuthFlowInfo{}, fmt.Errorf("could not generate random state: %v", err)
|
return AuthFlowInfo{}, fmt.Errorf("could not generate random state: %v", err)
|
||||||
@@ -110,40 +113,63 @@ func (p *PKCEAuthorizationFlow) WaitToken(ctx context.Context, _ AuthFlowInfo) (
|
|||||||
tokenChan := make(chan *oauth2.Token, 1)
|
tokenChan := make(chan *oauth2.Token, 1)
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
go p.startServer(tokenChan, errChan)
|
parsedURL, err := url.Parse(p.oAuthConfig.RedirectURL)
|
||||||
|
if err != nil {
|
||||||
|
return TokenInfo{}, fmt.Errorf("failed to parse redirect URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &http.Server{Addr: fmt.Sprintf(":%s", parsedURL.Port())}
|
||||||
|
defer func() {
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := server.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Errorf("failed to close the server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go p.startServer(server, tokenChan, errChan)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return TokenInfo{}, ctx.Err()
|
return TokenInfo{}, ctx.Err()
|
||||||
case token := <-tokenChan:
|
case token := <-tokenChan:
|
||||||
return p.handleOAuthToken(token)
|
return p.parseOAuthToken(token)
|
||||||
case err := <-errChan:
|
case err := <-errChan:
|
||||||
return TokenInfo{}, err
|
return TokenInfo{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errChan chan<- error) {
|
func (p *PKCEAuthorizationFlow) startServer(server *http.Server, tokenChan chan<- *oauth2.Token, errChan chan<- error) {
|
||||||
parsedURL, err := url.Parse(p.oAuthConfig.RedirectURL)
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
token, err := p.handleRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errChan <- fmt.Errorf("failed to parse redirect URL: %v", err)
|
renderPKCEFlowTmpl(w, err)
|
||||||
|
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
port := parsedURL.Port()
|
|
||||||
|
|
||||||
server := http.Server{Addr: fmt.Sprintf(":%s", port)}
|
renderPKCEFlowTmpl(w, nil)
|
||||||
defer func() {
|
tokenChan <- token
|
||||||
if err := server.Shutdown(context.Background()); err != nil {
|
})
|
||||||
log.Errorf("error while shutting down pkce flow server: %v", err)
|
|
||||||
|
server.Handler = mux
|
||||||
|
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
func (p *PKCEAuthorizationFlow) handleRequest(req *http.Request) (*oauth2.Token, error) {
|
||||||
tokenValidatorFunc := func() (*oauth2.Token, error) {
|
|
||||||
query := req.URL.Query()
|
query := req.URL.Query()
|
||||||
|
|
||||||
state := query.Get(queryState)
|
if authError := query.Get(queryError); authError != "" {
|
||||||
// Prevent timing attacks on state
|
authErrorDesc := query.Get(queryErrorDesc)
|
||||||
if subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
return nil, fmt.Errorf("%s.%s", authError, authErrorDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent timing attacks on the state
|
||||||
|
if state := query.Get(queryState); subtle.ConstantTimeCompare([]byte(p.state), []byte(state)) == 0 {
|
||||||
return nil, fmt.Errorf("invalid state")
|
return nil, fmt.Errorf("invalid state")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,22 +185,7 @@ func (p *PKCEAuthorizationFlow) startServer(tokenChan chan<- *oauth2.Token, errC
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := tokenValidatorFunc()
|
func (p *PKCEAuthorizationFlow) parseOAuthToken(token *oauth2.Token) (TokenInfo, error) {
|
||||||
if err != nil {
|
|
||||||
errChan <- fmt.Errorf("PKCE authorization flow failed: %v", err)
|
|
||||||
renderPKCEFlowTmpl(w, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenChan <- token
|
|
||||||
renderPKCEFlowTmpl(w, nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := server.ListenAndServe(); err != nil {
|
|
||||||
errChan <- err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *PKCEAuthorizationFlow) handleOAuthToken(token *oauth2.Token) (TokenInfo, error) {
|
|
||||||
tokenInfo := TokenInfo{
|
tokenInfo := TokenInfo{
|
||||||
AccessToken: token.AccessToken,
|
AccessToken: token.AccessToken,
|
||||||
RefreshToken: token.RefreshToken,
|
RefreshToken: token.RefreshToken,
|
||||||
@@ -186,7 +197,13 @@ func (p *PKCEAuthorizationFlow) handleOAuthToken(token *oauth2.Token) (TokenInfo
|
|||||||
tokenInfo.IDToken = idToken
|
tokenInfo.IDToken = idToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), p.providerConfig.Audience); err != nil {
|
// if a provider doesn't support an audience, use the Client ID for token verification
|
||||||
|
audience := p.providerConfig.Audience
|
||||||
|
if audience == "" {
|
||||||
|
audience = p.providerConfig.ClientID
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := isValidAccessToken(tokenInfo.GetTokenToUse(), audience); err != nil {
|
||||||
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
|
return TokenInfo{}, fmt.Errorf("validate access token failed with error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,15 +43,14 @@ func isValidAccessToken(token string, audience string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audience claim of JWT can be a string or an array of strings
|
// Audience claim of JWT can be a string or an array of strings
|
||||||
typ := reflect.TypeOf(claims.Audience)
|
switch aud := claims.Audience.(type) {
|
||||||
switch typ.Kind() {
|
case string:
|
||||||
case reflect.String:
|
if aud == audience {
|
||||||
if claims.Audience == audience {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
case reflect.Slice:
|
case []interface{}:
|
||||||
for _, aud := range claims.Audience.([]interface{}) {
|
for _, audItem := range aud {
|
||||||
if audience == aud {
|
if audStr, ok := audItem.(string); ok && audStr == audience {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
|
||||||
"github.com/netbirdio/netbird/client/ssh"
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/iface"
|
"github.com/netbirdio/netbird/iface"
|
||||||
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ManagementLegacyPort is the port that was used before by the Management gRPC server.
|
// managementLegacyPortString is the port that was used before by the Management gRPC server.
|
||||||
// It is used for backward compatibility now.
|
// It is used for backward compatibility now.
|
||||||
// NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import
|
// NB: hardcoded from github.com/netbirdio/netbird/management/cmd to avoid import
|
||||||
ManagementLegacyPort = 33073
|
managementLegacyPortString = "33073"
|
||||||
// DefaultManagementURL points to the NetBird's cloud management endpoint
|
// DefaultManagementURL points to the NetBird's cloud management endpoint
|
||||||
DefaultManagementURL = "https://api.wiretrustee.com:443"
|
DefaultManagementURL = "https://api.netbird.io:443"
|
||||||
|
// oldDefaultManagementURL points to the NetBird's old cloud management endpoint
|
||||||
|
oldDefaultManagementURL = "https://api.wiretrustee.com:443"
|
||||||
// DefaultAdminURL points to NetBird's cloud management console
|
// DefaultAdminURL points to NetBird's cloud management console
|
||||||
DefaultAdminURL = "https://app.netbird.io:443"
|
DefaultAdminURL = "https://app.netbird.io:443"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultInterfaceBlacklist = []string{iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
|
var defaultInterfaceBlacklist = []string{
|
||||||
"Tailscale", "tailscale", "docker", "veth", "br-", "lo"}
|
iface.WgInterfaceDefault, "wt", "utun", "tun0", "zt", "ZeroTier", "wg", "ts",
|
||||||
|
"Tailscale", "tailscale", "docker", "veth", "br-", "lo",
|
||||||
|
}
|
||||||
|
|
||||||
// ConfigInput carries configuration changes to the client
|
// ConfigInput carries configuration changes to the client
|
||||||
type ConfigInput struct {
|
type ConfigInput struct {
|
||||||
@@ -35,8 +46,17 @@ type ConfigInput struct {
|
|||||||
AdminURL string
|
AdminURL string
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
PreSharedKey *string
|
PreSharedKey *string
|
||||||
|
ServerSSHAllowed *bool
|
||||||
NATExternalIPs []string
|
NATExternalIPs []string
|
||||||
CustomDNSAddress []byte
|
CustomDNSAddress []byte
|
||||||
|
RosenpassEnabled *bool
|
||||||
|
RosenpassPermissive *bool
|
||||||
|
InterfaceName *string
|
||||||
|
WireguardPort *int
|
||||||
|
NetworkMonitor *bool
|
||||||
|
DisableAutoConnect *bool
|
||||||
|
ExtraIFaceBlackList []string
|
||||||
|
DNSRouteInterval *time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config Configuration type
|
// Config Configuration type
|
||||||
@@ -48,12 +68,16 @@ type Config struct {
|
|||||||
AdminURL *url.URL
|
AdminURL *url.URL
|
||||||
WgIface string
|
WgIface string
|
||||||
WgPort int
|
WgPort int
|
||||||
|
NetworkMonitor *bool
|
||||||
IFaceBlackList []string
|
IFaceBlackList []string
|
||||||
DisableIPv6Discovery bool
|
DisableIPv6Discovery bool
|
||||||
|
RosenpassEnabled bool
|
||||||
|
RosenpassPermissive bool
|
||||||
|
ServerSSHAllowed *bool
|
||||||
// SSHKey is a private SSH key in a PEM format
|
// SSHKey is a private SSH key in a PEM format
|
||||||
SSHKey string
|
SSHKey string
|
||||||
|
|
||||||
// ExternalIP mappings, if different than the host interface IP
|
// ExternalIP mappings, if different from the host interface IP
|
||||||
//
|
//
|
||||||
// External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
|
// External IP must not be behind a CGNAT and port-forwarding for incoming UDP packets from WgPort on ExternalIP
|
||||||
// to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
|
// to WgPort on host interface IP must be present. This can take form of single port-forwarding rule, 1:1 DNAT
|
||||||
@@ -71,6 +95,13 @@ type Config struct {
|
|||||||
NATExternalIPs []string
|
NATExternalIPs []string
|
||||||
// CustomDNSAddress sets the DNS resolver listening address in format ip:port
|
// CustomDNSAddress sets the DNS resolver listening address in format ip:port
|
||||||
CustomDNSAddress string
|
CustomDNSAddress string
|
||||||
|
|
||||||
|
// DisableAutoConnect determines whether the client should not start with the service
|
||||||
|
// it's set to false by default due to backwards compatibility
|
||||||
|
DisableAutoConnect bool
|
||||||
|
|
||||||
|
// DNSRouteInterval is the interval in which the DNS routes are updated
|
||||||
|
DNSRouteInterval time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
// ReadConfig read config file and return with Config. If it is not exists create a new with default values
|
||||||
@@ -80,6 +111,15 @@ func ReadConfig(configPath string) (*Config, error) {
|
|||||||
if _, err := util.ReadJson(configPath, config); err != nil {
|
if _, err := util.ReadJson(configPath, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// initialize through apply() without changes
|
||||||
|
if changed, err := config.apply(ConfigInput{}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if changed {
|
||||||
|
if err = WriteOutConfig(configPath, config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,55 +171,15 @@ func WriteOutConfig(path string, config *Config) error {
|
|||||||
|
|
||||||
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
// createNewConfig creates a new config generating a new Wireguard key and saving to file
|
||||||
func createNewConfig(input ConfigInput) (*Config, error) {
|
func createNewConfig(input ConfigInput) (*Config, error) {
|
||||||
wgKey := generateKey()
|
|
||||||
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
SSHKey: string(pem),
|
// defaults to false only for new (post 0.26) configurations
|
||||||
PrivateKey: wgKey,
|
ServerSSHAllowed: util.False(),
|
||||||
WgIface: iface.WgInterfaceDefault,
|
|
||||||
WgPort: iface.DefaultWgPort,
|
|
||||||
IFaceBlackList: []string{},
|
|
||||||
DisableIPv6Discovery: false,
|
|
||||||
NATExternalIPs: input.NATExternalIPs,
|
|
||||||
CustomDNSAddress: string(input.CustomDNSAddress),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
|
if _, err := config.apply(input); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
config.ManagementURL = defaultManagementURL
|
|
||||||
if input.ManagementURL != "" {
|
|
||||||
URL, err := parseURL("Management URL", input.ManagementURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.ManagementURL = URL
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.PreSharedKey != nil {
|
|
||||||
config.PreSharedKey = *input.PreSharedKey
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultAdminURL, err := parseURL("Admin URL", DefaultAdminURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
config.AdminURL = defaultAdminURL
|
|
||||||
if input.AdminURL != "" {
|
|
||||||
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.AdminURL = newURL
|
|
||||||
}
|
|
||||||
|
|
||||||
config.IFaceBlackList = defaultInterfaceBlacklist
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,64 +190,12 @@ func update(input ConfigInput) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh := false
|
updated, err := config.apply(input)
|
||||||
|
|
||||||
if input.ManagementURL != "" && config.ManagementURL.String() != input.ManagementURL {
|
|
||||||
log.Infof("new Management URL provided, updated to %s (old value %s)",
|
|
||||||
input.ManagementURL, config.ManagementURL)
|
|
||||||
newURL, err := parseURL("Management URL", input.ManagementURL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
config.ManagementURL = newURL
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.AdminURL != "" && (config.AdminURL == nil || config.AdminURL.String() != input.AdminURL) {
|
if updated {
|
||||||
log.Infof("new Admin Panel URL provided, updated to %s (old value %s)",
|
|
||||||
input.AdminURL, config.AdminURL)
|
|
||||||
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
config.AdminURL = newURL
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.PreSharedKey != nil && config.PreSharedKey != *input.PreSharedKey {
|
|
||||||
if *input.PreSharedKey != "" {
|
|
||||||
log.Infof("new pre-shared key provides, updated to %s (old value %s)",
|
|
||||||
*input.PreSharedKey, config.PreSharedKey)
|
|
||||||
config.PreSharedKey = *input.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 input.NATExternalIPs != nil && len(config.NATExternalIPs) != len(input.NATExternalIPs) {
|
|
||||||
config.NATExternalIPs = input.NATExternalIPs
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if input.CustomDNSAddress != nil {
|
|
||||||
config.CustomDNSAddress = string(input.CustomDNSAddress)
|
|
||||||
refresh = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if refresh {
|
|
||||||
// since we have new management URL, we need to update config file
|
|
||||||
if err := util.WriteJson(input.ConfigPath, config); err != nil {
|
if err := util.WriteJson(input.ConfigPath, config); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -256,6 +204,190 @@ func update(input ConfigInput) (*Config, error) {
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) apply(input ConfigInput) (updated bool, err error) {
|
||||||
|
if config.ManagementURL == nil {
|
||||||
|
log.Infof("using default Management URL %s", DefaultManagementURL)
|
||||||
|
config.ManagementURL, err = parseURL("Management URL", DefaultManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.ManagementURL != "" && input.ManagementURL != config.ManagementURL.String() {
|
||||||
|
log.Infof("new Management URL provided, updated to %#v (old value %#v)",
|
||||||
|
input.ManagementURL, config.ManagementURL.String())
|
||||||
|
URL, err := parseURL("Management URL", input.ManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
config.ManagementURL = URL
|
||||||
|
updated = true
|
||||||
|
} else if config.ManagementURL == nil {
|
||||||
|
log.Infof("using default Management URL %s", DefaultManagementURL)
|
||||||
|
config.ManagementURL, err = parseURL("Management URL", DefaultManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.AdminURL == nil {
|
||||||
|
log.Infof("using default Admin URL %s", DefaultManagementURL)
|
||||||
|
config.AdminURL, err = parseURL("Admin URL", DefaultAdminURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.AdminURL != "" && input.AdminURL != config.AdminURL.String() {
|
||||||
|
log.Infof("new Admin Panel URL provided, updated to %#v (old value %#v)",
|
||||||
|
input.AdminURL, config.AdminURL.String())
|
||||||
|
newURL, err := parseURL("Admin Panel URL", input.AdminURL)
|
||||||
|
if err != nil {
|
||||||
|
return updated, err
|
||||||
|
}
|
||||||
|
config.AdminURL = newURL
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.PrivateKey == "" {
|
||||||
|
log.Infof("generated new Wireguard key")
|
||||||
|
config.PrivateKey = generateKey()
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.SSHKey == "" {
|
||||||
|
log.Infof("generated new SSH key")
|
||||||
|
pem, err := ssh.GeneratePrivateKey(ssh.ED25519)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
config.SSHKey = string(pem)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.WireguardPort != nil && *input.WireguardPort != config.WgPort {
|
||||||
|
log.Infof("updating Wireguard port %d (old value %d)",
|
||||||
|
*input.WireguardPort, config.WgPort)
|
||||||
|
config.WgPort = *input.WireguardPort
|
||||||
|
updated = true
|
||||||
|
} else if config.WgPort == 0 {
|
||||||
|
config.WgPort = iface.DefaultWgPort
|
||||||
|
log.Infof("using default Wireguard port %d", config.WgPort)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.InterfaceName != nil && *input.InterfaceName != config.WgIface {
|
||||||
|
log.Infof("updating Wireguard interface %#v (old value %#v)",
|
||||||
|
*input.InterfaceName, config.WgIface)
|
||||||
|
config.WgIface = *input.InterfaceName
|
||||||
|
updated = true
|
||||||
|
} else if config.WgIface == "" {
|
||||||
|
config.WgIface = iface.WgInterfaceDefault
|
||||||
|
log.Infof("using default Wireguard interface %s", config.WgIface)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.NATExternalIPs != nil && !reflect.DeepEqual(config.NATExternalIPs, input.NATExternalIPs) {
|
||||||
|
log.Infof("updating NAT External IP [ %s ] (old value: [ %s ])",
|
||||||
|
strings.Join(input.NATExternalIPs, " "),
|
||||||
|
strings.Join(config.NATExternalIPs, " "))
|
||||||
|
config.NATExternalIPs = input.NATExternalIPs
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.PreSharedKey != nil && *input.PreSharedKey != config.PreSharedKey {
|
||||||
|
log.Infof("new pre-shared key provided, replacing old key")
|
||||||
|
config.PreSharedKey = *input.PreSharedKey
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.RosenpassEnabled != nil && *input.RosenpassEnabled != config.RosenpassEnabled {
|
||||||
|
log.Infof("switching Rosenpass to %t", *input.RosenpassEnabled)
|
||||||
|
config.RosenpassEnabled = *input.RosenpassEnabled
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.RosenpassPermissive != nil && *input.RosenpassPermissive != config.RosenpassPermissive {
|
||||||
|
log.Infof("switching Rosenpass permissive to %t", *input.RosenpassPermissive)
|
||||||
|
config.RosenpassPermissive = *input.RosenpassPermissive
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.NetworkMonitor != nil && input.NetworkMonitor != config.NetworkMonitor {
|
||||||
|
log.Infof("switching Network Monitor to %t", *input.NetworkMonitor)
|
||||||
|
config.NetworkMonitor = input.NetworkMonitor
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.NetworkMonitor == nil {
|
||||||
|
// enable network monitoring by default on windows and darwin clients
|
||||||
|
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||||
|
enabled := true
|
||||||
|
config.NetworkMonitor = &enabled
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.CustomDNSAddress != nil && string(input.CustomDNSAddress) != config.CustomDNSAddress {
|
||||||
|
log.Infof("updating custom DNS address %#v (old value %#v)",
|
||||||
|
string(input.CustomDNSAddress), config.CustomDNSAddress)
|
||||||
|
config.CustomDNSAddress = string(input.CustomDNSAddress)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.IFaceBlackList) == 0 {
|
||||||
|
log.Infof("filling in interface blacklist with defaults: [ %s ]",
|
||||||
|
strings.Join(defaultInterfaceBlacklist, " "))
|
||||||
|
config.IFaceBlackList = append(config.IFaceBlackList, defaultInterfaceBlacklist...)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(input.ExtraIFaceBlackList) > 0 {
|
||||||
|
for _, iFace := range util.SliceDiff(input.ExtraIFaceBlackList, config.IFaceBlackList) {
|
||||||
|
log.Infof("adding new entry to interface blacklist: %s", iFace)
|
||||||
|
config.IFaceBlackList = append(config.IFaceBlackList, iFace)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.DisableAutoConnect != nil && *input.DisableAutoConnect != config.DisableAutoConnect {
|
||||||
|
if *input.DisableAutoConnect {
|
||||||
|
log.Infof("turning off automatic connection on startup")
|
||||||
|
} else {
|
||||||
|
log.Infof("enabling automatic connection on startup")
|
||||||
|
}
|
||||||
|
config.DisableAutoConnect = *input.DisableAutoConnect
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.ServerSSHAllowed != nil && *input.ServerSSHAllowed != *config.ServerSSHAllowed {
|
||||||
|
if *input.ServerSSHAllowed {
|
||||||
|
log.Infof("enabling SSH server")
|
||||||
|
} else {
|
||||||
|
log.Infof("disabling SSH server")
|
||||||
|
}
|
||||||
|
config.ServerSSHAllowed = input.ServerSSHAllowed
|
||||||
|
updated = true
|
||||||
|
} else if config.ServerSSHAllowed == nil {
|
||||||
|
// enables SSH for configs from old versions to preserve backwards compatibility
|
||||||
|
log.Infof("falling back to enabled SSH server for pre-existing configuration")
|
||||||
|
config.ServerSSHAllowed = util.True()
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.DNSRouteInterval != nil && *input.DNSRouteInterval != config.DNSRouteInterval {
|
||||||
|
log.Infof("updating DNS route interval to %s (old value %s)",
|
||||||
|
input.DNSRouteInterval.String(), config.DNSRouteInterval.String())
|
||||||
|
config.DNSRouteInterval = *input.DNSRouteInterval
|
||||||
|
updated = true
|
||||||
|
} else if config.DNSRouteInterval == 0 {
|
||||||
|
config.DNSRouteInterval = dynamic.DefaultInterval
|
||||||
|
log.Infof("using default DNS route interval %s", config.DNSRouteInterval)
|
||||||
|
updated = true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
// parseURL parses and validates a service URL
|
// parseURL parses and validates a service URL
|
||||||
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
|
func parseURL(serviceName, serviceURL string) (*url.URL, error) {
|
||||||
parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
|
parsedMgmtURL, err := url.ParseRequestURI(serviceURL)
|
||||||
@@ -273,9 +405,9 @@ func parseURL(serviceName, serviceURL string) (*url.URL, error) {
|
|||||||
if parsedMgmtURL.Port() == "" {
|
if parsedMgmtURL.Port() == "" {
|
||||||
switch parsedMgmtURL.Scheme {
|
switch parsedMgmtURL.Scheme {
|
||||||
case "https":
|
case "https":
|
||||||
parsedMgmtURL.Host = parsedMgmtURL.Host + ":443"
|
parsedMgmtURL.Host += ":443"
|
||||||
case "http":
|
case "http":
|
||||||
parsedMgmtURL.Host = parsedMgmtURL.Host + ":80"
|
parsedMgmtURL.Host += ":80"
|
||||||
default:
|
default:
|
||||||
log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL)
|
log.Infof("unable to determine a default port for schema %s in URL %s", parsedMgmtURL.Scheme, serviceURL)
|
||||||
}
|
}
|
||||||
@@ -305,3 +437,85 @@ func configFileIsExists(path string) bool {
|
|||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
return !os.IsNotExist(err)
|
return !os.IsNotExist(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateOldManagementURL checks whether client can switch to the new Management URL with port 443 and the management domain.
|
||||||
|
// 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 UpdateOldManagementURL(ctx context.Context, config *Config, configPath string) (*Config, error) {
|
||||||
|
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedOldDefaultManagementURL, err := parseURL("Management URL", oldDefaultManagementURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ManagementURL.Hostname() != defaultManagementURL.Hostname() &&
|
||||||
|
config.ManagementURL.Hostname() != parsedOldDefaultManagementURL.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 config.ManagementURL.Port() != managementLegacyPortString &&
|
||||||
|
config.ManagementURL.Hostname() == defaultManagementURL.Hostname() {
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newURL, err := parseURL("Management URL", fmt.Sprintf("%s://%s:%d",
|
||||||
|
config.ManagementURL.Scheme, defaultManagementURL.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 := UpdateConfig(ConfigInput{
|
||||||
|
ManagementURL: newURL.String(),
|
||||||
|
ConfigPath: configPath,
|
||||||
|
})
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/util"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetConfig(t *testing.T) {
|
func TestGetConfig(t *testing.T) {
|
||||||
@@ -15,7 +18,6 @@ func TestGetConfig(t *testing.T) {
|
|||||||
config, err := UpdateOrCreateConfig(ConfigInput{
|
config, err := UpdateOrCreateConfig(ConfigInput{
|
||||||
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
ConfigPath: filepath.Join(t.TempDir(), "config.json"),
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -23,9 +25,6 @@ func TestGetConfig(t *testing.T) {
|
|||||||
assert.Equal(t, config.ManagementURL.String(), DefaultManagementURL)
|
assert.Equal(t, config.ManagementURL.String(), DefaultManagementURL)
|
||||||
assert.Equal(t, config.AdminURL.String(), DefaultAdminURL)
|
assert.Equal(t, config.AdminURL.String(), DefaultAdminURL)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
managementURL := "https://test.management.url:33071"
|
managementURL := "https://test.management.url:33071"
|
||||||
adminURL := "https://app.admin.url:443"
|
adminURL := "https://app.admin.url:443"
|
||||||
path := filepath.Join(t.TempDir(), "config.json")
|
path := filepath.Join(t.TempDir(), "config.json")
|
||||||
@@ -63,22 +62,7 @@ func TestGetConfig(t *testing.T) {
|
|||||||
assert.Equal(t, config.ManagementURL.String(), managementURL)
|
assert.Equal(t, config.ManagementURL.String(), managementURL)
|
||||||
assert.Equal(t, config.PreSharedKey, preSharedKey)
|
assert.Equal(t, config.PreSharedKey, preSharedKey)
|
||||||
|
|
||||||
// case 4: new empty pre-shared key config -> fetch it
|
// case 4: existing config, but new managementURL has been provided -> update config
|
||||||
newPreSharedKey := ""
|
|
||||||
config, err = UpdateOrCreateConfig(ConfigInput{
|
|
||||||
ManagementURL: managementURL,
|
|
||||||
AdminURL: adminURL,
|
|
||||||
ConfigPath: path,
|
|
||||||
PreSharedKey: &newPreSharedKey,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, config.ManagementURL.String(), managementURL)
|
|
||||||
assert.Equal(t, config.PreSharedKey, preSharedKey)
|
|
||||||
|
|
||||||
// case 5: existing config, but new managementURL has been provided -> update config
|
|
||||||
newManagementURL := "https://test.newManagement.url:33071"
|
newManagementURL := "https://test.newManagement.url:33071"
|
||||||
config, err = UpdateOrCreateConfig(ConfigInput{
|
config, err = UpdateOrCreateConfig(ConfigInput{
|
||||||
ManagementURL: newManagementURL,
|
ManagementURL: newManagementURL,
|
||||||
@@ -101,6 +85,26 @@ func TestGetConfig(t *testing.T) {
|
|||||||
assert.Equal(t, readConf.(*Config).ManagementURL.String(), newManagementURL)
|
assert.Equal(t, readConf.(*Config).ManagementURL.String(), newManagementURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtraIFaceBlackList(t *testing.T) {
|
||||||
|
extraIFaceBlackList := []string{"eth1"}
|
||||||
|
path := filepath.Join(t.TempDir(), "config.json")
|
||||||
|
config, err := UpdateOrCreateConfig(ConfigInput{
|
||||||
|
ConfigPath: path,
|
||||||
|
ExtraIFaceBlackList: extraIFaceBlackList,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Contains(t, config.IFaceBlackList, "eth1")
|
||||||
|
readConf, err := util.ReadJson(path, config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Contains(t, readConf.(*Config).IFaceBlackList, "eth1")
|
||||||
|
}
|
||||||
|
|
||||||
func TestHiddenPreSharedKey(t *testing.T) {
|
func TestHiddenPreSharedKey(t *testing.T) {
|
||||||
hidden := "**********"
|
hidden := "**********"
|
||||||
samplePreSharedKey := "mysecretpresharedkey"
|
samplePreSharedKey := "mysecretpresharedkey"
|
||||||
@@ -126,7 +130,6 @@ func TestHiddenPreSharedKey(t *testing.T) {
|
|||||||
ConfigPath: cfgFile,
|
ConfigPath: cfgFile,
|
||||||
PreSharedKey: tt.preSharedKey,
|
PreSharedKey: tt.preSharedKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to get cfg: %s", err)
|
t.Fatalf("failed to get cfg: %s", err)
|
||||||
}
|
}
|
||||||
@@ -137,3 +140,60 @@ func TestHiddenPreSharedKey(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateOldManagementURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
previousManagementURL string
|
||||||
|
expectedManagementURL string
|
||||||
|
fileShouldNotChange bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Update old management URL with legacy port",
|
||||||
|
previousManagementURL: "https://api.wiretrustee.com:33073",
|
||||||
|
expectedManagementURL: DefaultManagementURL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update old management URL",
|
||||||
|
previousManagementURL: oldDefaultManagementURL,
|
||||||
|
expectedManagementURL: DefaultManagementURL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No update needed when management URL is up to date",
|
||||||
|
previousManagementURL: DefaultManagementURL,
|
||||||
|
expectedManagementURL: DefaultManagementURL,
|
||||||
|
fileShouldNotChange: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No update needed when not using cloud management",
|
||||||
|
previousManagementURL: "https://netbird.example.com:33073",
|
||||||
|
expectedManagementURL: "https://netbird.example.com:33073",
|
||||||
|
fileShouldNotChange: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tempDir, "config.json")
|
||||||
|
config, err := UpdateOrCreateConfig(ConfigInput{
|
||||||
|
ManagementURL: tt.previousManagementURL,
|
||||||
|
ConfigPath: configPath,
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to create testing config")
|
||||||
|
previousStats, err := os.Stat(configPath)
|
||||||
|
require.NoError(t, err, "failed to create testing config stats")
|
||||||
|
resultConfig, err := UpdateOldManagementURL(context.TODO(), config, configPath)
|
||||||
|
require.NoError(t, err, "got error when updating old management url")
|
||||||
|
require.Equal(t, tt.expectedManagementURL, resultConfig.ManagementURL.String())
|
||||||
|
newStats, err := os.Stat(configPath)
|
||||||
|
require.NoError(t, err, "failed to create testing config stats")
|
||||||
|
switch tt.fileShouldNotChange {
|
||||||
|
case true:
|
||||||
|
require.Equal(t, previousStats.ModTime(), newStats.ModTime(), "file should not change")
|
||||||
|
case false:
|
||||||
|
require.NotEqual(t, previousStats.ModTime(), newStats.ModTime(), "file should have changed")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ package internal
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cenkalti/backoff/v4"
|
"github.com/cenkalti/backoff/v4"
|
||||||
@@ -13,8 +18,8 @@ import (
|
|||||||
gstatus "google.golang.org/grpc/status"
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/client/ssh"
|
"github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
@@ -22,27 +27,103 @@ import (
|
|||||||
mgm "github.com/netbirdio/netbird/management/client"
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
signal "github.com/netbirdio/netbird/signal/client"
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RunClient with main logic.
|
type ConnectClient struct {
|
||||||
func RunClient(ctx context.Context, config *Config, statusRecorder *peer.Status) error {
|
ctx context.Context
|
||||||
return runClient(ctx, config, statusRecorder, MobileDependency{})
|
config *Config
|
||||||
|
statusRecorder *peer.Status
|
||||||
|
engine *Engine
|
||||||
|
engineMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunClientMobile with main logic on mobile system
|
func NewConnectClient(
|
||||||
func RunClientMobile(ctx context.Context, config *Config, statusRecorder *peer.Status, tunAdapter iface.TunAdapter, iFaceDiscover stdnet.ExternalIFaceDiscover, routeListener routemanager.RouteListener, dnsAddresses []string, dnsReadyListener dns.ReadyListener) error {
|
ctx context.Context,
|
||||||
|
config *Config,
|
||||||
|
statusRecorder *peer.Status,
|
||||||
|
|
||||||
|
) *ConnectClient {
|
||||||
|
return &ConnectClient{
|
||||||
|
ctx: ctx,
|
||||||
|
config: config,
|
||||||
|
statusRecorder: statusRecorder,
|
||||||
|
engineMutex: sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run with main logic.
|
||||||
|
func (c *ConnectClient) Run() error {
|
||||||
|
return c.run(MobileDependency{}, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunWithProbes runs the client's main logic with probes attached
|
||||||
|
func (c *ConnectClient) RunWithProbes(
|
||||||
|
mgmProbe *Probe,
|
||||||
|
signalProbe *Probe,
|
||||||
|
relayProbe *Probe,
|
||||||
|
wgProbe *Probe,
|
||||||
|
) error {
|
||||||
|
return c.run(MobileDependency{}, mgmProbe, signalProbe, relayProbe, wgProbe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunOnAndroid with main logic on mobile system
|
||||||
|
func (c *ConnectClient) RunOnAndroid(
|
||||||
|
tunAdapter iface.TunAdapter,
|
||||||
|
iFaceDiscover stdnet.ExternalIFaceDiscover,
|
||||||
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
|
dnsAddresses []string,
|
||||||
|
dnsReadyListener dns.ReadyListener,
|
||||||
|
) error {
|
||||||
// in case of non Android os these variables will be nil
|
// in case of non Android os these variables will be nil
|
||||||
mobileDependency := MobileDependency{
|
mobileDependency := MobileDependency{
|
||||||
TunAdapter: tunAdapter,
|
TunAdapter: tunAdapter,
|
||||||
IFaceDiscover: iFaceDiscover,
|
IFaceDiscover: iFaceDiscover,
|
||||||
RouteListener: routeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
HostDNSAddresses: dnsAddresses,
|
HostDNSAddresses: dnsAddresses,
|
||||||
DnsReadyListener: dnsReadyListener,
|
DnsReadyListener: dnsReadyListener,
|
||||||
}
|
}
|
||||||
return runClient(ctx, config, statusRecorder, mobileDependency)
|
return c.run(mobileDependency, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnectClient) RunOniOS(
|
||||||
|
fileDescriptor int32,
|
||||||
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
|
dnsManager dns.IosDnsManager,
|
||||||
|
) error {
|
||||||
|
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
||||||
|
debug.SetGCPercent(5)
|
||||||
|
|
||||||
|
mobileDependency := MobileDependency{
|
||||||
|
FileDescriptor: fileDescriptor,
|
||||||
|
NetworkChangeListener: networkChangeListener,
|
||||||
|
DnsManager: dnsManager,
|
||||||
|
}
|
||||||
|
return c.run(mobileDependency, nil, nil, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnectClient) run(
|
||||||
|
mobileDependency MobileDependency,
|
||||||
|
mgmProbe *Probe,
|
||||||
|
signalProbe *Probe,
|
||||||
|
relayProbe *Probe,
|
||||||
|
wgProbe *Probe,
|
||||||
|
) error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Panicf("Panic occurred: %v, stack trace: %s", r, string(debug.Stack()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("starting NetBird client version %s on %s/%s", version.NetbirdVersion(), runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
// Check if client was not shut down in a clean way and restore DNS config if required.
|
||||||
|
// Otherwise, we might not be able to connect to the management server to retrieve new config.
|
||||||
|
if err := dns.CheckUncleanShutdown(c.config.WgIface); err != nil {
|
||||||
|
log.Errorf("checking unclean shutdown error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status, mobileDependency MobileDependency) error {
|
|
||||||
backOff := &backoff.ExponentialBackOff{
|
backOff := &backoff.ExponentialBackOff{
|
||||||
InitialInterval: time.Second,
|
InitialInterval: time.Second,
|
||||||
RandomizationFactor: 1,
|
RandomizationFactor: 1,
|
||||||
@@ -53,7 +134,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
Clock: backoff.SystemClock,
|
Clock: backoff.SystemClock,
|
||||||
}
|
}
|
||||||
|
|
||||||
state := CtxGetState(ctx)
|
state := CtxGetState(c.ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
s, err := state.Status()
|
s, err := state.Status()
|
||||||
if err != nil || s != StatusNeedsLogin {
|
if err != nil || s != StatusNeedsLogin {
|
||||||
@@ -62,49 +143,49 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
wrapErr := state.Wrap
|
wrapErr := state.Wrap
|
||||||
myPrivateKey, err := wgtypes.ParseKey(config.PrivateKey)
|
myPrivateKey, err := wgtypes.ParseKey(c.config.PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed parsing Wireguard key %s: [%s]", config.PrivateKey, err.Error())
|
log.Errorf("failed parsing Wireguard key %s: [%s]", c.config.PrivateKey, err.Error())
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var mgmTlsEnabled bool
|
var mgmTlsEnabled bool
|
||||||
if config.ManagementURL.Scheme == "https" {
|
if c.config.ManagementURL.Scheme == "https" {
|
||||||
mgmTlsEnabled = true
|
mgmTlsEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
publicSSHKey, err := ssh.GeneratePublicKey([]byte(config.SSHKey))
|
publicSSHKey, err := ssh.GeneratePublicKey([]byte(c.config.SSHKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer statusRecorder.ClientStop()
|
defer c.statusRecorder.ClientStop()
|
||||||
operation := func() error {
|
operation := func() error {
|
||||||
// if context cancelled we not start new backoff cycle
|
// if context cancelled we not start new backoff cycle
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-c.ctx.Done():
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Set(StatusConnecting)
|
state.Set(StatusConnecting)
|
||||||
|
|
||||||
engineCtx, cancel := context.WithCancel(ctx)
|
engineCtx, cancel := context.WithCancel(c.ctx)
|
||||||
defer func() {
|
defer func() {
|
||||||
statusRecorder.MarkManagementDisconnected()
|
c.statusRecorder.MarkManagementDisconnected(state.err)
|
||||||
statusRecorder.CleanLocalPeerState()
|
c.statusRecorder.CleanLocalPeerState()
|
||||||
cancel()
|
cancel()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Debugf("conecting to the Management service %s", config.ManagementURL.Host)
|
log.Debugf("connecting to the Management service %s", c.config.ManagementURL.Host)
|
||||||
mgmClient, err := mgm.NewClient(engineCtx, config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
mgmClient, err := mgm.NewClient(engineCtx, c.config.ManagementURL.Host, myPrivateKey, mgmTlsEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
|
return wrapErr(gstatus.Errorf(codes.FailedPrecondition, "failed connecting to Management Service : %s", err))
|
||||||
}
|
}
|
||||||
mgmNotifier := statusRecorderToMgmConnStateNotifier(statusRecorder)
|
mgmNotifier := statusRecorderToMgmConnStateNotifier(c.statusRecorder)
|
||||||
mgmClient.SetConnStateListener(mgmNotifier)
|
mgmClient.SetConnStateListener(mgmNotifier)
|
||||||
|
|
||||||
log.Debugf("connected to the Management service %s", config.ManagementURL.Host)
|
log.Debugf("connected to the Management service %s", c.config.ManagementURL.Host)
|
||||||
defer func() {
|
defer func() {
|
||||||
err = mgmClient.Close()
|
err = mgmClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,7 +203,7 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
}
|
}
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
statusRecorder.MarkManagementConnected()
|
c.statusRecorder.MarkManagementConnected()
|
||||||
|
|
||||||
localPeerState := peer.LocalPeerState{
|
localPeerState := peer.LocalPeerState{
|
||||||
IP: loginResp.GetPeerConfig().GetAddress(),
|
IP: loginResp.GetPeerConfig().GetAddress(),
|
||||||
@@ -131,17 +212,19 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
FQDN: loginResp.GetPeerConfig().GetFqdn(),
|
FQDN: loginResp.GetPeerConfig().GetFqdn(),
|
||||||
}
|
}
|
||||||
|
|
||||||
statusRecorder.UpdateLocalPeerState(localPeerState)
|
c.statusRecorder.UpdateLocalPeerState(localPeerState)
|
||||||
|
|
||||||
signalURL := fmt.Sprintf("%s://%s",
|
signalURL := fmt.Sprintf("%s://%s",
|
||||||
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
|
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
|
||||||
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
|
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
|
||||||
)
|
)
|
||||||
|
|
||||||
statusRecorder.UpdateSignalAddress(signalURL)
|
c.statusRecorder.UpdateSignalAddress(signalURL)
|
||||||
|
|
||||||
statusRecorder.MarkSignalDisconnected()
|
c.statusRecorder.MarkSignalDisconnected(nil)
|
||||||
defer statusRecorder.MarkSignalDisconnected()
|
defer func() {
|
||||||
|
c.statusRecorder.MarkSignalDisconnected(state.err)
|
||||||
|
}()
|
||||||
|
|
||||||
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
|
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
|
||||||
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
||||||
@@ -156,37 +239,40 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
signalNotifier := statusRecorderToSignalConnStateNotifier(statusRecorder)
|
signalNotifier := statusRecorderToSignalConnStateNotifier(c.statusRecorder)
|
||||||
signalClient.SetConnStateListener(signalNotifier)
|
signalClient.SetConnStateListener(signalNotifier)
|
||||||
|
|
||||||
statusRecorder.MarkSignalConnected()
|
c.statusRecorder.MarkSignalConnected()
|
||||||
|
|
||||||
peerConfig := loginResp.GetPeerConfig()
|
peerConfig := loginResp.GetPeerConfig()
|
||||||
|
|
||||||
engineConfig, err := createEngineConfig(myPrivateKey, config, peerConfig)
|
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
engine := NewEngine(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, statusRecorder)
|
checks := loginResp.GetChecks()
|
||||||
err = engine.Start()
|
|
||||||
|
c.engineMutex.Lock()
|
||||||
|
c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, engineConfig, mobileDependency, c.statusRecorder, mgmProbe, signalProbe, relayProbe, wgProbe, checks)
|
||||||
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
|
err = c.engine.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
log.Errorf("error while starting Netbird Connection Engine: %s", err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Print("Netbird engine started, my IP is: ", peerConfig.Address)
|
log.Infof("Netbird engine started, the IP is: %s", peerConfig.GetAddress())
|
||||||
state.Set(StatusConnected)
|
state.Set(StatusConnected)
|
||||||
|
|
||||||
statusRecorder.ClientStart()
|
|
||||||
|
|
||||||
<-engineCtx.Done()
|
<-engineCtx.Done()
|
||||||
statusRecorder.ClientTeardown()
|
c.statusRecorder.ClientTeardown()
|
||||||
|
|
||||||
backOff.Reset()
|
backOff.Reset()
|
||||||
|
|
||||||
err = engine.Stop()
|
err = c.engine.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed stopping engine %v", err)
|
log.Errorf("failed stopping engine %v", err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
@@ -194,13 +280,14 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
|
|
||||||
log.Info("stopped NetBird client")
|
log.Info("stopped NetBird client")
|
||||||
|
|
||||||
if _, err := state.Status(); err == ErrResetConnection {
|
if _, err := state.Status(); errors.Is(err, ErrResetConnection) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.statusRecorder.ClientStart()
|
||||||
err = backoff.Retry(operation, backOff)
|
err = backoff.Retry(operation, backOff)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
log.Debugf("exiting client retry loop due to unrecoverable error: %s", err)
|
||||||
@@ -212,8 +299,20 @@ func runClient(ctx context.Context, config *Config, statusRecorder *peer.Status,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *ConnectClient) Engine() *Engine {
|
||||||
|
var e *Engine
|
||||||
|
c.engineMutex.Lock()
|
||||||
|
e = c.engine
|
||||||
|
c.engineMutex.Unlock()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
// createEngineConfig converts configuration received from Management Service to EngineConfig
|
// createEngineConfig converts configuration received from Management Service to EngineConfig
|
||||||
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
|
func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.PeerConfig) (*EngineConfig, error) {
|
||||||
|
nm := false
|
||||||
|
if config.NetworkMonitor != nil {
|
||||||
|
nm = *config.NetworkMonitor
|
||||||
|
}
|
||||||
engineConf := &EngineConfig{
|
engineConf := &EngineConfig{
|
||||||
WgIfaceName: config.WgIface,
|
WgIfaceName: config.WgIface,
|
||||||
WgAddr: peerConfig.Address,
|
WgAddr: peerConfig.Address,
|
||||||
@@ -221,9 +320,14 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
|
|||||||
DisableIPv6Discovery: config.DisableIPv6Discovery,
|
DisableIPv6Discovery: config.DisableIPv6Discovery,
|
||||||
WgPrivateKey: key,
|
WgPrivateKey: key,
|
||||||
WgPort: config.WgPort,
|
WgPort: config.WgPort,
|
||||||
|
NetworkMonitor: nm,
|
||||||
SSHKey: []byte(config.SSHKey),
|
SSHKey: []byte(config.SSHKey),
|
||||||
NATExternalIPs: config.NATExternalIPs,
|
NATExternalIPs: config.NATExternalIPs,
|
||||||
CustomDNSAddress: config.CustomDNSAddress,
|
CustomDNSAddress: config.CustomDNSAddress,
|
||||||
|
RosenpassEnabled: config.RosenpassEnabled,
|
||||||
|
RosenpassPermissive: config.RosenpassPermissive,
|
||||||
|
ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed),
|
||||||
|
DNSRouteInterval: config.DNSRouteInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.PreSharedKey != "" {
|
if config.PreSharedKey != "" {
|
||||||
@@ -234,6 +338,15 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
|
|||||||
engineConf.PreSharedKey = &preSharedKey
|
engineConf.PreSharedKey = &preSharedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
port, err := freePort(config.WgPort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if port != config.WgPort {
|
||||||
|
log.Infof("using %d as wireguard port: %d is in use", port, config.WgPort)
|
||||||
|
}
|
||||||
|
engineConf.WgPort = port
|
||||||
|
|
||||||
return engineConf, nil
|
return engineConf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,83 +385,6 @@ func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte)
|
|||||||
return loginResp, nil
|
return loginResp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
|
||||||
|
|
||||||
defaultManagementURL, err := parseURL("Management URL", DefaultManagementURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ManagementURL.Hostname() != defaultManagementURL.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 := UpdateConfig(ConfigInput{
|
|
||||||
ManagementURL: newURL.String(),
|
|
||||||
ConfigPath: configPath,
|
|
||||||
})
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func statusRecorderToMgmConnStateNotifier(statusRecorder *peer.Status) mgm.ConnStateNotifier {
|
func statusRecorderToMgmConnStateNotifier(statusRecorder *peer.Status) mgm.ConnStateNotifier {
|
||||||
var sri interface{} = statusRecorder
|
var sri interface{} = statusRecorder
|
||||||
mgmNotifier, _ := sri.(mgm.ConnStateNotifier)
|
mgmNotifier, _ := sri.(mgm.ConnStateNotifier)
|
||||||
@@ -360,3 +396,20 @@ func statusRecorderToSignalConnStateNotifier(statusRecorder *peer.Status) signal
|
|||||||
notifier, _ := sri.(signal.ConnStateNotifier)
|
notifier, _ := sri.(signal.ConnStateNotifier)
|
||||||
return notifier
|
return notifier
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func freePort(start int) (int, error) {
|
||||||
|
addr := net.UDPAddr{}
|
||||||
|
if start == 0 {
|
||||||
|
start = iface.DefaultWgPort
|
||||||
|
}
|
||||||
|
for x := start; x <= 65535; x++ {
|
||||||
|
addr.Port = x
|
||||||
|
conn, err := net.ListenUDP("udp", &addr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
return 0, errors.New("no free ports")
|
||||||
|
}
|
||||||
|
|||||||
57
client/internal/connect_test.go
Normal file
57
client/internal/connect_test.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_freePort(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
port int
|
||||||
|
want int
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "available",
|
||||||
|
port: 51820,
|
||||||
|
want: 51820,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "notavailable",
|
||||||
|
port: 51830,
|
||||||
|
want: 51831,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noports",
|
||||||
|
port: 65535,
|
||||||
|
want: 0,
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
|
||||||
|
c1, err := net.ListenUDP("udp", &net.UDPAddr{Port: 51830})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("freePort error = %v", err)
|
||||||
|
}
|
||||||
|
c2, err := net.ListenUDP("udp", &net.UDPAddr{Port: 65535})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("freePort error = %v", err)
|
||||||
|
}
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := freePort(tt.port)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("freePort() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("freePort() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
c1.Close()
|
||||||
|
c2.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
6
client/internal/dns/consts_freebsd.go
Normal file
6
client/internal/dns/consts_freebsd.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileUncleanShutdownResolvConfLocation = "/var/db/netbird/resolv.conf"
|
||||||
|
fileUncleanShutdownManagerTypeLocation = "/var/db/netbird/manager"
|
||||||
|
)
|
||||||
8
client/internal/dns/consts_linux.go
Normal file
8
client/internal/dns/consts_linux.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !android
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileUncleanShutdownResolvConfLocation = "/var/lib/netbird/resolv.conf"
|
||||||
|
fileUncleanShutdownManagerTypeLocation = "/var/lib/netbird/manager"
|
||||||
|
)
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
//go:build !android
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const dbusDefaultFlag = 0
|
const dbusDefaultFlag = 0
|
||||||
@@ -14,6 +16,7 @@ const dbusDefaultFlag = 0
|
|||||||
func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
|
func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
|
||||||
obj, closeConn, err := getDbusObject(dest, path)
|
obj, closeConn, err := getDbusObject(dest, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Tracef("error getting dbus object: %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer closeConn()
|
defer closeConn()
|
||||||
@@ -21,14 +24,18 @@ func isDbusListenerRunning(dest string, path dbus.ObjectPath) bool {
|
|||||||
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.TODO(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store()
|
if err = obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0).Store(); err != nil {
|
||||||
return err == nil
|
log.Tracef("error calling dbus: %s", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDbusObject(dest string, path dbus.ObjectPath) (dbus.BusObject, func(), error) {
|
func getDbusObject(dest string, path dbus.ObjectPath) (dbus.BusObject, func(), error) {
|
||||||
conn, err := dbus.SystemBus()
|
conn, err := dbus.SystemBus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, fmt.Errorf("get dbus: %w", err)
|
||||||
}
|
}
|
||||||
obj := conn.Object(dest, path)
|
obj := conn.Object(dest, path)
|
||||||
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
//go:build !android
|
|
||||||
|
|
||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
|
|
||||||
fileGeneratedResolvConfSearchBeginContent = "search "
|
|
||||||
fileGeneratedResolvConfContentFormat = fileGeneratedResolvConfContentHeader +
|
|
||||||
"\n# If needed you can restore the original file by copying back %s\n\nnameserver %s\n" +
|
|
||||||
fileGeneratedResolvConfSearchBeginContent + "%s\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
|
|
||||||
fileMaxLineCharsLimit = 256
|
|
||||||
fileMaxNumberOfSearchDomains = 6
|
|
||||||
)
|
|
||||||
|
|
||||||
var fileSearchLineBeginCharCount = len(fileGeneratedResolvConfSearchBeginContent)
|
|
||||||
|
|
||||||
type fileConfigurator struct {
|
|
||||||
originalPerms os.FileMode
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFileConfigurator() (hostManager, error) {
|
|
||||||
return &fileConfigurator{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileConfigurator) supportCustomPort() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
|
||||||
backupFileExist := false
|
|
||||||
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
|
|
||||||
if err == nil {
|
|
||||||
backupFileExist = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.routeAll {
|
|
||||||
if backupFileExist {
|
|
||||||
err = f.restore()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to configure DNS for this peer using file manager without a Primary nameserver group. Restoring the original file return err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
|
|
||||||
}
|
|
||||||
managerType, err := getOSDNSManagerType()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
switch managerType {
|
|
||||||
case fileManager, netbirdManager:
|
|
||||||
if !backupFileExist {
|
|
||||||
err = f.backup()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("unable to backup the resolv.conf file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// todo improve this and maybe restart DNS manager from scratch
|
|
||||||
return fmt.Errorf("something happened and file manager is not your prefered host dns configurator, restart the agent")
|
|
||||||
}
|
|
||||||
|
|
||||||
var searchDomains string
|
|
||||||
appendedDomains := 0
|
|
||||||
for _, dConf := range config.domains {
|
|
||||||
if dConf.matchOnly || dConf.disabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if appendedDomains >= fileMaxNumberOfSearchDomains {
|
|
||||||
// lets log all skipped domains
|
|
||||||
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, dConf.domain)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fileSearchLineBeginCharCount+len(searchDomains) > fileMaxLineCharsLimit {
|
|
||||||
// lets log all skipped domains
|
|
||||||
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, dConf.domain)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
searchDomains += " " + dConf.domain
|
|
||||||
appendedDomains++
|
|
||||||
}
|
|
||||||
content := fmt.Sprintf(fileGeneratedResolvConfContentFormat, fileDefaultResolvConfBackupLocation, config.serverIP, searchDomains)
|
|
||||||
err = writeDNSConfig(content, defaultResolvConfPath, f.originalPerms)
|
|
||||||
if err != nil {
|
|
||||||
err = f.restore()
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("attempt to restore default file failed with error: %s", err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Infof("created a NetBird managed %s file with your DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, appendedDomains, searchDomains)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileConfigurator) restoreHostDNS() error {
|
|
||||||
return f.restore()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileConfigurator) backup() error {
|
|
||||||
stats, err := os.Stat(defaultResolvConfPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("got an error while checking stats for %s file. Error: %s", defaultResolvConfPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.originalPerms = stats.Mode()
|
|
||||||
|
|
||||||
err = copyFile(defaultResolvConfPath, fileDefaultResolvConfBackupLocation)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("got error while backing up the %s file. Error: %s", defaultResolvConfPath, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fileConfigurator) restore() error {
|
|
||||||
err := copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("got error while restoring the %s file from %s. Error: %s", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeDNSConfig(content, fileName string, permissions os.FileMode) error {
|
|
||||||
log.Debugf("creating managed file %s", fileName)
|
|
||||||
var buf bytes.Buffer
|
|
||||||
buf.WriteString(content)
|
|
||||||
err := os.WriteFile(fileName, buf.Bytes(), permissions)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("got an creating resolver file %s. Error: %s", fileName, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFile(src, dest string) error {
|
|
||||||
stats, err := os.Stat(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("got an error while checking stats for %s file when copying it. Error: %s", src, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bytesRead, err := os.ReadFile(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("got an error while reading the file %s file for copy. Error: %s", src, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(dest, bytesRead, stats.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("got an writing the destination file %s for copy. Error: %s", dest, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
168
client/internal/dns/file_parser_unix.go
Normal file
168
client/internal/dns/file_parser_unix.go
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultResolvConfPath = "/etc/resolv.conf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var timeoutRegex = regexp.MustCompile(`timeout:\d+`)
|
||||||
|
var attemptsRegex = regexp.MustCompile(`attempts:\d+`)
|
||||||
|
|
||||||
|
type resolvConf struct {
|
||||||
|
nameServers []string
|
||||||
|
searchDomains []string
|
||||||
|
others []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resolvConf) String() string {
|
||||||
|
return fmt.Sprintf("search domains: %v, name servers: %v, others: %s", r.searchDomains, r.nameServers, r.others)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDefaultResolvConf() (*resolvConf, error) {
|
||||||
|
return parseResolvConfFile(defaultResolvConfPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBackupResolvConf() (*resolvConf, error) {
|
||||||
|
return parseResolvConfFile(fileDefaultResolvConfBackupLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseResolvConfFile(resolvConfFile string) (*resolvConf, error) {
|
||||||
|
rconf := &resolvConf{
|
||||||
|
searchDomains: make([]string, 0),
|
||||||
|
nameServers: make([]string, 0),
|
||||||
|
others: make([]string, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(resolvConfFile)
|
||||||
|
if err != nil {
|
||||||
|
return rconf, fmt.Errorf("failed to open %s file: %w", resolvConfFile, err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
log.Errorf("failed closing %s: %s", resolvConfFile, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cur, err := os.ReadFile(resolvConfFile)
|
||||||
|
if err != nil {
|
||||||
|
return rconf, fmt.Errorf("failed to read %s file: %w", resolvConfFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cur) == 0 {
|
||||||
|
return rconf, fmt.Errorf("file is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(cur), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "domain") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "options") && strings.Contains(line, "rotate") {
|
||||||
|
line = strings.ReplaceAll(line, "rotate", "")
|
||||||
|
splitLines := strings.Fields(line)
|
||||||
|
if len(splitLines) == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
line = strings.Join(splitLines, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "search") {
|
||||||
|
splitLines := strings.Fields(line)
|
||||||
|
if len(splitLines) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rconf.searchDomains = splitLines[1:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "nameserver") {
|
||||||
|
splitLines := strings.Fields(line)
|
||||||
|
if len(splitLines) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rconf.nameServers = append(rconf.nameServers, splitLines[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if line != "" {
|
||||||
|
rconf.others = append(rconf.others, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rconf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareOptionsWithTimeout appends timeout to existing options if it doesn't exist,
|
||||||
|
// otherwise it adds a new option with timeout and attempts.
|
||||||
|
func prepareOptionsWithTimeout(input []string, timeout int, attempts int) []string {
|
||||||
|
configs := make([]string, len(input))
|
||||||
|
copy(configs, input)
|
||||||
|
|
||||||
|
for i, config := range configs {
|
||||||
|
if strings.HasPrefix(config, "options") {
|
||||||
|
config = strings.ReplaceAll(config, "rotate", "")
|
||||||
|
config = strings.Join(strings.Fields(config), " ")
|
||||||
|
|
||||||
|
if strings.Contains(config, "timeout:") {
|
||||||
|
config = timeoutRegex.ReplaceAllString(config, fmt.Sprintf("timeout:%d", timeout))
|
||||||
|
} else {
|
||||||
|
config = strings.Replace(config, "options ", fmt.Sprintf("options timeout:%d ", timeout), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(config, "attempts:") {
|
||||||
|
config = attemptsRegex.ReplaceAllString(config, fmt.Sprintf("attempts:%d", attempts))
|
||||||
|
} else {
|
||||||
|
config = strings.Replace(config, "options ", fmt.Sprintf("options attempts:%d ", attempts), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
configs[i] = config
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(configs, fmt.Sprintf("options timeout:%d attempts:%d", timeout, attempts))
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeFirstNbNameserver removes the given nameserver from the given file if it is in the first position
|
||||||
|
// and writes the file back to the original location
|
||||||
|
func removeFirstNbNameserver(filename, nameserverIP string) error {
|
||||||
|
resolvConf, err := parseResolvConfFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse backup resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resolvConf.nameServers) > 1 && resolvConf.nameServers[0] == nameserverIP {
|
||||||
|
newContent := strings.Replace(string(content), fmt.Sprintf("nameserver %s\n", nameserverIP), "", 1)
|
||||||
|
|
||||||
|
stat, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stat %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filename, []byte(newContent), stat.Mode()); err != nil {
|
||||||
|
return fmt.Errorf("write %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
304
client/internal/dns/file_parser_unix_test.go
Normal file
304
client/internal/dns/file_parser_unix_test.go
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_parseResolvConf(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
input string
|
||||||
|
expectedSearch []string
|
||||||
|
expectedNS []string
|
||||||
|
expectedOther []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: `domain example.org
|
||||||
|
search example.org
|
||||||
|
nameserver 192.168.0.1
|
||||||
|
`,
|
||||||
|
expectedSearch: []string{"example.org"},
|
||||||
|
expectedNS: []string{"192.168.0.1"},
|
||||||
|
expectedOther: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).
|
||||||
|
# Do not edit.
|
||||||
|
#
|
||||||
|
# This file might be symlinked as /etc/resolv.conf. If you're looking at
|
||||||
|
# /etc/resolv.conf and seeing this text, you have followed the symlink.
|
||||||
|
#
|
||||||
|
# This is a dynamic resolv.conf file for connecting local clients directly to
|
||||||
|
# all known uplink DNS servers. This file lists all configured search domains.
|
||||||
|
#
|
||||||
|
# Third party programs should typically not access this file directly, but only
|
||||||
|
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
|
||||||
|
# different way, replace this symlink by a static file or a different symlink.
|
||||||
|
#
|
||||||
|
# See man:systemd-resolved.service(8) for details about the supported modes of
|
||||||
|
# operation for /etc/resolv.conf.
|
||||||
|
|
||||||
|
nameserver 192.168.2.1
|
||||||
|
nameserver 100.81.99.197
|
||||||
|
search netbird.cloud
|
||||||
|
`,
|
||||||
|
expectedSearch: []string{"netbird.cloud"},
|
||||||
|
expectedNS: []string{"192.168.2.1", "100.81.99.197"},
|
||||||
|
expectedOther: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).
|
||||||
|
# Do not edit.
|
||||||
|
#
|
||||||
|
# This file might be symlinked as /etc/resolv.conf. If you're looking at
|
||||||
|
# /etc/resolv.conf and seeing this text, you have followed the symlink.
|
||||||
|
#
|
||||||
|
# This is a dynamic resolv.conf file for connecting local clients directly to
|
||||||
|
# all known uplink DNS servers. This file lists all configured search domains.
|
||||||
|
#
|
||||||
|
# Third party programs should typically not access this file directly, but only
|
||||||
|
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
|
||||||
|
# different way, replace this symlink by a static file or a different symlink.
|
||||||
|
#
|
||||||
|
# See man:systemd-resolved.service(8) for details about the supported modes of
|
||||||
|
# operation for /etc/resolv.conf.
|
||||||
|
|
||||||
|
nameserver 192.168.2.1
|
||||||
|
nameserver 100.81.99.197
|
||||||
|
search netbird.cloud
|
||||||
|
options debug
|
||||||
|
`,
|
||||||
|
expectedSearch: []string{"netbird.cloud"},
|
||||||
|
expectedNS: []string{"192.168.2.1", "100.81.99.197"},
|
||||||
|
expectedOther: []string{"options debug"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run("test", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf")
|
||||||
|
err := os.WriteFile(tmpResolvConf, []byte(testCase.input), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cfg, err := parseResolvConfFile(tmpResolvConf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ok := compareLists(cfg.searchDomains, testCase.expectedSearch)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("invalid parse result for search domains, expected: %v, got: %v", testCase.expectedSearch, cfg.searchDomains)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = compareLists(cfg.nameServers, testCase.expectedNS)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("invalid parse result for ns domains, expected: %v, got: %v", testCase.expectedNS, cfg.nameServers)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = compareLists(cfg.others, testCase.expectedOther)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("invalid parse result for others, expected: %v, got: %v", testCase.expectedOther, cfg.others)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareLists(search []string, search2 []string) bool {
|
||||||
|
if len(search) != len(search2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, v := range search {
|
||||||
|
if v != search2[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_emptyFile(t *testing.T) {
|
||||||
|
cfg, err := parseResolvConfFile("/tmp/nothing")
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error, got nil")
|
||||||
|
}
|
||||||
|
if len(cfg.others) != 0 || len(cfg.searchDomains) != 0 || len(cfg.nameServers) != 0 {
|
||||||
|
t.Errorf("expected empty config, got %v", cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_symlink(t *testing.T) {
|
||||||
|
input := `# This is /run/systemd/resolve/resolv.conf managed by man:systemd-resolved(8).
|
||||||
|
# Do not edit.
|
||||||
|
#
|
||||||
|
# This file might be symlinked as /etc/resolv.conf. If you're looking at
|
||||||
|
# /etc/resolv.conf and seeing this text, you have followed the symlink.
|
||||||
|
#
|
||||||
|
# This is a dynamic resolv.conf file for connecting local clients directly to
|
||||||
|
# all known uplink DNS servers. This file lists all configured search domains.
|
||||||
|
#
|
||||||
|
# Third party programs should typically not access this file directly, but only
|
||||||
|
# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a
|
||||||
|
# different way, replace this symlink by a static file or a different symlink.
|
||||||
|
#
|
||||||
|
# See man:systemd-resolved.service(8) for details about the supported modes of
|
||||||
|
# operation for /etc/resolv.conf.
|
||||||
|
|
||||||
|
nameserver 192.168.0.1
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf")
|
||||||
|
err := os.WriteFile(tmpResolvConf, []byte(input), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpLink := filepath.Join(t.TempDir(), "symlink")
|
||||||
|
err = os.Symlink(tmpResolvConf, tmpLink)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := parseResolvConfFile(tmpLink)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.nameServers) != 1 {
|
||||||
|
t.Errorf("unexpected resolv.conf content: %v", cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareOptionsWithTimeout(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
others []string
|
||||||
|
timeout int
|
||||||
|
attempts int
|
||||||
|
expected []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Append new options with timeout and attempts",
|
||||||
|
others: []string{"some config"},
|
||||||
|
timeout: 2,
|
||||||
|
attempts: 2,
|
||||||
|
expected: []string{"some config", "options timeout:2 attempts:2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Modify existing options to exclude rotate and include timeout and attempts",
|
||||||
|
others: []string{"some config", "options rotate someother"},
|
||||||
|
timeout: 3,
|
||||||
|
attempts: 2,
|
||||||
|
expected: []string{"some config", "options attempts:2 timeout:3 someother"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Existing options with timeout and attempts are updated",
|
||||||
|
others: []string{"some config", "options timeout:4 attempts:3"},
|
||||||
|
timeout: 5,
|
||||||
|
attempts: 4,
|
||||||
|
expected: []string{"some config", "options timeout:5 attempts:4"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Modify existing options, add missing attempts before timeout",
|
||||||
|
others: []string{"some config", "options timeout:4"},
|
||||||
|
timeout: 4,
|
||||||
|
attempts: 3,
|
||||||
|
expected: []string{"some config", "options attempts:3 timeout:4"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := prepareOptionsWithTimeout(tc.others, tc.timeout, tc.attempts)
|
||||||
|
assert.Equal(t, tc.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRemoveFirstNbNameserver(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
content string
|
||||||
|
ipToRemove string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Unrelated nameservers with comments and options",
|
||||||
|
content: `# This is a comment
|
||||||
|
options rotate
|
||||||
|
nameserver 1.1.1.1
|
||||||
|
# Another comment
|
||||||
|
nameserver 8.8.4.4
|
||||||
|
search example.com`,
|
||||||
|
ipToRemove: "9.9.9.9",
|
||||||
|
expected: `# This is a comment
|
||||||
|
options rotate
|
||||||
|
nameserver 1.1.1.1
|
||||||
|
# Another comment
|
||||||
|
nameserver 8.8.4.4
|
||||||
|
search example.com`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "First nameserver matches",
|
||||||
|
content: `search example.com
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
# oof, a comment
|
||||||
|
nameserver 8.8.4.4
|
||||||
|
options attempts:5`,
|
||||||
|
ipToRemove: "9.9.9.9",
|
||||||
|
expected: `search example.com
|
||||||
|
# oof, a comment
|
||||||
|
nameserver 8.8.4.4
|
||||||
|
options attempts:5`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Target IP not the first nameserver",
|
||||||
|
// nolint:dupword
|
||||||
|
content: `# Comment about the first nameserver
|
||||||
|
nameserver 8.8.4.4
|
||||||
|
# Comment before our target
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
options timeout:2`,
|
||||||
|
ipToRemove: "9.9.9.9",
|
||||||
|
// nolint:dupword
|
||||||
|
expected: `# Comment about the first nameserver
|
||||||
|
nameserver 8.8.4.4
|
||||||
|
# Comment before our target
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
options timeout:2`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Only nameserver matches",
|
||||||
|
content: `options debug
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
search localdomain`,
|
||||||
|
ipToRemove: "9.9.9.9",
|
||||||
|
expected: `options debug
|
||||||
|
nameserver 9.9.9.9
|
||||||
|
search localdomain`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
tempFile := filepath.Join(tempDir, "resolv.conf")
|
||||||
|
err := os.WriteFile(tempFile, []byte(tc.content), 0644)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = removeFirstNbNameserver(tempFile, tc.ipToRemove)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(tempFile)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.expected, string(content), "The resulting content should match the expected output.")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
159
client/internal/dns/file_repair_unix.go
Normal file
159
client/internal/dns/file_repair_unix.go
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
eventTypes = []fsnotify.Op{
|
||||||
|
fsnotify.Create,
|
||||||
|
fsnotify.Write,
|
||||||
|
fsnotify.Remove,
|
||||||
|
fsnotify.Rename,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type repairConfFn func([]string, string, *resolvConf) error
|
||||||
|
|
||||||
|
type repair struct {
|
||||||
|
operationFile string
|
||||||
|
updateFn repairConfFn
|
||||||
|
watchDir string
|
||||||
|
|
||||||
|
inotify *fsnotify.Watcher
|
||||||
|
inotifyWg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRepair(operationFile string, updateFn repairConfFn) *repair {
|
||||||
|
targetFile := targetFile(operationFile)
|
||||||
|
return &repair{
|
||||||
|
operationFile: targetFile,
|
||||||
|
watchDir: path.Dir(targetFile),
|
||||||
|
updateFn: updateFn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *repair) watchFileChanges(nbSearchDomains []string, nbNameserverIP string) {
|
||||||
|
if f.inotify != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("start to watch resolv.conf: %s", f.operationFile)
|
||||||
|
inotify, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to start inotify watcher for resolv.conf: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.inotify = inotify
|
||||||
|
|
||||||
|
f.inotifyWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer f.inotifyWg.Done()
|
||||||
|
for event := range f.inotify.Events {
|
||||||
|
if !f.isEventRelevant(event) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("%s changed, check if it is broken", f.operationFile)
|
||||||
|
|
||||||
|
rConf, err := parseResolvConfFile(f.operationFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse resolv conf: %s", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("check resolv.conf parameters: %s", rConf)
|
||||||
|
if !isNbParamsMissing(nbSearchDomains, nbNameserverIP, rConf) {
|
||||||
|
log.Tracef("resolv.conf still correct, skip the update")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Info("broken params in resolv.conf, repairing it...")
|
||||||
|
|
||||||
|
err = f.inotify.Remove(f.watchDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to rm inotify watch for resolv.conf: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.updateFn(nbSearchDomains, nbNameserverIP, rConf)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to repair resolv.conf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.inotify.Add(f.watchDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to re-add inotify watch for resolv.conf: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = f.inotify.Add(f.watchDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to add inotify watch for resolv.conf: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *repair) stopWatchFileChanges() {
|
||||||
|
if f.inotify == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := f.inotify.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to close resolv.conf inotify: %v", err)
|
||||||
|
}
|
||||||
|
f.inotifyWg.Wait()
|
||||||
|
f.inotify = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *repair) isEventRelevant(event fsnotify.Event) bool {
|
||||||
|
var ok bool
|
||||||
|
for _, et := range eventTypes {
|
||||||
|
if event.Has(et) {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Name == f.operationFile {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// nbParamsAreMissing checks if the resolv.conf file contains all the parameters that NetBird needs
|
||||||
|
// check the NetBird related nameserver IP at the first place
|
||||||
|
// check the NetBird related search domains in the search domains list
|
||||||
|
func isNbParamsMissing(nbSearchDomains []string, nbNameserverIP string, rConf *resolvConf) bool {
|
||||||
|
if !isContains(nbSearchDomains, rConf.searchDomains) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rConf.nameServers) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if rConf.nameServers[0] != nbNameserverIP {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func targetFile(filename string) string {
|
||||||
|
target, err := filepath.EvalSymlinks(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("evarl err: %s", err)
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
175
client/internal/dns/file_repair_unix_test.go
Normal file
175
client/internal/dns/file_repair_unix_test.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
_ = util.InitLog("debug", "console")
|
||||||
|
code := m.Run()
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newRepairtmp(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
resolvConfContent string
|
||||||
|
touchedConfContent string
|
||||||
|
wantChange bool
|
||||||
|
}
|
||||||
|
tests := []args{
|
||||||
|
{
|
||||||
|
resolvConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
|
||||||
|
touchedConfContent: `
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
wantChange: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
|
||||||
|
touchedConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something somethingelse`,
|
||||||
|
wantChange: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
|
||||||
|
touchedConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
wantChange: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
|
||||||
|
touchedConfContent: `
|
||||||
|
searchdomain something`,
|
||||||
|
wantChange: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
|
||||||
|
touchedConfContent: `
|
||||||
|
nameserver 10.0.0.1`,
|
||||||
|
wantChange: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
resolvConfContent: `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`,
|
||||||
|
|
||||||
|
touchedConfContent: `
|
||||||
|
nameserver 8.8.8.8`,
|
||||||
|
wantChange: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run("test", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
workDir := t.TempDir()
|
||||||
|
operationFile := workDir + "/resolv.conf"
|
||||||
|
err := os.WriteFile(operationFile, []byte(tt.resolvConfContent), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write out resolv.conf: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed bool
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
updateFn := func([]string, string, *resolvConf) error {
|
||||||
|
changed = true
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := newRepair(operationFile, updateFn)
|
||||||
|
r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1")
|
||||||
|
|
||||||
|
err = os.WriteFile(operationFile, []byte(tt.touchedConfContent), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write out resolv.conf: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
r.stopWatchFileChanges()
|
||||||
|
|
||||||
|
if changed != tt.wantChange {
|
||||||
|
t.Errorf("unexpected result: want: %v, got: %v", tt.wantChange, changed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_newRepairSymlink(t *testing.T) {
|
||||||
|
resolvConfContent := `
|
||||||
|
nameserver 10.0.0.1
|
||||||
|
nameserver 8.8.8.8
|
||||||
|
searchdomain netbird.cloud something`
|
||||||
|
|
||||||
|
modifyContent := `nameserver 8.8.8.8`
|
||||||
|
|
||||||
|
tmpResolvConf := filepath.Join(t.TempDir(), "resolv.conf")
|
||||||
|
err := os.WriteFile(tmpResolvConf, []byte(resolvConfContent), 0644)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpLink := filepath.Join(t.TempDir(), "symlink")
|
||||||
|
err = os.Symlink(tmpResolvConf, tmpLink)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed bool
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
updateFn := func([]string, string, *resolvConf) error {
|
||||||
|
changed = true
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r := newRepair(tmpLink, updateFn)
|
||||||
|
r.watchFileChanges([]string{"netbird.cloud"}, "10.0.0.1")
|
||||||
|
|
||||||
|
err = os.WriteFile(tmpLink, []byte(modifyContent), 0755)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write out resolv.conf: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
|
||||||
|
r.stopWatchFileChanges()
|
||||||
|
|
||||||
|
if changed != true {
|
||||||
|
t.Errorf("unexpected result: want: %v, got: %v", true, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
329
client/internal/dns/file_unix.go
Normal file
329
client/internal/dns/file_unix.go
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileGeneratedResolvConfContentHeader = "# Generated by NetBird"
|
||||||
|
fileGeneratedResolvConfContentHeaderNextLine = fileGeneratedResolvConfContentHeader + `
|
||||||
|
# If needed you can restore the original file by copying back ` + fileDefaultResolvConfBackupLocation + "\n\n"
|
||||||
|
|
||||||
|
fileDefaultResolvConfBackupLocation = defaultResolvConfPath + ".original.netbird"
|
||||||
|
|
||||||
|
fileMaxLineCharsLimit = 256
|
||||||
|
fileMaxNumberOfSearchDomains = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsFailoverTimeout = 4 * time.Second
|
||||||
|
dnsFailoverAttempts = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileConfigurator struct {
|
||||||
|
repair *repair
|
||||||
|
|
||||||
|
originalPerms os.FileMode
|
||||||
|
nbNameserverIP string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileConfigurator() (hostManager, error) {
|
||||||
|
fc := &fileConfigurator{}
|
||||||
|
fc.repair = newRepair(defaultResolvConfPath, fc.updateConfig)
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) supportCustomPort() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) applyDNSConfig(config HostDNSConfig) error {
|
||||||
|
backupFileExist := f.isBackupFileExist()
|
||||||
|
if !config.RouteAll {
|
||||||
|
if backupFileExist {
|
||||||
|
f.repair.stopWatchFileChanges()
|
||||||
|
err := f.restore()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("restoring the original resolv.conf file return err: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unable to configure DNS for this peer using file manager without a nameserver group with all domains configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !backupFileExist {
|
||||||
|
err := f.backup()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to backup the resolv.conf file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nbSearchDomains := searchDomains(config)
|
||||||
|
f.nbNameserverIP = config.ServerIP
|
||||||
|
|
||||||
|
resolvConf, err := parseBackupResolvConf()
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("could not read original search domains from %s: %s", fileDefaultResolvConfBackupLocation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.repair.stopWatchFileChanges()
|
||||||
|
|
||||||
|
err = f.updateConfig(nbSearchDomains, f.nbNameserverIP, resolvConf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.repair.watchFileChanges(nbSearchDomains, f.nbNameserverIP)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) updateConfig(nbSearchDomains []string, nbNameserverIP string, cfg *resolvConf) error {
|
||||||
|
searchDomainList := mergeSearchDomains(nbSearchDomains, cfg.searchDomains)
|
||||||
|
nameServers := generateNsList(nbNameserverIP, cfg)
|
||||||
|
|
||||||
|
options := prepareOptionsWithTimeout(cfg.others, int(dnsFailoverTimeout.Seconds()), dnsFailoverAttempts)
|
||||||
|
buf := prepareResolvConfContent(
|
||||||
|
searchDomainList,
|
||||||
|
nameServers,
|
||||||
|
options)
|
||||||
|
|
||||||
|
log.Debugf("creating managed file %s", defaultResolvConfPath)
|
||||||
|
err := os.WriteFile(defaultResolvConfPath, buf.Bytes(), f.originalPerms)
|
||||||
|
if err != nil {
|
||||||
|
restoreErr := f.restore()
|
||||||
|
if restoreErr != nil {
|
||||||
|
log.Errorf("attempt to restore default file failed with error: %s", err)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("creating resolver file %s. Error: %w", defaultResolvConfPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("created a NetBird managed %s file with the DNS settings. Added %d search domains. Search list: %s", defaultResolvConfPath, len(searchDomainList), searchDomainList)
|
||||||
|
|
||||||
|
// create another backup for unclean shutdown detection right after overwriting the original resolv.conf
|
||||||
|
if err := createUncleanShutdownIndicator(fileDefaultResolvConfBackupLocation, fileManager, nbNameserverIP); err != nil {
|
||||||
|
log.Errorf("failed to create unclean shutdown resolv.conf backup: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) restoreHostDNS() error {
|
||||||
|
f.repair.stopWatchFileChanges()
|
||||||
|
return f.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) backup() error {
|
||||||
|
stats, err := os.Stat(defaultResolvConfPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking stats for %s file. Error: %w", defaultResolvConfPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.originalPerms = stats.Mode()
|
||||||
|
|
||||||
|
err = copyFile(defaultResolvConfPath, fileDefaultResolvConfBackupLocation)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("backing up %s: %w", defaultResolvConfPath, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) restore() error {
|
||||||
|
err := removeFirstNbNameserver(fileDefaultResolvConfBackupLocation, f.nbNameserverIP)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Failed to remove netbird nameserver from %s on backup restore: %s", fileDefaultResolvConfBackupLocation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = copyFile(fileDefaultResolvConfBackupLocation, defaultResolvConfPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileDefaultResolvConfBackupLocation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := removeUncleanShutdownIndicator(); err != nil {
|
||||||
|
log.Errorf("failed to remove unclean shutdown resolv.conf backup: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.RemoveAll(fileDefaultResolvConfBackupLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error {
|
||||||
|
resolvConf, err := parseDefaultResolvConf()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse current resolv.conf: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// no current nameservers set -> restore
|
||||||
|
if len(resolvConf.nameServers) == 0 {
|
||||||
|
return restoreResolvConfFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDNSAddress, err := netip.ParseAddr(resolvConf.nameServers[0])
|
||||||
|
// not a valid first nameserver -> restore
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("restoring unclean shutdown: parse dns address %s failed: %s", resolvConf.nameServers[0], err)
|
||||||
|
return restoreResolvConfFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
// current address is still netbird's non-available dns address -> restore
|
||||||
|
// comparing parsed addresses only, to remove ambiguity
|
||||||
|
if currentDNSAddress.String() == storedDNSAddress.String() {
|
||||||
|
return restoreResolvConfFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("restoring unclean shutdown: first current nameserver differs from saved nameserver pre-netbird: not restoring")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fileConfigurator) isBackupFileExist() bool {
|
||||||
|
_, err := os.Stat(fileDefaultResolvConfBackupLocation)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreResolvConfFile() error {
|
||||||
|
log.Debugf("restoring unclean shutdown: restoring %s from %s", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation)
|
||||||
|
|
||||||
|
if err := copyFile(fileUncleanShutdownResolvConfLocation, defaultResolvConfPath); err != nil {
|
||||||
|
return fmt.Errorf("restoring %s from %s: %w", defaultResolvConfPath, fileUncleanShutdownResolvConfLocation, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := removeUncleanShutdownIndicator(); err != nil {
|
||||||
|
log.Errorf("failed to remove unclean shutdown resolv.conf file: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateNsList generates a list of nameservers from the config and adds the primary nameserver to the beginning of the list
|
||||||
|
func generateNsList(nbNameserverIP string, cfg *resolvConf) []string {
|
||||||
|
ns := make([]string, 1, len(cfg.nameServers)+1)
|
||||||
|
ns[0] = nbNameserverIP
|
||||||
|
for _, cfgNs := range cfg.nameServers {
|
||||||
|
if nbNameserverIP != cfgNs {
|
||||||
|
ns = append(ns, cfgNs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ns
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareResolvConfContent(searchDomains, nameServers, others []string) bytes.Buffer {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.WriteString(fileGeneratedResolvConfContentHeaderNextLine)
|
||||||
|
|
||||||
|
for _, cfgLine := range others {
|
||||||
|
buf.WriteString(cfgLine)
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(searchDomains) > 0 {
|
||||||
|
buf.WriteString("search ")
|
||||||
|
buf.WriteString(strings.Join(searchDomains, " "))
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ns := range nameServers {
|
||||||
|
buf.WriteString("nameserver ")
|
||||||
|
buf.WriteString(ns)
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchDomains(config HostDNSConfig) []string {
|
||||||
|
listOfDomains := make([]string, 0)
|
||||||
|
for _, dConf := range config.Domains {
|
||||||
|
if dConf.MatchOnly || dConf.Disabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
listOfDomains = append(listOfDomains, dConf.Domain)
|
||||||
|
}
|
||||||
|
return listOfDomains
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge search Domains lists and cut off the list if it is too long
|
||||||
|
func mergeSearchDomains(searchDomains []string, originalSearchDomains []string) []string {
|
||||||
|
lineSize := len("search")
|
||||||
|
searchDomainsList := make([]string, 0, len(searchDomains)+len(originalSearchDomains))
|
||||||
|
|
||||||
|
lineSize = validateAndFillSearchDomains(lineSize, &searchDomainsList, searchDomains)
|
||||||
|
_ = validateAndFillSearchDomains(lineSize, &searchDomainsList, originalSearchDomains)
|
||||||
|
|
||||||
|
return searchDomainsList
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateAndFillSearchDomains checks if the search Domains list is not too long and if the line is not too long
|
||||||
|
// extend s slice with vs elements
|
||||||
|
// return with the number of characters in the searchDomains line
|
||||||
|
func validateAndFillSearchDomains(initialLineChars int, s *[]string, vs []string) int {
|
||||||
|
for _, sd := range vs {
|
||||||
|
duplicated := false
|
||||||
|
for _, fs := range *s {
|
||||||
|
if fs == sd {
|
||||||
|
duplicated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if duplicated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpCharsNumber := initialLineChars + 1 + len(sd)
|
||||||
|
if tmpCharsNumber > fileMaxLineCharsLimit {
|
||||||
|
// lets log all skipped Domains
|
||||||
|
log.Infof("search list line is larger than %d characters. Skipping append of %s domain", fileMaxLineCharsLimit, sd)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
initialLineChars = tmpCharsNumber
|
||||||
|
|
||||||
|
if len(*s) >= fileMaxNumberOfSearchDomains {
|
||||||
|
// lets log all skipped Domains
|
||||||
|
log.Infof("already appended %d domains to search list. Skipping append of %s domain", fileMaxNumberOfSearchDomains, sd)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*s = append(*s, sd)
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialLineChars
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFile(src, dest string) error {
|
||||||
|
stats, err := os.Stat(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking stats for %s file when copying it. Error: %s", src, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead, err := os.ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading the file %s file for copy. Error: %s", src, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(dest, bytesRead, stats.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing the destination file %s for copy. Error: %s", dest, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isContains(subList []string, list []string) bool {
|
||||||
|
for _, sl := range subList {
|
||||||
|
var found bool
|
||||||
|
for _, l := range list {
|
||||||
|
if sl == l {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
125
client/internal/dns/file_unix_test.go
Normal file
125
client/internal/dns/file_unix_test.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
//go:build (linux && !android) || freebsd
|
||||||
|
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_mergeSearchDomains(t *testing.T) {
|
||||||
|
searchDomains := []string{"a", "b"}
|
||||||
|
originDomains := []string{"c", "d"}
|
||||||
|
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||||
|
if len(mergedDomains) != 4 {
|
||||||
|
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mergeSearchTooMuchDomains(t *testing.T) {
|
||||||
|
searchDomains := []string{"a", "b", "c", "d", "e", "f", "g"}
|
||||||
|
originDomains := []string{"h", "i"}
|
||||||
|
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||||
|
if len(mergedDomains) != 6 {
|
||||||
|
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mergeSearchTooMuchDomainsInOrigin(t *testing.T) {
|
||||||
|
searchDomains := []string{"a", "b"}
|
||||||
|
originDomains := []string{"c", "d", "e", "f", "g"}
|
||||||
|
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||||
|
if len(mergedDomains) != 6 {
|
||||||
|
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mergeSearchTooLongDomain(t *testing.T) {
|
||||||
|
searchDomains := []string{getLongLine()}
|
||||||
|
originDomains := []string{"b"}
|
||||||
|
mergedDomains := mergeSearchDomains(searchDomains, originDomains)
|
||||||
|
if len(mergedDomains) != 1 {
|
||||||
|
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
searchDomains = []string{"b"}
|
||||||
|
originDomains = []string{getLongLine()}
|
||||||
|
|
||||||
|
mergedDomains = mergeSearchDomains(searchDomains, originDomains)
|
||||||
|
if len(mergedDomains) != 1 {
|
||||||
|
t.Errorf("invalid len of result domains: %d, want: %d", len(mergedDomains), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_isContains(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
subList []string
|
||||||
|
list []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
subList: []string{"a", "b", "c"},
|
||||||
|
list: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
subList: []string{"a"},
|
||||||
|
list: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
subList: []string{"d"},
|
||||||
|
list: []string{"a", "b", "c"},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
subList: []string{"a"},
|
||||||
|
list: []string{},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
subList: []string{},
|
||||||
|
list: []string{"b"},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
args: args{
|
||||||
|
subList: []string{},
|
||||||
|
list: []string{},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("list check test", func(t *testing.T) {
|
||||||
|
if got := isContains(tt.args.subList, tt.args.list); got != tt.want {
|
||||||
|
t.Errorf("isContains() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLongLine() string {
|
||||||
|
x := "search "
|
||||||
|
for {
|
||||||
|
for i := 0; i <= 9; i++ {
|
||||||
|
if len(x) > fileMaxLineCharsLimit {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
x = fmt.Sprintf("%s%d", x, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,37 +2,40 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type hostManager interface {
|
type hostManager interface {
|
||||||
applyDNSConfig(config hostDNSConfig) error
|
applyDNSConfig(config HostDNSConfig) error
|
||||||
restoreHostDNS() error
|
restoreHostDNS() error
|
||||||
supportCustomPort() bool
|
supportCustomPort() bool
|
||||||
|
restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type hostDNSConfig struct {
|
type HostDNSConfig struct {
|
||||||
domains []domainConfig
|
Domains []DomainConfig `json:"domains"`
|
||||||
routeAll bool
|
RouteAll bool `json:"routeAll"`
|
||||||
serverIP string
|
ServerIP string `json:"serverIP"`
|
||||||
serverPort int
|
ServerPort int `json:"serverPort"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type domainConfig struct {
|
type DomainConfig struct {
|
||||||
disabled bool
|
Disabled bool `json:"disabled"`
|
||||||
domain string
|
Domain string `json:"domain"`
|
||||||
matchOnly bool
|
MatchOnly bool `json:"matchOnly"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockHostConfigurator struct {
|
type mockHostConfigurator struct {
|
||||||
applyDNSConfigFunc func(config hostDNSConfig) error
|
applyDNSConfigFunc func(config HostDNSConfig) error
|
||||||
restoreHostDNSFunc func() error
|
restoreHostDNSFunc func() error
|
||||||
supportCustomPortFunc func() bool
|
supportCustomPortFunc func() bool
|
||||||
|
restoreUncleanShutdownDNSFunc func(*netip.Addr) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockHostConfigurator) applyDNSConfig(config hostDNSConfig) error {
|
func (m *mockHostConfigurator) applyDNSConfig(config HostDNSConfig) error {
|
||||||
if m.applyDNSConfigFunc != nil {
|
if m.applyDNSConfigFunc != nil {
|
||||||
return m.applyDNSConfigFunc(config)
|
return m.applyDNSConfigFunc(config)
|
||||||
}
|
}
|
||||||
@@ -53,40 +56,48 @@ func (m *mockHostConfigurator) supportCustomPort() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockHostConfigurator) restoreUncleanShutdownDNS(storedDNSAddress *netip.Addr) error {
|
||||||
|
if m.restoreUncleanShutdownDNSFunc != nil {
|
||||||
|
return m.restoreUncleanShutdownDNSFunc(storedDNSAddress)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("method restoreUncleanShutdownDNS is not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
func newNoopHostMocker() hostManager {
|
func newNoopHostMocker() hostManager {
|
||||||
return &mockHostConfigurator{
|
return &mockHostConfigurator{
|
||||||
applyDNSConfigFunc: func(config hostDNSConfig) error { return nil },
|
applyDNSConfigFunc: func(config HostDNSConfig) error { return nil },
|
||||||
restoreHostDNSFunc: func() error { return nil },
|
restoreHostDNSFunc: func() error { return nil },
|
||||||
supportCustomPortFunc: func() bool { return true },
|
supportCustomPortFunc: func() bool { return true },
|
||||||
|
restoreUncleanShutdownDNSFunc: func(*netip.Addr) error { return nil },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) hostDNSConfig {
|
func dnsConfigToHostDNSConfig(dnsConfig nbdns.Config, ip string, port int) HostDNSConfig {
|
||||||
config := hostDNSConfig{
|
config := HostDNSConfig{
|
||||||
routeAll: false,
|
RouteAll: false,
|
||||||
serverIP: ip,
|
ServerIP: ip,
|
||||||
serverPort: port,
|
ServerPort: port,
|
||||||
}
|
}
|
||||||
for _, nsConfig := range dnsConfig.NameServerGroups {
|
for _, nsConfig := range dnsConfig.NameServerGroups {
|
||||||
if len(nsConfig.NameServers) == 0 {
|
if len(nsConfig.NameServers) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if nsConfig.Primary {
|
if nsConfig.Primary {
|
||||||
config.routeAll = true
|
config.RouteAll = true
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range nsConfig.Domains {
|
for _, domain := range nsConfig.Domains {
|
||||||
config.domains = append(config.domains, domainConfig{
|
config.Domains = append(config.Domains, DomainConfig{
|
||||||
domain: strings.TrimSuffix(domain, "."),
|
Domain: strings.TrimSuffix(domain, "."),
|
||||||
matchOnly: true,
|
MatchOnly: !nsConfig.SearchDomainsEnabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, customZone := range dnsConfig.CustomZones {
|
for _, customZone := range dnsConfig.CustomZones {
|
||||||
config.domains = append(config.domains, domainConfig{
|
config.Domains = append(config.Domains, DomainConfig{
|
||||||
domain: strings.TrimSuffix(customZone.Domain, "."),
|
Domain: strings.TrimSuffix(customZone.Domain, "."),
|
||||||
matchOnly: false,
|
MatchOnly: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user