mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-26 23:07:34 +00:00
Compare commits
364 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7c51d9e971 | ||
![]() |
f3e5513f62 | ||
![]() |
871d6119ec | ||
![]() |
6901e2fc9a | ||
![]() |
977a0e7215 | ||
![]() |
90ace46587 | ||
![]() |
65e34bbbab | ||
![]() |
f09adc2192 | ||
![]() |
3eb1a40c68 | ||
![]() |
c78e1b98cc | ||
![]() |
b4b3609678 | ||
![]() |
d29b5a074a | ||
![]() |
cd11d2eccd | ||
![]() |
dff1dca19c | ||
![]() |
2928fcb046 | ||
![]() |
8aaaeb26eb | ||
![]() |
bbe2f56b74 | ||
![]() |
74a904d04c | ||
![]() |
06c6dfc67f | ||
![]() |
f791df4977 | ||
![]() |
14f4da764c | ||
![]() |
f6cdb8e38e | ||
![]() |
08ad163dfe | ||
![]() |
1892fdc429 | ||
![]() |
bec044346e | ||
![]() |
ff49ca6d30 | ||
![]() |
6801d713a7 | ||
![]() |
5ed197b3ca | ||
![]() |
80d087404f | ||
![]() |
9724dd2351 | ||
![]() |
43993a3460 | ||
![]() |
0fdc814c4a | ||
![]() |
e759fb1c38 | ||
![]() |
ba4507c02e | ||
![]() |
40e02f01b0 | ||
![]() |
34949f7c32 | ||
![]() |
9eace183c6 | ||
![]() |
3b2044666d | ||
![]() |
22e6505079 | ||
![]() |
467002bbf7 | ||
![]() |
02f98a2592 | ||
![]() |
f2d01aa54d | ||
![]() |
9d0b8ac6f4 | ||
![]() |
262c93504f | ||
![]() |
5315bc25c5 | ||
![]() |
2da3ef420c | ||
![]() |
af478e0e45 | ||
![]() |
bd2d706745 | ||
![]() |
2d5f99a008 | ||
![]() |
586781b49c | ||
![]() |
caa7b739af | ||
![]() |
ffbdef292f | ||
![]() |
f99c224171 | ||
![]() |
5149c6c349 | ||
![]() |
3524c6eff6 | ||
![]() |
8e784438c7 | ||
![]() |
4bc009d845 | ||
![]() |
8bd566d4d8 | ||
![]() |
d0c2ce90bb | ||
![]() |
4532d0e0c8 | ||
![]() |
5106c217c7 | ||
![]() |
fe772dd38e | ||
![]() |
dd6a1dfccc | ||
![]() |
09228554cb | ||
![]() |
2eedcce3fd | ||
![]() |
ae48a1721e | ||
![]() |
eae8f9a666 | ||
![]() |
cc066d7aeb | ||
![]() |
9f4fc3669b | ||
![]() |
58b60af208 | ||
![]() |
3d4b49b693 | ||
![]() |
9e17e41b79 | ||
![]() |
8a04cbe3c8 | ||
![]() |
a7f5c427d4 | ||
![]() |
6170f7268f | ||
![]() |
ecc0cd4992 | ||
![]() |
4fc0117e08 | ||
![]() |
80b876d21d | ||
![]() |
8b7b3452cf | ||
![]() |
8ade7aed62 | ||
![]() |
150cf810dd | ||
![]() |
ad30e36881 | ||
![]() |
684632eb3d | ||
![]() |
b7ccdaf423 | ||
![]() |
5a89a869be | ||
![]() |
b5f4637b5c | ||
![]() |
319457ae27 | ||
![]() |
86da073226 | ||
![]() |
dcfe55dae8 | ||
![]() |
38093219fd | ||
![]() |
05b07adba2 | ||
![]() |
b3e2b8e6a5 | ||
![]() |
5912dcc72c | ||
![]() |
099fee9cae | ||
![]() |
498d664f51 | ||
![]() |
315aadae06 | ||
![]() |
a6be4bacbc | ||
![]() |
8239989c36 | ||
![]() |
5b10af7399 | ||
![]() |
bd9055ddd7 | ||
![]() |
d8d1e63c36 | ||
![]() |
0ec6207e05 | ||
![]() |
a34ca40594 | ||
![]() |
d253bb750c | ||
![]() |
7954fa3c33 | ||
![]() |
9937a6102e | ||
![]() |
12e635f946 | ||
![]() |
d520a8a1d5 | ||
![]() |
9f16d0ed1f | ||
![]() |
e17efb6e91 | ||
![]() |
9046dbde4f | ||
![]() |
4e156bd4f7 | ||
![]() |
8d6beebac4 | ||
![]() |
5a7c2b250c | ||
![]() |
3efc9bfa22 | ||
![]() |
6d0e40045a | ||
![]() |
12cc7fc639 | ||
![]() |
4870a2e149 | ||
![]() |
5953027411 | ||
![]() |
5fa23b1e38 | ||
![]() |
e9cff0506c | ||
![]() |
ae4107a3b2 | ||
![]() |
ef6cece720 | ||
![]() |
289f1ce7c2 | ||
![]() |
fc5a5830aa | ||
![]() |
be3a7b3e68 | ||
![]() |
8cf8b0ec41 | ||
![]() |
3b8cd0a8d6 | ||
![]() |
1b1b776097 | ||
![]() |
b3887e554c | ||
![]() |
6fab0e9507 | ||
![]() |
adc32fe92f | ||
![]() |
d50e1bc803 | ||
![]() |
15d5b3f82c | ||
![]() |
7af85c7d70 | ||
![]() |
685b565512 | ||
![]() |
9542bfa902 | ||
![]() |
fbfae473d4 | ||
![]() |
39dab53ac7 | ||
![]() |
a3a53f92c3 | ||
![]() |
0240375417 | ||
![]() |
424faa1c51 | ||
![]() |
cb7a5f17d9 | ||
![]() |
2f75075da3 | ||
![]() |
bc62af7f7d | ||
![]() |
bc578f571c | ||
![]() |
f0947223bb | ||
![]() |
19e6aaf9f5 | ||
![]() |
e3d4aed44a | ||
![]() |
8c2327a2bf | ||
![]() |
cfdbc481a5 | ||
![]() |
7218b5a56c | ||
![]() |
c7f2427de1 | ||
![]() |
87b0f5fe24 | ||
![]() |
295e9c9a10 | ||
![]() |
ec751e8cc7 | ||
![]() |
52206dc381 | ||
![]() |
a008b42f99 | ||
![]() |
671c7f2a47 | ||
![]() |
c0531627bc | ||
![]() |
f088a244da | ||
![]() |
253861ebd3 | ||
![]() |
5e3959f1d0 | ||
![]() |
aab0502a4a | ||
![]() |
f0bd40ff68 | ||
![]() |
bcbd24120d | ||
![]() |
efe6cec11a | ||
![]() |
b809adf981 | ||
![]() |
6c59ae862a | ||
![]() |
95201669fe | ||
![]() |
8825494d59 | ||
![]() |
3dbffae99f | ||
![]() |
d851d9afe7 | ||
![]() |
63d6ab4251 | ||
![]() |
f3ec8c5b37 | ||
![]() |
5a85d3515d | ||
![]() |
02f0611dde | ||
![]() |
1720dff476 | ||
![]() |
03a88fe304 | ||
![]() |
a9f72a6ee1 | ||
![]() |
9f129bc7b0 | ||
![]() |
8844dedb8a | ||
![]() |
b087e955fb | ||
![]() |
bb975d2edd | ||
![]() |
fde5b18be4 | ||
![]() |
18428b0f93 | ||
![]() |
eb42fd4973 | ||
![]() |
ba8af20817 | ||
![]() |
1233371962 | ||
![]() |
1d00131416 | ||
![]() |
1e6667567a | ||
![]() |
3ed63ede1e | ||
![]() |
3f237372c9 | ||
![]() |
2e2c58bfef | ||
![]() |
401960e17e | ||
![]() |
85e8968a4d | ||
![]() |
980f18b266 | ||
![]() |
81eea137d4 | ||
![]() |
ffa8580d30 | ||
![]() |
501dc2bb3d | ||
![]() |
605b6829db | ||
![]() |
8eed15b813 | ||
![]() |
b6ff6e96cd | ||
![]() |
d027a9ba75 | ||
![]() |
69cd736112 | ||
![]() |
7d8a1859f0 | ||
![]() |
0b1a6611fd | ||
![]() |
8113b4cc22 | ||
![]() |
95c551d011 | ||
![]() |
b530916044 | ||
![]() |
2674e1cb8b | ||
![]() |
f57567ea56 | ||
![]() |
7e3426ba93 | ||
![]() |
b7f2f8b55c | ||
![]() |
6844b9df51 | ||
![]() |
387ae9ea6c | ||
![]() |
3f8a4ab17d | ||
![]() |
b368421dbd | ||
![]() |
1796000b05 | ||
![]() |
aecc151baf | ||
![]() |
81ca5d8ede | ||
![]() |
25661ebcad | ||
![]() |
f2345a9a63 | ||
![]() |
e833cdfb98 | ||
![]() |
4666b8f6cd | ||
![]() |
ff83527ac7 | ||
![]() |
c6dbc307ae | ||
![]() |
514de5434f | ||
![]() |
b4db89ea9d | ||
![]() |
67b8a7a53d | ||
![]() |
c4e6894d6a | ||
![]() |
ebb4ec7c33 | ||
![]() |
68a482ed92 | ||
![]() |
36dcab9300 | ||
![]() |
fec7100898 | ||
![]() |
11b0a82c4a | ||
![]() |
ddab8ecf33 | ||
![]() |
d171552577 | ||
![]() |
e00ed4c95d | ||
![]() |
38e8b036d2 | ||
![]() |
81fde1a805 | ||
![]() |
98f1dd1624 | ||
![]() |
3f4295f8cd | ||
![]() |
f53699367b | ||
![]() |
9cbcaf39ac | ||
![]() |
388683e3f2 | ||
![]() |
996a593fa2 | ||
![]() |
ab73e3cb90 | ||
![]() |
438fcdfc5f | ||
![]() |
dc0c3f9f8b | ||
![]() |
6d1e705684 | ||
![]() |
2b7b32ff3a | ||
![]() |
549d6f9dd2 | ||
![]() |
9ff08c1b34 | ||
![]() |
91a374d698 | ||
![]() |
55b56e8686 | ||
![]() |
df9cadd938 | ||
![]() |
1baafdd17d | ||
![]() |
f4bb2aaaeb | ||
![]() |
d12e321584 | ||
![]() |
cff7ef026f | ||
![]() |
5cff8428c3 | ||
![]() |
f21cbaef9c | ||
![]() |
059fe24526 | ||
![]() |
8bcb761cef | ||
![]() |
69cf64dce5 | ||
![]() |
0d9a6d7a49 | ||
![]() |
d59bdfeb99 | ||
![]() |
70e755fdd3 | ||
![]() |
bf90447cc4 | ||
![]() |
face270298 | ||
![]() |
b24c7ffa6b | ||
![]() |
0e9a9f97ba | ||
![]() |
a5af69df8a | ||
![]() |
2a931df07a | ||
![]() |
14d48597da | ||
![]() |
36c89da848 | ||
![]() |
1692bd98fd | ||
![]() |
171e1e7823 | ||
![]() |
047b7d95a1 | ||
![]() |
cd6030ec8f | ||
![]() |
adc21baa28 | ||
![]() |
ba4047b51a | ||
![]() |
ad5dc9ea87 | ||
![]() |
e6a47f705d | ||
![]() |
1a65c065d0 | ||
![]() |
7da4967f5e | ||
![]() |
a7c8be4d69 | ||
![]() |
885ba4452d | ||
![]() |
d0e6a9ad41 | ||
![]() |
af99cebf11 | ||
![]() |
1d05e511b3 | ||
![]() |
1fced2bdf0 | ||
![]() |
dd6ca6e4b6 | ||
![]() |
1a0771b016 | ||
![]() |
b63b534fa7 | ||
![]() |
01f0ec34f4 | ||
![]() |
3d0b39f05a | ||
![]() |
a7d1f21271 | ||
![]() |
11acb0129d | ||
![]() |
7695a3fcbf | ||
![]() |
4ad2446557 | ||
![]() |
03949dcf3f | ||
![]() |
9c028e1d0d | ||
![]() |
189628b381 | ||
![]() |
0ad801bcfe | ||
![]() |
4b83efa218 | ||
![]() |
52a0027aea | ||
![]() |
988f4ad265 | ||
![]() |
2ae213c255 | ||
![]() |
cceecf4b1a | ||
![]() |
0021f3463f | ||
![]() |
fd074a4364 | ||
![]() |
f68f779bee | ||
![]() |
79a35caf24 | ||
![]() |
5dfa01a0e8 | ||
![]() |
e2d739f646 | ||
![]() |
8e7edf566c | ||
![]() |
254be42614 | ||
![]() |
19014a198e | ||
![]() |
f599a1a2c1 | ||
![]() |
7a19507665 | ||
![]() |
1a60e89ada | ||
![]() |
12bcb6cc1f | ||
![]() |
c4d28c4f65 | ||
![]() |
2c3074a979 | ||
![]() |
b415adee6d | ||
![]() |
10a66a4edc | ||
![]() |
e8e7e6bcf5 | ||
![]() |
f0fd19b5e5 | ||
![]() |
496dc94f02 | ||
![]() |
0ca2cda49b | ||
![]() |
9ac7d4e0df | ||
![]() |
0ec5f1c02c | ||
![]() |
21b15c97a9 | ||
![]() |
e65a66b181 | ||
![]() |
5fc4dddf83 | ||
![]() |
2fe493ba6c | ||
![]() |
17146ee5bb | ||
![]() |
3be8d97cc3 | ||
![]() |
20fc551a67 | ||
![]() |
0aea4bd395 | ||
![]() |
3d9ab25930 | ||
![]() |
28d187d5a0 | ||
![]() |
0c74c74879 | ||
![]() |
8025e51299 | ||
![]() |
7fe038f87e | ||
![]() |
6c556da05e | ||
![]() |
6a1927a09e | ||
![]() |
415748d381 | ||
![]() |
d9c9787611 | ||
![]() |
e8eaabf0c8 | ||
![]() |
aa46f67d08 | ||
![]() |
57837057b7 | ||
![]() |
7cc067e3a5 | ||
![]() |
dde0486f03 | ||
![]() |
2ab5a1f1c2 | ||
![]() |
f7a7f601a0 | ||
![]() |
36f80cb12c | ||
![]() |
695610c305 | ||
![]() |
93ffc0b876 | ||
![]() |
e7fca66655 | ||
![]() |
474fdda8ca | ||
![]() |
330175889e |
@@ -5,62 +5,90 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/golang:1.9
|
||||
|
||||
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
|
||||
- image: circleci/golang:1.11
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
- run:
|
||||
name: Create artifact upload directory and set variables
|
||||
command: |
|
||||
mkdir /tmp/upload
|
||||
echo 'export CINAME=$(sh contrib/semver/name.sh)' >> $BASH_ENV
|
||||
echo 'export CIVERSION=$(sh contrib/semver/version.sh | cut -c 2-)' >> $BASH_ENV
|
||||
echo 'export CIVERSION=$(sh contrib/semver/version.sh --bare)' >> $BASH_ENV
|
||||
git config --global user.email "$(git log --format='%ae' HEAD -1)";
|
||||
git config --global user.name "$(git log --format='%an' HEAD -1)";
|
||||
|
||||
- run:
|
||||
name: Build for Linux (including Debian packages)
|
||||
name: Install alien
|
||||
command: |
|
||||
PKGARCH=amd64 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-amd64;
|
||||
PKGARCH=i386 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-i386;
|
||||
PKGARCH=mipsel sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mipsel;
|
||||
PKGARCH=mips sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mips;
|
||||
PKGARCH=armhf sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-armhf;
|
||||
sudo apt-get install -y alien
|
||||
|
||||
- run:
|
||||
name: Test debug builds
|
||||
command: |
|
||||
./build -d
|
||||
test -f yggdrasil && test -f yggdrasilctl
|
||||
|
||||
- run:
|
||||
name: Build for Linux (including Debian packages and RPMs)
|
||||
command: |
|
||||
rm -f {yggdrasil,yggdrasilctl}
|
||||
PKGARCH=amd64 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-amd64;
|
||||
PKGARCH=i386 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-i386;
|
||||
PKGARCH=mipsel sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mipsel && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-mipsel;
|
||||
PKGARCH=mips sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mips && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-mips;
|
||||
PKGARCH=armhf sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-armhf && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-armhf;
|
||||
PKGARCH=arm64 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-arm64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-arm64;
|
||||
sudo alien --to-rpm yggdrasil*.deb --scripts --keep-version && mv *.rpm /tmp/upload/;
|
||||
mv *.deb /tmp/upload/
|
||||
|
||||
- run:
|
||||
name: Build for macOS
|
||||
command: |
|
||||
GOOS=darwin GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-amd64;
|
||||
GOOS=darwin GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-i386;
|
||||
rm -f {yggdrasil,yggdrasilctl}
|
||||
GOOS=darwin GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-darwin-amd64;
|
||||
GOOS=darwin GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-darwin-i386;
|
||||
|
||||
- run:
|
||||
name: Build for macOS (.pkg format)
|
||||
command: |
|
||||
rm -rf {yggdrasil,yggdrasilctl}
|
||||
GOOS=darwin GOARCH=amd64 ./build && PKGARCH=amd64 sh contrib/macos/create-pkg.sh && mv *.pkg /tmp/upload/
|
||||
GOOS=darwin GOARCH=386 ./build && PKGARCH=i386 sh contrib/macos/create-pkg.sh && mv *.pkg /tmp/upload/
|
||||
|
||||
- run:
|
||||
name: Build for OpenBSD
|
||||
command: |
|
||||
GOOS=openbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-amd64;
|
||||
GOOS=openbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-i386;
|
||||
rm -f {yggdrasil,yggdrasilctl}
|
||||
GOOS=openbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-openbsd-amd64;
|
||||
GOOS=openbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-openbsd-i386;
|
||||
|
||||
- run:
|
||||
name: Build for FreeBSD
|
||||
command: |
|
||||
GOOS=freebsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-amd64;
|
||||
GOOS=freebsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-i386;
|
||||
rm -f {yggdrasil,yggdrasilctl}
|
||||
GOOS=freebsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-freebsd-amd64;
|
||||
GOOS=freebsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-freebsd-i386;
|
||||
|
||||
- run:
|
||||
name: Build for NetBSD
|
||||
command: |
|
||||
GOOS=netbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-amd64;
|
||||
GOOS=netbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-i386;
|
||||
rm -f {yggdrasil,yggdrasilctl}
|
||||
GOOS=netbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-netbsd-amd64;
|
||||
GOOS=netbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-netbsd-i386;
|
||||
|
||||
- run:
|
||||
name: Build for Windows
|
||||
command: |
|
||||
GOOS=windows GOARCH=amd64 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-amd64.exe;
|
||||
GOOS=windows GOARCH=386 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-i386.exe;
|
||||
rm -f {yggdrasil,yggdrasilctl}
|
||||
GOOS=windows GOARCH=amd64 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-amd64.exe && mv yggdrasilctl.exe /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-windows-amd64.exe;
|
||||
GOOS=windows GOARCH=386 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-i386.exe && mv yggdrasilctl.exe /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-windows-i386.exe;
|
||||
|
||||
- run:
|
||||
name: Build for EdgeRouter
|
||||
command: |
|
||||
rm -f {yggdrasil,yggdrasilctl}
|
||||
git clone https://github.com/neilalexander/vyatta-yggdrasil /tmp/vyatta-yggdrasil;
|
||||
cd /tmp/vyatta-yggdrasil;
|
||||
BUILDDIR_YGG=$CIRCLE_WORKING_DIRECTORY ./build-edgerouter-x $CIRCLE_BRANCH;
|
||||
@@ -70,3 +98,13 @@ jobs:
|
||||
- store_artifacts:
|
||||
path: /tmp/upload
|
||||
destination: /
|
||||
|
||||
- run:
|
||||
name: Create tags (master branch only)
|
||||
command: >
|
||||
if [ "${CIRCLE_BRANCH}" == "master" ]; then
|
||||
git tag -f -a $(sh contrib/semver/version.sh) -m "Created by CircleCI" && git push -f --tags;
|
||||
else
|
||||
echo "Only runs for master branch (this is ${CIRCLE_BRANCH})";
|
||||
fi;
|
||||
when: on_success
|
||||
|
160
CHANGELOG.md
Normal file
160
CHANGELOG.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
||||
|
||||
<!-- Use this as a template
|
||||
## [X.Y.Z] - YYYY-MM-DD
|
||||
### Added
|
||||
- for new features.
|
||||
|
||||
### Changed
|
||||
- for changes in existing functionality.
|
||||
|
||||
### Deprecated
|
||||
- for soon-to-be removed features.
|
||||
|
||||
### Removed
|
||||
- for now removed features.
|
||||
|
||||
### Fixed
|
||||
- for any bug fixes.
|
||||
|
||||
### Security
|
||||
- in case of vulnerabilities.
|
||||
-->
|
||||
|
||||
## [0.3.0] - 2018-12-12
|
||||
### Added
|
||||
- Crypto-key routing support for tunnelling both IPv4 and IPv6 over Yggdrasil
|
||||
- Add advanced `SwitchOptions` in configuration file for tuning the switch
|
||||
- Add `dhtPing` to the admin socket to aid in crawling the network
|
||||
- New macOS .pkgs built automatically by CircleCI
|
||||
- Add Dockerfile to repository for Docker support
|
||||
- Add `-json` command line flag for generating and normalising configuration in plain JSON instead of HJSON
|
||||
- Build name and version numbers are now imprinted onto the build, accessible through `yggdrasil -version` and `yggdrasilctl getSelf`
|
||||
- Add ability to disable admin socket by setting `AdminListen` to `"none"`
|
||||
- `yggdrasilctl` now tries to look for the default configuration file to find `AdminListen` if `-endpoint` is not specified
|
||||
- `yggdrasilctl` now returns more useful logging in the event of a fatal error
|
||||
|
||||
### Changed
|
||||
- Switched to Chord DHT (instead of Kademlia, although still compatible at the protocol level)
|
||||
- The `AdminListen` option and `yggdrasilctl` now default to `unix:///var/run/yggdrasil.sock` on BSDs, macOS and Linux
|
||||
- Cleaned up some of the parameter naming in the admin socket
|
||||
- Latency-based parent selection for the switch instead of uptime-based (should help to avoid high latency links somewhat)
|
||||
- Real peering endpoints now shown in the admin socket `getPeers` call to help identify peerings
|
||||
- Reuse the multicast port on supported platforms so that multiple Yggdrasil processes can run
|
||||
- `yggdrasilctl` now has more useful help text (with `-help` or when no arguments passed)
|
||||
|
||||
### Fixed
|
||||
- Memory leaks in the DHT fixed
|
||||
- Crash fixed where the ICMPv6 NDP goroutine would incorrectly start in TUN mode
|
||||
- Removing peers from the switch table if they stop sending switch messages but keep the TCP connection alive
|
||||
|
||||
## [0.2.7] - 2018-10-13
|
||||
### Added
|
||||
- Session firewall, which makes it possible to control who can open sessions with your node
|
||||
- Add `getSwitchQueues` to admin socket
|
||||
- Add `InterfacePeers` for configuring static peerings via specific network interfaces
|
||||
- More output shown in `getSwitchPeers`
|
||||
- FreeBSD service script in `contrib`
|
||||
|
||||
## Changed
|
||||
- CircleCI builds are now built with Go 1.11 instead of Go 1.9
|
||||
|
||||
## Fixed
|
||||
- Race condition in the switch table, reported by trn
|
||||
- Debug builds are now tested by CircleCI as well as platform release builds
|
||||
- Port number fixed on admin graph from unknown nodes
|
||||
|
||||
## [0.2.6] - 2018-07-31
|
||||
### Added
|
||||
- Configurable TCP timeouts to assist in peering over Tor/I2P
|
||||
- Prefer IPv6 flow label when extending coordinates to sort backpressure queues
|
||||
- `arm64` builds through CircleCI
|
||||
|
||||
### Changed
|
||||
- Sort dot graph links by integer value
|
||||
|
||||
## [0.2.5] - 2018-07-19
|
||||
### Changed
|
||||
- Make `yggdrasilctl` less case sensitive
|
||||
- More verbose TCP disconnect messages
|
||||
|
||||
### Fixed
|
||||
- Fixed debug builds
|
||||
- Cap maximum MTU on Linux in TAP mode
|
||||
- Process successfully-read TCP traffic before checking for / handling errors (fixes EOF behavior)
|
||||
|
||||
## [0.2.4] - 2018-07-08
|
||||
### Added
|
||||
- Support for UNIX domain sockets for the admin socket using `unix:///path/to/file.sock`
|
||||
- Centralised platform-specific defaults
|
||||
|
||||
### Changed
|
||||
- Backpressure tuning, including reducing resource consumption
|
||||
|
||||
### Fixed
|
||||
- macOS local ping bug, which previously prevented you from pinging your own `utun` adapter's IPv6 address
|
||||
|
||||
## [0.2.3] - 2018-06-29
|
||||
### Added
|
||||
- Begin keeping changelog (incomplete and possibly inaccurate information before this point).
|
||||
- Build RPMs in CircleCI using alien. This provides package support for Fedora, Red Hat Enterprise Linux, CentOS and other RPM-based distributions.
|
||||
|
||||
### Changed
|
||||
- Local backpressure improvements.
|
||||
- Change `box_pub_key` to `key` in admin API for simplicity.
|
||||
- Session cleanup.
|
||||
|
||||
## [0.2.2] - 2018-06-21
|
||||
### Added
|
||||
- Add `yggdrasilconf` utility for testing with the `vyatta-yggdrasil` package.
|
||||
- Add a randomized retry delay after TCP disconnects, to prevent synchronization livelocks.
|
||||
|
||||
### Changed
|
||||
- Update build script to strip by default, which significantly reduces the size of the binary.
|
||||
- Add debug `-d` and UPX `-u` flags to the `build` script.
|
||||
- Start pprof in debug builds based on an environment variable (e.g. `PPROFLISTEN=localhost:6060`), instead of a flag.
|
||||
|
||||
### Fixed
|
||||
- Fix typo in big-endian BOM so that both little-endian and big-endian UTF-16 files are detected correctly.
|
||||
|
||||
## [0.2.1] - 2018-06-15
|
||||
### Changed
|
||||
- The address range was moved from `fd00::/8` to `200::/7`. This range was chosen as it is marked as deprecated. The change prevents overlap with other ULA privately assigned ranges.
|
||||
|
||||
### Fixed
|
||||
- UTF-16 detection conversion for configuration files, which can particularly be a problem on Windows 10 if a configuration file is generated from within PowerShell.
|
||||
- Fixes to the Debian package control file.
|
||||
- Fixes to the launchd service for macOS.
|
||||
- Fixes to the DHT and switch.
|
||||
|
||||
## [0.2.0] - 2018-06-13
|
||||
### Added
|
||||
- Exchange version information during connection setup, to prevent connections with incompatible versions.
|
||||
|
||||
### Changed
|
||||
- Wire format changes (backwards incompatible).
|
||||
- Less maintenance traffic per peer.
|
||||
- Exponential back-off for DHT maintenance traffic (less maintenance traffic for known good peers).
|
||||
- Iterative DHT (added some time between v0.1.0 and here).
|
||||
- Use local queue sizes for a sort of local-only backpressure routing, instead of the removed bandwidth estimates, when deciding where to send a packet.
|
||||
|
||||
### Removed
|
||||
- UDP peering, this may be added again if/when a better implementation appears.
|
||||
- Per peer bandwidth estimation, as this has been replaced with an early local backpressure implementation.
|
||||
|
||||
## [0.1.0] - 2018-02-01
|
||||
### Added
|
||||
- Adopt semantic versioning.
|
||||
|
||||
### Changed
|
||||
- Wire format changes (backwards incompatible).
|
||||
- Many other undocumented changes leading up to this release and before the next one.
|
||||
|
||||
## [0.0.1] - 2017-12-28
|
||||
### Added
|
||||
- First commit.
|
||||
- Initial public release.
|
1
Dockerfile
Normal file
1
Dockerfile
Normal file
@@ -0,0 +1 @@
|
||||
contrib/docker/Dockerfile
|
3
LICENSE
3
LICENSE
@@ -17,11 +17,10 @@ statement from your version. This exception does not (and cannot) modify any
|
||||
license terms which apply to the Application, with which you must still
|
||||
comply.
|
||||
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
16
README.md
16
README.md
@@ -15,7 +15,7 @@ You're encouraged to play with it, but it is strongly advised not to use it for
|
||||
|
||||
## Building
|
||||
|
||||
1. Install Go (tested on 1.9+, [godeb](https://github.com/niemeyer/godeb) is recommended for debian-based linux distributions).
|
||||
1. Install Go (requires 1.11 or later, [godeb](https://github.com/niemeyer/godeb) is recommended for Debian-based Linux distributions).
|
||||
2. Clone this repository.
|
||||
2. `./build`
|
||||
|
||||
@@ -85,7 +85,7 @@ journalctl -u yggdrasil
|
||||
|
||||
- Tested and working on Windows 7 and Windows 10, and should work on any recent versions of Windows, but it depends on the [OpenVPN TAP driver](https://openvpn.net/index.php/open-source/downloads.html) being installed first.
|
||||
- Has been proven to work with both the [NDIS 5](https://swupdate.openvpn.org/community/releases/tap-windows-9.9.2_3.exe) (`tap-windows-9.9.2_3`) driver and the [NDIS 6](https://swupdate.openvpn.org/community/releases/tap-windows-9.21.2.exe) (`tap-windows-9.21.2`) driver, however there are substantial performance issues with the NDIS 6 driver therefore it is recommended to use the NDIS 5 driver instead.
|
||||
- Be aware that connectivity issues can occur on Windows if multiple IPv6 addresses from the `fd00::/8` prefix are assigned to the TAP interface. If this happens, then you may need to manually remove the old/unused addresses from the interface (though the code has a workaround in place to do this automatically in some cases).
|
||||
- Be aware that connectivity issues can occur on Windows if multiple IPv6 addresses from the `200::/7` prefix are assigned to the TAP interface. If this happens, then you may need to manually remove the old/unused addresses from the interface (though the code has a workaround in place to do this automatically in some cases).
|
||||
- TUN mode is not supported on Windows.
|
||||
- Yggdrasil can be installed as a Windows service so that it runs automatically in the background. From an Administrator Command Prompt:
|
||||
```
|
||||
@@ -105,26 +105,26 @@ sc create yggdrasil binpath= "\"C:\path\to\yggdrasil.exe\" -autoconf"
|
||||
|
||||
## Optional: advertise a prefix locally
|
||||
|
||||
Suppose a node has generated the address: `fd00:1111:2222:3333:4444:5555:6666:7777`
|
||||
Suppose a node has generated the address: `200:1111:2222:3333:4444:5555:6666:7777`
|
||||
|
||||
Then the node may also use addresses from the prefix: `fd80:1111:2222:3333::/64` (note the `fd00` changed to `fd80`, a separate `/9` is used for prefixes, but the rest of the first 64 bits are the same).
|
||||
Then the node may also use addresses from the prefix: `300:1111:2222:3333::/64` (note the `200` changed to `300`, a separate `/8` is used for prefixes, but the rest of the first 64 bits are the same).
|
||||
|
||||
To advertise this prefix and a route to `fd00::/8`, the following seems to work on the developers' networks:
|
||||
To advertise this prefix and a route to `200::/7`, the following seems to work on the developers' networks:
|
||||
|
||||
1. Enable IPv6 forwarding (e.g. `sysctl -w net.ipv6.conf.all.forwarding=1` or add it to sysctl.conf).
|
||||
|
||||
2. `ip addr add fd80:1111:2222:3333::1/64 dev eth0` or similar, to assign an address for the router to use in that prefix, where the LAN is reachable through `eth0`.
|
||||
2. `ip addr add 300:1111:2222:3333::1/64 dev eth0` or similar, to assign an address for the router to use in that prefix, where the LAN is reachable through `eth0`.
|
||||
|
||||
3. Install/run `radvd` with something like the following in `/etc/radvd.conf`:
|
||||
```
|
||||
interface eth0
|
||||
{
|
||||
AdvSendAdvert on;
|
||||
prefix fd80:1111:2222:3333::/64 {
|
||||
prefix 300:1111:2222:3333::/64 {
|
||||
AdvOnLink on;
|
||||
AdvAutonomous on;
|
||||
};
|
||||
route fd00::/8 {};
|
||||
route 200::/7 {};
|
||||
};
|
||||
```
|
||||
|
||||
|
43
build
43
build
@@ -1,11 +1,36 @@
|
||||
#!/bin/sh
|
||||
export GOPATH=$PWD
|
||||
echo "Downloading..."
|
||||
go get -d -v
|
||||
go get -d -v yggdrasil
|
||||
for file in *.go ; do
|
||||
echo "Building: $file"
|
||||
go build $@ $file
|
||||
#go build -ldflags="-s -w" -v $file
|
||||
#upx --brute ${file/.go/}
|
||||
|
||||
PKGSRC=${PKGSRC:-github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil}
|
||||
PKGNAME=${PKGNAME:-$(sh contrib/semver/name.sh)}
|
||||
PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)}
|
||||
|
||||
LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER"
|
||||
|
||||
while getopts "udtc:l:" option
|
||||
do
|
||||
case "${option}"
|
||||
in
|
||||
u) UPX=true;;
|
||||
d) DEBUG=true;;
|
||||
t) TABLES=true;;
|
||||
c) GCFLAGS="$GCFLAGS $OPTARG";;
|
||||
l) LDFLAGS="$LDFLAGS $OPTARG";;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z $TABLES ]; then
|
||||
STRIP="-s -w"
|
||||
fi
|
||||
|
||||
for CMD in `ls cmd/` ; do
|
||||
echo "Building: $CMD"
|
||||
|
||||
if [ $DEBUG ]; then
|
||||
go build -ldflags="$LDFLAGS" -gcflags="$GCFLAGS" -tags debug -v ./cmd/$CMD
|
||||
else
|
||||
go build -ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" -v ./cmd/$CMD
|
||||
fi
|
||||
if [ $UPX ]; then
|
||||
upx --brute $CMD
|
||||
fi
|
||||
done
|
||||
|
@@ -1,24 +1,30 @@
|
||||
package main
|
||||
|
||||
import "encoding/json"
|
||||
import "encoding/hex"
|
||||
import "flag"
|
||||
import "fmt"
|
||||
import "io/ioutil"
|
||||
import "os"
|
||||
import "os/signal"
|
||||
import "syscall"
|
||||
import "time"
|
||||
import "regexp"
|
||||
import "math/rand"
|
||||
import "log"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
import "yggdrasil"
|
||||
import "yggdrasil/config"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
|
||||
import "github.com/kardianos/minwinsvc"
|
||||
import "github.com/neilalexander/hjson-go"
|
||||
import "github.com/mitchellh/mapstructure"
|
||||
"github.com/kardianos/minwinsvc"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/neilalexander/hjson-go"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/config"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
|
||||
)
|
||||
|
||||
type nodeConfig = config.NodeConfig
|
||||
type Core = yggdrasil.Core
|
||||
@@ -48,26 +54,37 @@ func generateConfig(isAutoconf bool) *nodeConfig {
|
||||
r1 := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
cfg.Listen = fmt.Sprintf("[::]:%d", r1.Intn(65534-32768)+32768)
|
||||
}
|
||||
cfg.AdminListen = "localhost:9001"
|
||||
cfg.AdminListen = defaults.GetDefaults().DefaultAdminListen
|
||||
cfg.EncryptionPublicKey = hex.EncodeToString(bpub[:])
|
||||
cfg.EncryptionPrivateKey = hex.EncodeToString(bpriv[:])
|
||||
cfg.SigningPublicKey = hex.EncodeToString(spub[:])
|
||||
cfg.SigningPrivateKey = hex.EncodeToString(spriv[:])
|
||||
cfg.Peers = []string{}
|
||||
cfg.InterfacePeers = map[string][]string{}
|
||||
cfg.AllowedEncryptionPublicKeys = []string{}
|
||||
cfg.MulticastInterfaces = []string{".*"}
|
||||
cfg.IfName = core.GetTUNDefaultIfName()
|
||||
cfg.IfMTU = core.GetTUNDefaultIfMTU()
|
||||
cfg.IfTAPMode = core.GetTUNDefaultIfTAPMode()
|
||||
cfg.IfName = defaults.GetDefaults().DefaultIfName
|
||||
cfg.IfMTU = defaults.GetDefaults().DefaultIfMTU
|
||||
cfg.IfTAPMode = defaults.GetDefaults().DefaultIfTAPMode
|
||||
cfg.SessionFirewall.Enable = false
|
||||
cfg.SessionFirewall.AllowFromDirect = true
|
||||
cfg.SessionFirewall.AllowFromRemote = true
|
||||
cfg.SwitchOptions.MaxTotalQueueSize = yggdrasil.SwitchQueueTotalMinSize
|
||||
|
||||
return &cfg
|
||||
}
|
||||
|
||||
// Generates a new configuration and returns it in HJSON format. This is used
|
||||
// with -genconf.
|
||||
func doGenconf() string {
|
||||
func doGenconf(isjson bool) string {
|
||||
cfg := generateConfig(false)
|
||||
bs, err := hjson.Marshal(cfg)
|
||||
var bs []byte
|
||||
var err error
|
||||
if isjson {
|
||||
bs, err = json.MarshalIndent(cfg, "", " ")
|
||||
} else {
|
||||
bs, err = hjson.Marshal(cfg)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -77,16 +94,21 @@ func doGenconf() string {
|
||||
// The main function is responsible for configuring and starting Yggdrasil.
|
||||
func main() {
|
||||
// Configure the command line parameters.
|
||||
pprof := flag.Bool("pprof", false, "Run pprof, see http://localhost:6060/debug/pprof/")
|
||||
genconf := flag.Bool("genconf", false, "print a new config to stdout")
|
||||
useconf := flag.Bool("useconf", false, "read config from stdin")
|
||||
useconffile := flag.String("useconffile", "", "read config from specified file path")
|
||||
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
|
||||
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
|
||||
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
|
||||
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
|
||||
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
|
||||
version := flag.Bool("version", false, "prints the version of this build")
|
||||
flag.Parse()
|
||||
|
||||
var cfg *nodeConfig
|
||||
switch {
|
||||
case *version:
|
||||
fmt.Println("Build name:", yggdrasil.GetBuildName())
|
||||
fmt.Println("Build version:", yggdrasil.GetBuildVersion())
|
||||
os.Exit(0)
|
||||
case *autoconf:
|
||||
// Use an autoconf-generated config, this will give us random keys and
|
||||
// port numbers, and will use an automatically selected TUN/TAP interface.
|
||||
@@ -107,6 +129,19 @@ func main() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// If there's a byte order mark - which Windows 10 is now incredibly fond of
|
||||
// throwing everywhere when it's converting things into UTF-16 for the hell
|
||||
// of it - remove it and decode back down into UTF-8. This is necessary
|
||||
// because hjson doesn't know what to do with UTF-16 and will panic
|
||||
if bytes.Compare(config[0:2], []byte{0xFF, 0xFE}) == 0 ||
|
||||
bytes.Compare(config[0:2], []byte{0xFE, 0xFF}) == 0 {
|
||||
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
|
||||
decoder := utf.NewDecoder()
|
||||
config, err = decoder.Bytes(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
// Generate a new configuration - this gives us a set of sane defaults -
|
||||
// then parse the configuration we loaded above on top of it. The effect
|
||||
// of this is that any configuration item that is missing from the provided
|
||||
@@ -163,7 +198,12 @@ func main() {
|
||||
// their configuration file with newly mapped names (like above) or to
|
||||
// convert from plain JSON to commented HJSON.
|
||||
if *normaliseconf {
|
||||
bs, err := hjson.Marshal(cfg)
|
||||
var bs []byte
|
||||
if *confjson {
|
||||
bs, err = json.MarshalIndent(cfg, "", " ")
|
||||
} else {
|
||||
bs, err = hjson.Marshal(cfg)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -172,7 +212,7 @@ func main() {
|
||||
}
|
||||
case *genconf:
|
||||
// Generate a new configuration and print it to stdout.
|
||||
fmt.Println(doGenconf())
|
||||
fmt.Println(doGenconf(*confjson))
|
||||
default:
|
||||
// No flags were provided, therefore print the list of flags to stdout.
|
||||
flag.PrintDefaults()
|
||||
@@ -185,12 +225,6 @@ func main() {
|
||||
}
|
||||
// Create a new logger that logs output to stdout.
|
||||
logger := log.New(os.Stdout, "", log.Flags())
|
||||
// If the -pprof flag was provided then start the pprof service on port 6060.
|
||||
if *pprof {
|
||||
if err := yggdrasil.StartProfiler(logger); err != nil {
|
||||
logger.Println(err)
|
||||
}
|
||||
}
|
||||
// Setup the Yggdrasil node itself. The node{} type includes a Core, so we
|
||||
// don't need to create this manually.
|
||||
n := node{}
|
||||
@@ -219,14 +253,20 @@ func main() {
|
||||
// configure them. The loop ensures that disconnected peers will eventually
|
||||
// be reconnected with.
|
||||
go func() {
|
||||
if len(cfg.Peers) == 0 {
|
||||
if len(cfg.Peers) == 0 && len(cfg.InterfacePeers) == 0 {
|
||||
return
|
||||
}
|
||||
for {
|
||||
for _, p := range cfg.Peers {
|
||||
n.core.AddPeer(p)
|
||||
for _, peer := range cfg.Peers {
|
||||
n.core.AddPeer(peer, "")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
for intf, intfpeers := range cfg.InterfacePeers {
|
||||
for _, peer := range intfpeers {
|
||||
n.core.AddPeer(peer, intf)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Minute)
|
||||
}
|
||||
}()
|
401
cmd/yggdrasilctl/main.go
Normal file
401
cmd/yggdrasilctl/main.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
|
||||
"github.com/neilalexander/hjson-go"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
|
||||
)
|
||||
|
||||
type admin_info map[string]interface{}
|
||||
|
||||
func main() {
|
||||
logbuffer := &bytes.Buffer{}
|
||||
logger := log.New(logbuffer, "", log.Flags())
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Println("Fatal error:", r)
|
||||
fmt.Print(logbuffer)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
endpoint := defaults.GetDefaults().DefaultAdminListen
|
||||
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] command [key=value] [key=value] ...\n", os.Args[0])
|
||||
fmt.Println("Options:")
|
||||
flag.PrintDefaults()
|
||||
fmt.Println("Commands:\n - Use \"list\" for a list of available commands")
|
||||
fmt.Println("Examples:")
|
||||
fmt.Println(" - ", os.Args[0], "list")
|
||||
fmt.Println(" - ", os.Args[0], "getPeers")
|
||||
fmt.Println(" - ", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false")
|
||||
fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getDHT")
|
||||
fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT")
|
||||
}
|
||||
server := flag.String("endpoint", endpoint, "Admin socket endpoint")
|
||||
injson := flag.Bool("json", false, "Output in JSON format (as opposed to pretty-print)")
|
||||
verbose := flag.Bool("v", false, "Verbose output (includes public keys)")
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) == 0 {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if *server == endpoint {
|
||||
if config, err := ioutil.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil {
|
||||
if bytes.Compare(config[0:2], []byte{0xFF, 0xFE}) == 0 ||
|
||||
bytes.Compare(config[0:2], []byte{0xFE, 0xFF}) == 0 {
|
||||
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
|
||||
decoder := utf.NewDecoder()
|
||||
config, err = decoder.Bytes(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
var dat map[string]interface{}
|
||||
if err := hjson.Unmarshal(config, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") {
|
||||
endpoint = ep
|
||||
logger.Println("Found platform default config file", defaults.GetDefaults().DefaultConfigFile)
|
||||
logger.Println("Using endpoint", endpoint, "from AdminListen")
|
||||
} else {
|
||||
logger.Println("Configuration file doesn't contain appropriate AdminListen option")
|
||||
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen)
|
||||
}
|
||||
} else {
|
||||
logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile)
|
||||
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen)
|
||||
}
|
||||
} else {
|
||||
logger.Println("Using endpoint", endpoint, "from command line")
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
u, err := url.Parse(endpoint)
|
||||
if err == nil {
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "unix":
|
||||
logger.Println("Connecting to UNIX socket", endpoint[7:])
|
||||
conn, err = net.Dial("unix", endpoint[7:])
|
||||
case "tcp":
|
||||
logger.Println("Connecting to TCP socket", u.Host)
|
||||
conn, err = net.Dial("tcp", u.Host)
|
||||
default:
|
||||
logger.Println("Unknown protocol or malformed address - check your endpoint")
|
||||
err = errors.New("protocol not supported")
|
||||
}
|
||||
} else {
|
||||
logger.Println("Connecting to TCP socket", u.Host)
|
||||
conn, err = net.Dial("tcp", endpoint)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logger.Println("Connected")
|
||||
defer conn.Close()
|
||||
|
||||
decoder := json.NewDecoder(conn)
|
||||
encoder := json.NewEncoder(conn)
|
||||
send := make(admin_info)
|
||||
recv := make(admin_info)
|
||||
|
||||
for c, a := range args {
|
||||
if c == 0 {
|
||||
logger.Printf("Sending request: %v\n", a)
|
||||
send["request"] = a
|
||||
continue
|
||||
}
|
||||
tokens := strings.Split(a, "=")
|
||||
if i, err := strconv.Atoi(tokens[1]); err == nil {
|
||||
logger.Printf("Sending parameter %s: %d\n", tokens[0], i)
|
||||
send[tokens[0]] = i
|
||||
} else {
|
||||
switch strings.ToLower(tokens[1]) {
|
||||
case "true":
|
||||
send[tokens[0]] = true
|
||||
case "false":
|
||||
send[tokens[0]] = false
|
||||
default:
|
||||
send[tokens[0]] = tokens[1]
|
||||
}
|
||||
logger.Printf("Sending parameter %s: %v\n", tokens[0], send[tokens[0]])
|
||||
}
|
||||
}
|
||||
|
||||
if err := encoder.Encode(&send); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
logger.Printf("Request sent")
|
||||
if err := decoder.Decode(&recv); err == nil {
|
||||
logger.Printf("Response received")
|
||||
if recv["status"] == "error" {
|
||||
if err, ok := recv["error"]; ok {
|
||||
fmt.Println("Admin socket returned an error:", err)
|
||||
} else {
|
||||
fmt.Println("Admin socket returned an error but didn't specify any error text")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, ok := recv["request"]; !ok {
|
||||
fmt.Println("Missing request in response (malformed response?)")
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, ok := recv["response"]; !ok {
|
||||
fmt.Println("Missing response body (malformed response?)")
|
||||
os.Exit(1)
|
||||
}
|
||||
req := recv["request"].(map[string]interface{})
|
||||
res := recv["response"].(map[string]interface{})
|
||||
|
||||
if *injson {
|
||||
if json, err := json.MarshalIndent(res, "", " "); err == nil {
|
||||
fmt.Println(string(json))
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
switch strings.ToLower(req["request"].(string)) {
|
||||
case "dot":
|
||||
fmt.Println(res["dot"])
|
||||
case "list", "getpeers", "getswitchpeers", "getdht", "getsessions", "dhtping":
|
||||
maxWidths := make(map[string]int)
|
||||
var keyOrder []string
|
||||
keysOrdered := false
|
||||
|
||||
for _, tlv := range res {
|
||||
for slk, slv := range tlv.(map[string]interface{}) {
|
||||
if !keysOrdered {
|
||||
for k := range slv.(map[string]interface{}) {
|
||||
if !*verbose {
|
||||
if k == "box_pub_key" || k == "box_sig_key" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
keyOrder = append(keyOrder, fmt.Sprint(k))
|
||||
}
|
||||
sort.Strings(keyOrder)
|
||||
keysOrdered = true
|
||||
}
|
||||
for k, v := range slv.(map[string]interface{}) {
|
||||
if len(fmt.Sprint(slk)) > maxWidths["key"] {
|
||||
maxWidths["key"] = len(fmt.Sprint(slk))
|
||||
}
|
||||
if len(fmt.Sprint(v)) > maxWidths[k] {
|
||||
maxWidths[k] = len(fmt.Sprint(v))
|
||||
if maxWidths[k] < len(k) {
|
||||
maxWidths[k] = len(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(keyOrder) > 0 {
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", "")
|
||||
for _, v := range keyOrder {
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths[v])+"s ", v)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
for slk, slv := range tlv.(map[string]interface{}) {
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", slk)
|
||||
for _, k := range keyOrder {
|
||||
preformatted := slv.(map[string]interface{})[k]
|
||||
var formatted string
|
||||
switch k {
|
||||
case "bytes_sent", "bytes_recvd":
|
||||
formatted = fmt.Sprintf("%d", uint(preformatted.(float64)))
|
||||
case "uptime", "last_seen":
|
||||
seconds := uint(preformatted.(float64)) % 60
|
||||
minutes := uint(preformatted.(float64)/60) % 60
|
||||
hours := uint(preformatted.(float64) / 60 / 60)
|
||||
formatted = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
|
||||
default:
|
||||
formatted = fmt.Sprint(preformatted)
|
||||
}
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths[k])+"s ", formatted)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
case "gettuntap", "settuntap":
|
||||
for k, v := range res {
|
||||
fmt.Println("Interface name:", k)
|
||||
if mtu, ok := v.(map[string]interface{})["mtu"].(float64); ok {
|
||||
fmt.Println("Interface MTU:", mtu)
|
||||
}
|
||||
if tap_mode, ok := v.(map[string]interface{})["tap_mode"].(bool); ok {
|
||||
fmt.Println("TAP mode:", tap_mode)
|
||||
}
|
||||
}
|
||||
case "getself":
|
||||
for k, v := range res["self"].(map[string]interface{}) {
|
||||
if buildname, ok := v.(map[string]interface{})["build_name"].(string); ok && buildname != "unknown" {
|
||||
fmt.Println("Build name:", buildname)
|
||||
}
|
||||
if buildversion, ok := v.(map[string]interface{})["build_version"].(string); ok && buildversion != "unknown" {
|
||||
fmt.Println("Build version:", buildversion)
|
||||
}
|
||||
fmt.Println("IPv6 address:", k)
|
||||
if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok {
|
||||
fmt.Println("IPv6 subnet:", subnet)
|
||||
}
|
||||
if coords, ok := v.(map[string]interface{})["coords"].(string); ok {
|
||||
fmt.Println("Coords:", coords)
|
||||
}
|
||||
if *verbose {
|
||||
if boxPubKey, ok := v.(map[string]interface{})["box_pub_key"].(string); ok {
|
||||
fmt.Println("Public encryption key:", boxPubKey)
|
||||
}
|
||||
if boxSigKey, ok := v.(map[string]interface{})["box_sig_key"].(string); ok {
|
||||
fmt.Println("Public signing key:", boxSigKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "getswitchqueues":
|
||||
maximumqueuesize := float64(4194304)
|
||||
portqueues := make(map[float64]float64)
|
||||
portqueuesize := make(map[float64]float64)
|
||||
portqueuepackets := make(map[float64]float64)
|
||||
v := res["switchqueues"].(map[string]interface{})
|
||||
if queuecount, ok := v["queues_count"].(float64); ok {
|
||||
fmt.Printf("Active queue count: %d queues\n", uint(queuecount))
|
||||
}
|
||||
if queuesize, ok := v["queues_size"].(float64); ok {
|
||||
fmt.Printf("Active queue size: %d bytes\n", uint(queuesize))
|
||||
}
|
||||
if highestqueuecount, ok := v["highest_queues_count"].(float64); ok {
|
||||
fmt.Printf("Highest queue count: %d queues\n", uint(highestqueuecount))
|
||||
}
|
||||
if highestqueuesize, ok := v["highest_queues_size"].(float64); ok {
|
||||
fmt.Printf("Highest queue size: %d bytes\n", uint(highestqueuesize))
|
||||
}
|
||||
if m, ok := v["maximum_queues_size"].(float64); ok {
|
||||
maximumqueuesize = m
|
||||
fmt.Printf("Maximum queue size: %d bytes\n", uint(maximumqueuesize))
|
||||
}
|
||||
if queues, ok := v["queues"].([]interface{}); ok {
|
||||
if len(queues) != 0 {
|
||||
fmt.Println("Active queues:")
|
||||
for _, v := range queues {
|
||||
queueport := v.(map[string]interface{})["queue_port"].(float64)
|
||||
queuesize := v.(map[string]interface{})["queue_size"].(float64)
|
||||
queuepackets := v.(map[string]interface{})["queue_packets"].(float64)
|
||||
queueid := v.(map[string]interface{})["queue_id"].(string)
|
||||
portqueues[queueport]++
|
||||
portqueuesize[queueport] += queuesize
|
||||
portqueuepackets[queueport] += queuepackets
|
||||
queuesizepercent := (100 / maximumqueuesize) * queuesize
|
||||
fmt.Printf("- Switch port %d, Stream ID: %v, size: %d bytes (%d%% full), %d packets\n",
|
||||
uint(queueport), []byte(queueid), uint(queuesize),
|
||||
uint(queuesizepercent), uint(queuepackets))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(portqueuesize) > 0 && len(portqueuepackets) > 0 {
|
||||
fmt.Println("Aggregated statistics by switchport:")
|
||||
for k, v := range portqueuesize {
|
||||
queuesizepercent := (100 / (portqueues[k] * maximumqueuesize)) * v
|
||||
fmt.Printf("- Switch port %d, size: %d bytes (%d%% full), %d packets\n",
|
||||
uint(k), uint(v), uint(queuesizepercent), uint(portqueuepackets[k]))
|
||||
}
|
||||
}
|
||||
case "addpeer", "removepeer", "addallowedencryptionpublickey", "removeallowedencryptionpublickey", "addsourcesubnet", "addroute", "removesourcesubnet", "removeroute":
|
||||
if _, ok := res["added"]; ok {
|
||||
for _, v := range res["added"].([]interface{}) {
|
||||
fmt.Println("Added:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
if _, ok := res["not_added"]; ok {
|
||||
for _, v := range res["not_added"].([]interface{}) {
|
||||
fmt.Println("Not added:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
if _, ok := res["removed"]; ok {
|
||||
for _, v := range res["removed"].([]interface{}) {
|
||||
fmt.Println("Removed:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
if _, ok := res["not_removed"]; ok {
|
||||
for _, v := range res["not_removed"].([]interface{}) {
|
||||
fmt.Println("Not removed:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
case "getallowedencryptionpublickeys":
|
||||
if _, ok := res["allowed_box_pubs"]; !ok {
|
||||
fmt.Println("All connections are allowed")
|
||||
} else if res["allowed_box_pubs"] == nil {
|
||||
fmt.Println("All connections are allowed")
|
||||
} else {
|
||||
fmt.Println("Connections are allowed only from the following public box keys:")
|
||||
for _, v := range res["allowed_box_pubs"].([]interface{}) {
|
||||
fmt.Println("-", v)
|
||||
}
|
||||
}
|
||||
case "getmulticastinterfaces":
|
||||
if _, ok := res["multicast_interfaces"]; !ok {
|
||||
fmt.Println("No multicast interfaces found")
|
||||
} else if res["multicast_interfaces"] == nil {
|
||||
fmt.Println("No multicast interfaces found")
|
||||
} else {
|
||||
fmt.Println("Multicast peer discovery is active on:")
|
||||
for _, v := range res["multicast_interfaces"].([]interface{}) {
|
||||
fmt.Println("-", v)
|
||||
}
|
||||
}
|
||||
case "getsourcesubnets":
|
||||
if _, ok := res["source_subnets"]; !ok {
|
||||
fmt.Println("No source subnets found")
|
||||
} else if res["source_subnets"] == nil {
|
||||
fmt.Println("No source subnets found")
|
||||
} else {
|
||||
fmt.Println("Source subnets:")
|
||||
for _, v := range res["source_subnets"].([]interface{}) {
|
||||
fmt.Println("-", v)
|
||||
}
|
||||
}
|
||||
case "getroutes":
|
||||
if _, ok := res["routes"]; !ok {
|
||||
fmt.Println("No routes found")
|
||||
} else if res["routes"] == nil {
|
||||
fmt.Println("No routes found")
|
||||
} else {
|
||||
fmt.Println("Routes:")
|
||||
for _, v := range res["routes"].([]interface{}) {
|
||||
fmt.Println("-", v)
|
||||
}
|
||||
}
|
||||
default:
|
||||
if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil {
|
||||
fmt.Println(string(json))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Println("Error receiving response:", err)
|
||||
}
|
||||
|
||||
if v, ok := recv["status"]; ok && v != "success" {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
97
contrib/config/yggdrasilconf.go
Normal file
97
contrib/config/yggdrasilconf.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
This is a small utility that is designed to accompany the vyatta-yggdrasil
|
||||
package. It takes a HJSON configuration file, makes changes to it based on
|
||||
the command line arguments, and then spits out an updated file.
|
||||
*/
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
|
||||
"github.com/neilalexander/hjson-go"
|
||||
"golang.org/x/text/encoding/unicode"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/config"
|
||||
)
|
||||
|
||||
type nodeConfig = config.NodeConfig
|
||||
|
||||
func main() {
|
||||
useconffile := flag.String("useconffile", "/etc/yggdrasil.conf", "update config at specified file path")
|
||||
flag.Parse()
|
||||
cfg := nodeConfig{}
|
||||
var config []byte
|
||||
var err error
|
||||
config, err = ioutil.ReadFile(*useconffile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if bytes.Compare(config[0:2], []byte{0xFF, 0xFE}) == 0 ||
|
||||
bytes.Compare(config[0:2], []byte{0xFE, 0xFF}) == 0 {
|
||||
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
|
||||
decoder := utf.NewDecoder()
|
||||
config, err = decoder.Bytes(config)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
var dat map[string]interface{}
|
||||
if err := hjson.Unmarshal(config, &dat); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
confJson, err := json.Marshal(dat)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
json.Unmarshal(confJson, &cfg)
|
||||
switch flag.Arg(0) {
|
||||
case "setMTU":
|
||||
cfg.IfMTU, err = strconv.Atoi(flag.Arg(1))
|
||||
if err != nil {
|
||||
cfg.IfMTU = 1280
|
||||
}
|
||||
if mtu, _ := strconv.Atoi(flag.Arg(1)); mtu < 1280 {
|
||||
cfg.IfMTU = 1280
|
||||
}
|
||||
case "setIfName":
|
||||
cfg.IfName = flag.Arg(1)
|
||||
case "setListen":
|
||||
cfg.Listen = flag.Arg(1)
|
||||
case "setAdminListen":
|
||||
cfg.AdminListen = flag.Arg(1)
|
||||
case "setIfTapMode":
|
||||
if flag.Arg(1) == "true" {
|
||||
cfg.IfTAPMode = true
|
||||
} else {
|
||||
cfg.IfTAPMode = false
|
||||
}
|
||||
case "addPeer":
|
||||
found := false
|
||||
for _, v := range cfg.Peers {
|
||||
if v == flag.Arg(1) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cfg.Peers = append(cfg.Peers, flag.Arg(1))
|
||||
}
|
||||
case "removePeer":
|
||||
for k, v := range cfg.Peers {
|
||||
if v == flag.Arg(1) {
|
||||
cfg.Peers = append(cfg.Peers[:k], cfg.Peers[k+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
bs, err := hjson.Marshal(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(string(bs))
|
||||
return
|
||||
}
|
@@ -7,23 +7,29 @@
|
||||
if [ `pwd` != `git rev-parse --show-toplevel` ]
|
||||
then
|
||||
echo "You should run this script from the top-level directory of the git repo"
|
||||
exit -1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PKGBRANCH=$(basename `git name-rev --name-only HEAD`)
|
||||
PKGNAME=$(sh contrib/semver/name.sh)
|
||||
PKGVERSION=$(sh contrib/semver/version.sh | cut -c 2-)
|
||||
PKGVERSION=$(sh contrib/semver/version.sh --bare)
|
||||
PKGARCH=${PKGARCH-amd64}
|
||||
PKGFILE=$PKGNAME-$PKGVERSION-$PKGARCH.deb
|
||||
PKGREPLACES=yggdrasil
|
||||
|
||||
if [ $PKGBRANCH = "master" ]; then
|
||||
PKGREPLACES=yggdrasil-develop
|
||||
fi
|
||||
|
||||
if [ $PKGARCH = "amd64" ]; then GOARCH=amd64 GOOS=linux ./build
|
||||
elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build
|
||||
elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build
|
||||
elif [ $PKGARCH = "mips" ]; then GOARCH=mips64 GOOS=linux ./build
|
||||
elif [ $PKGARCH = "armhf" ]; then GOARCH=arm GOOS=linux GOARM=7 ./build
|
||||
elif [ $PKGARCH = "arm64" ]; then GOARCH=arm64 GOOS=linux ./build
|
||||
else
|
||||
echo "Specify PKGARCH=amd64,i386,mips,mipsel,armhf"
|
||||
exit -1
|
||||
echo "Specify PKGARCH=amd64,i386,mips,mipsel,armhf,arm64"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building $PKGFILE"
|
||||
@@ -34,7 +40,7 @@ mkdir -p /tmp/$PKGNAME/usr/bin/
|
||||
mkdir -p /tmp/$PKGNAME/etc/systemd/system/
|
||||
|
||||
cat > /tmp/$PKGNAME/debian/changelog << EOF
|
||||
Please see https://github.com/Arceliar/yggdrasil-go/
|
||||
Please see https://github.com/yggdrasil-network/yggdrasil-go/
|
||||
EOF
|
||||
echo 9 > /tmp/$PKGNAME/debian/compat
|
||||
cat > /tmp/$PKGNAME/debian/control << EOF
|
||||
@@ -43,15 +49,17 @@ Version: $PKGVERSION
|
||||
Section: contrib/net
|
||||
Priority: extra
|
||||
Architecture: $PKGARCH
|
||||
Replaces: $PKGREPLACES
|
||||
Conflicts: $PKGREPLACES
|
||||
Maintainer: Neil Alexander <neilalexander@users.noreply.github.com>
|
||||
Description: Debian yggdrasil package
|
||||
Binary yggdrasil package for Debian and Ubuntu
|
||||
EOF
|
||||
cat > /tmp/$PKGNAME/debian/copyright << EOF
|
||||
Please see https://github.com/Arceliar/yggdrasil-go/
|
||||
Please see https://github.com/yggdrasil-network/yggdrasil-go/
|
||||
EOF
|
||||
cat > /tmp/$PKGNAME/debian/docs << EOF
|
||||
Please see https://github.com/Arceliar/yggdrasil-go/
|
||||
Please see https://github.com/yggdrasil-network/yggdrasil-go/
|
||||
EOF
|
||||
cat > /tmp/$PKGNAME/debian/install << EOF
|
||||
usr/bin/yggdrasil usr/bin
|
||||
|
22
contrib/docker/Dockerfile
Normal file
22
contrib/docker/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM docker.io/golang:alpine as builder
|
||||
|
||||
COPY . /src
|
||||
WORKDIR /src
|
||||
RUN apk add git && ./build
|
||||
|
||||
FROM docker.io/alpine
|
||||
LABEL maintainer="Christer Waren/CWINFO <christer.waren@cwinfo.org>"
|
||||
|
||||
COPY --from=builder /src/yggdrasil /usr/bin/yggdrasil
|
||||
COPY --from=builder /src/yggdrasilctl /usr/bin/yggdrasilctl
|
||||
COPY contrib/docker/entrypoint.sh /usr/bin/entrypoint.sh
|
||||
|
||||
# RUN addgroup -g 1000 -S yggdrasil-network \
|
||||
# && adduser -u 1000 -S -g 1000 --home /etc/yggdrasil-network yggdrasil-network
|
||||
#
|
||||
# USER yggdrasil-network
|
||||
# TODO: Make running unprivileged work
|
||||
|
||||
VOLUME [ "/etc/yggdrasil-network" ]
|
||||
|
||||
ENTRYPOINT [ "/usr/bin/entrypoint.sh" ]
|
13
contrib/docker/entrypoint.sh
Executable file
13
contrib/docker/entrypoint.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -e
|
||||
|
||||
CONF_DIR="/etc/yggdrasil-network"
|
||||
|
||||
if [ ! -f "$CONF_DIR/config.conf" ]; then
|
||||
echo "generate $CONF_DIR/config.conf"
|
||||
yggdrasil --genconf > "$CONF_DIR/config.conf"
|
||||
fi
|
||||
|
||||
yggdrasil --useconf < "$CONF_DIR/config.conf"
|
||||
exit $?
|
72
contrib/freebsd/yggdrasil
Normal file
72
contrib/freebsd/yggdrasil
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Put the yggdrasil and yggdrasilctl binaries into /usr/local/bin
|
||||
# Then copy this script into /etc/rc.d/yggdrasil
|
||||
# Finally, run:
|
||||
# 1. chmod +x /etc/rc.d/yggdrasil /usr/local/bin/{yggdrasil,yggdrasilctl}
|
||||
# 2. echo "yggdrasil_enable=yes" >> /etc/rc.d
|
||||
# 3. service yggdrasil start
|
||||
#
|
||||
# PROVIDE: yggdrasil
|
||||
# REQUIRE: networking
|
||||
# KEYWORD:
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="yggdrasil"
|
||||
rcvar="yggdrasil_enable"
|
||||
|
||||
start_cmd="${name}_start"
|
||||
stop_cmd="${name}_stop"
|
||||
|
||||
pidfile="/var/run/yggdrasil/${name}.pid"
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-P ${pidfile} -r -f ${yggdrasil_command}"
|
||||
|
||||
yggdrasil_start()
|
||||
{
|
||||
test ! -x /usr/local/bin/yggdrasil && (
|
||||
logger -s -t yggdrasil "Warning: /usr/local/bin/yggdrasil is missing or not executable"
|
||||
logger -s -t yggdrasil "Copy the yggdrasil binary into /usr/local/bin and then chmod +x /usr/local/bin/yggdrasil"
|
||||
return 1
|
||||
)
|
||||
|
||||
test ! -f /etc/yggdrasil.conf && (
|
||||
logger -s -t yggdrasil "Generating new configuration file into /etc/yggdrasil.conf"
|
||||
/usr/local/bin/yggdrasil -genconf > /etc/yggdrasil.conf
|
||||
)
|
||||
|
||||
tap_path="$(cat /etc/yggdrasil.conf | egrep -o '/dev/tap[0-9]{1,2}$')"
|
||||
tap_name="$(echo -n ${tap_path} | tr -d '/dev/')"
|
||||
|
||||
/sbin/ifconfig ${tap_name} >/dev/null 2>&1 || (
|
||||
logger -s -t yggdrasil "Creating ${tap_name} adapter"
|
||||
/sbin/ifconfig ${tap_name} create || logger -s -t yggdrasil "Failed to create ${tap_name} adapter"
|
||||
)
|
||||
|
||||
test ! -d /var/run/yggdrasil && mkdir -p /var/run/yggdrasil
|
||||
|
||||
logger -s -t yggdrasil "Starting yggdrasil"
|
||||
${command} ${command_args} /usr/local/bin/yggdrasil -useconffile /etc/yggdrasil.conf \
|
||||
1>/var/log/yggdrasil.stdout.log \
|
||||
2>/var/log/yggdrasil.stderr.log &
|
||||
}
|
||||
|
||||
yggdrasil_stop()
|
||||
{
|
||||
logger -s -t yggdrasil "Stopping yggdrasil"
|
||||
test -f /var/run/yggdrasil/${name}.pid && kill -TERM $(cat /var/run/yggdrasil/${name}.pid)
|
||||
|
||||
tap_path="$(cat /etc/yggdrasil.conf | grep /dev/tap | egrep -o '/dev/.*$')"
|
||||
tap_name="$(echo -n ${tap_path} | tr -d '/dev/')"
|
||||
|
||||
/sbin/ifconfig ${tap_name} >/dev/null 2>&1 && (
|
||||
logger -s -t yggdrasil "Destroying ${tap_name} adapter"
|
||||
/sbin/ifconfig ${tap_name} destroy || logger -s -t yggdrasil "Failed to destroy ${tap_name} adapter"
|
||||
)
|
||||
}
|
||||
|
||||
load_rc_config $name
|
||||
: ${yggdrasil_enable:=no}
|
||||
|
||||
run_rc_command "$1"
|
122
contrib/macos/create-pkg.sh
Executable file
122
contrib/macos/create-pkg.sh
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Check if xar and mkbom are available
|
||||
command -v xar >/dev/null 2>&1 || (
|
||||
echo "Building xar"
|
||||
sudo apt-get install libxml2-dev libssl1.0-dev zlib1g-dev -y
|
||||
mkdir -p /tmp/xar && cd /tmp/xar
|
||||
git clone https://github.com/mackyle/xar && cd xar/xar
|
||||
(sh autogen.sh && make && sudo make install) || (echo "Failed to build xar"; exit 1)
|
||||
)
|
||||
command -v mkbom >/dev/null 2>&1 || (
|
||||
echo "Building mkbom"
|
||||
mkdir -p /tmp/mkbom && cd /tmp/mkbom
|
||||
git clone https://github.com/hogliux/bomutils && cd bomutils
|
||||
sudo make install || (echo "Failed to build mkbom"; exit 1)
|
||||
)
|
||||
|
||||
# Check if we can find the files we need - they should
|
||||
# exist if you are running this script from the root of
|
||||
# the yggdrasil-go repo and you have ran ./build
|
||||
test -f yggdrasil || (echo "yggdrasil binary not found"; exit 1)
|
||||
test -f yggdrasilctl || (echo "yggdrasilctl binary not found"; exit 1)
|
||||
test -f contrib/macos/yggdrasil.plist || (echo "contrib/macos/yggdrasil.plist not found"; exit 1)
|
||||
test -f contrib/semver/version.sh || (echo "contrib/semver/version.sh not found"; exit 1)
|
||||
|
||||
# Delete the pkgbuild folder if it already exists
|
||||
test -d pkgbuild && rm -rf pkgbuild
|
||||
|
||||
# Create our folder structure
|
||||
mkdir -p pkgbuild/scripts
|
||||
mkdir -p pkgbuild/flat/base.pkg
|
||||
mkdir -p pkgbuild/flat/Resources/en.lproj
|
||||
mkdir -p pkgbuild/root/usr/local/bin
|
||||
mkdir -p pkgbuild/root/Library/LaunchDaemons
|
||||
|
||||
# Copy package contents into the pkgbuild root
|
||||
cp yggdrasil pkgbuild/root/usr/local/bin
|
||||
cp yggdrasilctl pkgbuild/root/usr/local/bin
|
||||
cp contrib/macos/yggdrasil.plist pkgbuild/root/Library/LaunchDaemons
|
||||
|
||||
# Create the postinstall script
|
||||
cat > pkgbuild/scripts/postinstall << EOF
|
||||
#!/bin/sh
|
||||
|
||||
# Normalise the config if it exists, generate it if it doesn't
|
||||
if [ -f /etc/yggdrasil.conf ];
|
||||
then
|
||||
mkdir -p /Library/Preferences/Yggdrasil
|
||||
echo "Backing up configuration file to /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d`"
|
||||
cp /etc/yggdrasil.conf /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d`
|
||||
echo "Normalising /etc/yggdrasil.conf"
|
||||
/usr/local/bin/yggdrasil -useconffile /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d` -normaliseconf > /etc/yggdrasil.conf
|
||||
else
|
||||
/usr/local/bin/yggdrasil -genconf > /etc/yggdrasil.conf
|
||||
fi
|
||||
|
||||
# Unload existing Yggdrasil launchd service, if possible
|
||||
test -f /Library/LaunchDaemons/yggdrasil.plist && (launchctl unload /Library/LaunchDaemons/yggdrasil.plist || true)
|
||||
|
||||
# Load Yggdrasil launchd service and start Yggdrasil
|
||||
launchctl load /Library/LaunchDaemons/yggdrasil.plist
|
||||
EOF
|
||||
|
||||
# Set execution permissions
|
||||
chmod +x pkgbuild/scripts/postinstall
|
||||
chmod +x pkgbuild/root/usr/local/bin/yggdrasil
|
||||
chmod +x pkgbuild/root/usr/local/bin/yggdrasilctl
|
||||
|
||||
# Pack payload and scripts
|
||||
( cd pkgbuild/scripts && find . | cpio -o --format odc --owner 0:80 | gzip -c ) > pkgbuild/flat/base.pkg/Scripts
|
||||
( cd pkgbuild/root && find . | cpio -o --format odc --owner 0:80 | gzip -c ) > pkgbuild/flat/base.pkg/Payload
|
||||
|
||||
# Work out metadata for the package info
|
||||
PKGNAME=$(sh contrib/semver/name.sh)
|
||||
PKGVERSION=$(sh contrib/semver/version.sh --bare)
|
||||
PKGARCH=${PKGARCH-amd64}
|
||||
PAYLOADSIZE=$(( $(wc -c pkgbuild/flat/base.pkg/Payload | awk '{ print $1 }') / 1024 ))
|
||||
|
||||
# Create the PackageInfo file
|
||||
cat > pkgbuild/flat/base.pkg/PackageInfo << EOF
|
||||
<pkg-info format-version="2" identifier="io.github.yggdrasil-network.pkg" version="${PKGVERSION}" install-location="/" auth="root">
|
||||
<payload installKBytes="${PAYLOADSIZE}" numberOfFiles="3"/>
|
||||
<scripts>
|
||||
<postinstall file="./postinstall"/>
|
||||
</scripts>
|
||||
</pkg-info>
|
||||
EOF
|
||||
|
||||
# Create the BOM
|
||||
( cd pkgbuild && mkbom root flat/base.pkg/Bom )
|
||||
|
||||
# Create the Distribution file
|
||||
cat > pkgbuild/flat/Distribution << EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-script minSpecVersion="1.000000" authoringTool="com.apple.PackageMaker" authoringToolVersion="3.0.3" authoringToolBuild="174">
|
||||
<title>Yggdrasil (${PKGNAME}-${PKGVERSION})</title>
|
||||
<options customize="never" allow-external-scripts="no"/>
|
||||
<domains enable_anywhere="true"/>
|
||||
<installation-check script="pm_install_check();"/>
|
||||
<script>
|
||||
function pm_install_check() {
|
||||
if(!(system.compareVersions(system.version.ProductVersion,'10.10') >= 0)) {
|
||||
my.result.title = 'Failure';
|
||||
my.result.message = 'You need at least Mac OS X 10.10 to install Yggdrasil.';
|
||||
my.result.type = 'Fatal';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
</script>
|
||||
<choices-outline>
|
||||
<line choice="choice1"/>
|
||||
</choices-outline>
|
||||
<choice id="choice1" title="base">
|
||||
<pkg-ref id="io.github.yggdrasil-network.pkg"/>
|
||||
</choice>
|
||||
<pkg-ref id="io.github.yggdrasil-network.pkg" installKBytes="${PAYLOADSIZE}" version="${VERSION}" auth="Root">#base.pkg</pkg-ref>
|
||||
</installer-script>
|
||||
EOF
|
||||
|
||||
# Finally pack the .pkg
|
||||
( cd pkgbuild/flat && xar --compression none -cf "../../${PKGNAME}-${PKGVERSION}-macos-${PKGARCH}.pkg" * )
|
@@ -8,7 +8,7 @@
|
||||
<array>
|
||||
<string>sh</string>
|
||||
<string>-c</string>
|
||||
<string>/usr/bin/yggdrasil -useconf < /etc/yggdrasil.conf</string>
|
||||
<string>/usr/local/bin/yggdrasil -useconf < /etc/yggdrasil.conf</string>
|
||||
</array>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
47
contrib/rpm/yggdrasil.spec
Normal file
47
contrib/rpm/yggdrasil.spec
Normal file
@@ -0,0 +1,47 @@
|
||||
Name: yggdrasil
|
||||
Version: 0.3.0
|
||||
Release: 1%{?dist}
|
||||
Summary: End-to-end encrypted IPv6 networking
|
||||
|
||||
License: GPLv3
|
||||
URL: https://yggdrasil-network.github.io
|
||||
Source0: https://codeload.github.com/yggdrasil-network/yggdrasil-go/tar.gz/v0.3.0
|
||||
|
||||
%{?systemd_requires}
|
||||
BuildRequires: systemd golang >= 1.11
|
||||
|
||||
%description
|
||||
Yggdrasil is a proof-of-concept to explore a wholly different approach to
|
||||
network routing. Whereas current computer networks depend heavily on very
|
||||
centralised design and configuration, Yggdrasil breaks this mould by making
|
||||
use of a global spanning tree to form a scalable IPv6 encrypted mesh network.
|
||||
|
||||
%prep
|
||||
%setup -qn yggdrasil-go-%{version}
|
||||
|
||||
%build
|
||||
./build -t -l "-linkmode=external"
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
mkdir -p %{buildroot}/%{_bindir}
|
||||
mkdir -p %{buildroot}/%{_sysconfdir}/systemd/system
|
||||
install -m 0755 yggdrasil %{buildroot}/%{_bindir}/yggdrasil
|
||||
install -m 0755 yggdrasilctl %{buildroot}/%{_bindir}/yggdrasilctl
|
||||
install -m 0755 contrib/systemd/yggdrasil.service %{buildroot}/%{_sysconfdir}/systemd/system/yggdrasil.service
|
||||
install -m 0755 contrib/systemd/yggdrasil-resume.service %{buildroot}/%{_sysconfdir}/systemd/system/yggdrasil-resume.service
|
||||
|
||||
%files
|
||||
%{_bindir}/yggdrasil
|
||||
%{_bindir}/yggdrasilctl
|
||||
%{_sysconfdir}/systemd/system/yggdrasil.service
|
||||
%{_sysconfdir}/systemd/system/yggdrasil-resume.service
|
||||
|
||||
%post
|
||||
%systemd_post yggdrasil.service
|
||||
|
||||
%preun
|
||||
%systemd_preun yggdrasil.service
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart yggdrasil.service
|
@@ -1,7 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Get the branch name, removing any "/" characters from pull requests
|
||||
BRANCH=$(git symbolic-ref --short HEAD | tr -d "/" 2>/dev/null)
|
||||
# Get the current branch name
|
||||
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
||||
|
||||
# Complain if the git history is not available
|
||||
if [ $? != 0 ]; then
|
||||
printf "unknown"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Remove "/" characters from the branch name if present
|
||||
BRANCH=$(echo $BRANCH | tr -d "/")
|
||||
|
||||
# Check if the branch name is not master
|
||||
if [ "$BRANCH" = "master" ]; then
|
||||
|
@@ -1,26 +1,62 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Get the last tag
|
||||
TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*" 2>/dev/null)
|
||||
# Merge commits from this branch are counted
|
||||
DEVELOPBRANCH="yggdrasil-network/develop"
|
||||
|
||||
# Get the number of commits from the last tag
|
||||
COUNT=$(git rev-list $TAG..HEAD --count 2>/dev/null)
|
||||
# Get the last tag
|
||||
TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*\.0" 2>/dev/null)
|
||||
|
||||
# Get last merge to master
|
||||
MERGE=$(git rev-list $TAG..master --grep "from $DEVELOPBRANCH" 2>/dev/null | head -n 1)
|
||||
|
||||
# Get the number of merges since the last merge to master
|
||||
PATCH=$(git rev-list $TAG..master --count --merges --grep="from $DEVELOPBRANCH" 2>/dev/null)
|
||||
|
||||
# Decide whether we should prepend the version with "v" - the default is that
|
||||
# we do because we use it in git tags, but we might not always need it
|
||||
PREPEND="v"
|
||||
if [ "$1" = "--bare" ]; then
|
||||
PREPEND=""
|
||||
fi
|
||||
|
||||
# If it fails then there's no last tag - go from the first commit
|
||||
if [ $? != 0 ]; then
|
||||
COUNT=$(git rev-list HEAD --count 2>/dev/null)
|
||||
PATCH=$(git rev-list HEAD --count 2>/dev/null)
|
||||
|
||||
printf 'v0.0.%d' "$COUNT"
|
||||
exit -1
|
||||
# Complain if the git history is not available
|
||||
if [ $? != 0 ]; then
|
||||
printf 'unknown'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s0.0.%d' "$PREPEND" "$PATCH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the number of merges on the current branch since the last tag
|
||||
BUILD=$(git rev-list $TAG..HEAD --count --merges)
|
||||
|
||||
# Split out into major, minor and patch numbers
|
||||
MAJOR=$(echo $TAG | cut -c 2- | cut -d "." -f 1)
|
||||
MINOR=$(echo $TAG | cut -c 2- | cut -d "." -f 2)
|
||||
|
||||
# Get the current checked out branch
|
||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
||||
|
||||
# Output in the desired format
|
||||
if [ $COUNT = 0 ]; then
|
||||
printf 'v%d.%d' "$MAJOR" "$MINOR"
|
||||
if [ $PATCH = 0 ]; then
|
||||
if [ ! -z $FULL ]; then
|
||||
printf '%s%d.%d.0' "$PREPEND" "$MAJOR" "$MINOR"
|
||||
else
|
||||
printf '%s%d.%d' "$PREPEND" "$MAJOR" "$MINOR"
|
||||
fi
|
||||
else
|
||||
printf 'v%d.%d.%d' "$MAJOR" "$MINOR" "$COUNT"
|
||||
printf '%s%d.%d.%d' "$PREPEND" "$MAJOR" "$MINOR" "$PATCH"
|
||||
fi
|
||||
|
||||
# Add the build tag on non-master branches
|
||||
if [ $BRANCH != "master" ]; then
|
||||
if [ $BUILD != 0 ]; then
|
||||
printf -- "-%04d" "$BUILD"
|
||||
fi
|
||||
fi
|
||||
|
188
doc/README.md
188
doc/README.md
@@ -1,188 +0,0 @@
|
||||
# Yggdrasil-go
|
||||
|
||||
## What is it?
|
||||
|
||||
This is a toy implementation of an encrypted IPv6 network.
|
||||
A number of years ago, I started to spend some of my free time studying and routing schemes, and eventually decided that it made sense to come up with my own.
|
||||
After much time spent reflecting on the problem, and a few failed starts, I eventually cobbled together one that seemed to have, more or less, the performance characteristics I was looking for.
|
||||
I resolved to eventually write a proof-of-principle / test implementation, and I thought it would make sense to include many of the nice bells and whistles that I've grown accustomed to from using [cjdns](https://github.com/cjdelisle/cjdns), plus a few additional features that I wanted to test.
|
||||
Fast forward through a couple years of procrastination, and I've finally started working on it in my limited spare time.
|
||||
I've found that it's now marginally more interesting than embarrassing, so here it is.
|
||||
|
||||
The routing scheme was designed for scalable name-independent routing on graphs with an internet-like topology.
|
||||
By internet-like, I mean that the network has a densely connected core with many triangles, a diameter that increases slowly with network size, and where any sparse edges tend to be relatively tree-like, all of which appear to be common features of large graphs describing "organically" grown relationships.
|
||||
By scalable name-independent routing, I mean:
|
||||
|
||||
1. Scalable: resource consumption should grow slowly with the size of the network.
|
||||
In particular, for internet-like networks, the goal is to use only a (poly)logarithmic amount of memory, use a logarithmic amount of bandwidth per one-hop neighbor for control traffic, and to maintain low average multiplicative path stretch (introducing overhead of perhaps a few percent) that does not become worse as the network grows.
|
||||
|
||||
2. Name-independent: a node's identifier should be independent of network topology and state, such that a node may freely change their identifier in a static network, or keep it static under state changes in a dynamic network.
|
||||
In particular, addresses are self-assigned and derived from a public key, which circumvents the use of a centralized addressing authority or public key infrastructure.
|
||||
|
||||
Running this code will:
|
||||
|
||||
1. Set up a `tun` device and assign it a Unique Local Address (ULA) in `fd00::/8`.
|
||||
2. Connect to other nodes running the software.
|
||||
3. Route traffic for and through other nodes.
|
||||
|
||||
A device's ULA is actually from `fd00::/9`, and a matching `/64` prefix is available under `fd80::/9`. This allows the node to advertise a route on its LAN, as a workaround for unsupported devices.
|
||||
|
||||
## Building
|
||||
|
||||
1. Install Go (tested on 1.9, I use [godeb](https://github.com/niemeyer/godeb)).
|
||||
2. Clone this repository.
|
||||
2. `./build`
|
||||
|
||||
It's written in Go because I felt like learning a new language, and Go seemed like an easy language to learn while still being a reasonable choice for language to prototype network code.
|
||||
Note that the build script defines its own `$GOPATH`, so the build and its dependencies should be self contained.
|
||||
It only works on Linux at this time, because a little code (related to the `tun` device) is platform dependent, and changing that hasn't been a high priority.
|
||||
|
||||
## Running
|
||||
|
||||
To run the program, you'll need permission to create a `tun` device and configure it using `ip`.
|
||||
If you don't want to mess with capabilities for the `tun` device, then using `sudo` should work, with the usual security caveats about running a program as root.
|
||||
|
||||
To run with default settings:
|
||||
|
||||
1. `./yggdrasil --autoconf`
|
||||
|
||||
That will generate a new set of keys (and an IP address) each time the program is run.
|
||||
The program will bind to all addresses on a random port and listen for incoming connections.
|
||||
It will send announcements over IPv6 link-local multicast, and attempt to start a connection if it hears an announcement from another device.
|
||||
|
||||
In practice, you probably want to run this instead:
|
||||
|
||||
1. `./yggdrasil --genconf > conf.json`
|
||||
2. `./yggdrasil --useconf < conf.json`
|
||||
|
||||
The first step generates a configuration file with a set of cryptographic keys and default settings.
|
||||
The second step runs the program using the configuration provided in that file.
|
||||
Because ULAs are derived from keys, using a fixed set of keys causes a node to keep the same address each time the program is run.
|
||||
|
||||
If you want to use it as an overlay network on top of e.g. the internet, then you can do so by adding the address and port of the device you want to connect to (as a string, e.g. `"1.2.3.4:5678"`) to the list of `Peers` in the configuration file.
|
||||
This should accept IPv4 and IPv6 addresses, and I think it should resolve host/domain names, but I haven't really tested that, so your mileage may vary.
|
||||
You can also configure which address and/or port to listen on by editing the configuration file, in case you want to bind to a specific address or listen for incoming connections on a fixed port.
|
||||
|
||||
Also note that the nodes is connected to the network through a `tun` device, so it follows point-to-point semantics.
|
||||
This means it's limited to routing traffic with source and destination addresses in `fd00::/8`--you can't add a prefix to your routing table "via" an address in that range, as the router has no idea who you meant to send it to.
|
||||
In particular, this means you can't set a working default route that *directly* uses the overlay network, but I've had success *indirectly* using it to connect to an off-the-shelf VPN that I can use as a default route for internet access.
|
||||
|
||||
## Optional: advertise a prefix locally
|
||||
|
||||
Suppose a node has been given the address: `fd00:1111:2222:3333:4444:5555:6666:7777`
|
||||
|
||||
Then the node may also use addresses from the prefix: `fd80:1111:2222:3333::/64` (note the `fd00` -> `fd80`, a separate `/9` is used for prefixes).
|
||||
|
||||
To advertise this prefix and a route to `fd00::/8`, the following seems to work for me:
|
||||
|
||||
1. Enable IPv6 forwarding (e.g. `sysctl -w net.ipv6.conf.all.forwarding=1` or add it to sysctl.conf).
|
||||
|
||||
2. `ip addr add fd80:1111:2222:3333::1/64 dev eth0` or similar, to assign an address for the router to use in that prefix, where the LAN is reachable through `eth0`.
|
||||
|
||||
3. Install/run `radvd` with something like the following in `/etc/radvd.conf`:
|
||||
```
|
||||
interface eth0
|
||||
{
|
||||
AdvSendAdvert on;
|
||||
prefix fd80:1111:2222:3333::/64 {
|
||||
AdvOnLink on;
|
||||
AdvAutonomous on;
|
||||
};
|
||||
route fd00::/8 {};
|
||||
};
|
||||
```
|
||||
|
||||
Now any IPv6-enabled device in the LAN can use stateless address auto-configuration to assign itself a working `fd00::/8` address from the `/64` prefix, and communicate with the wider network through the router, without requiring any special configuration for each device.
|
||||
I've used this to e.g. get my phone on the network.
|
||||
Note that there are a some differences when accessing the network this way:
|
||||
|
||||
1. There are 64 fewer bits of address space available for self-certifying addresses.
|
||||
This means that it is 64 bits easier to brute force a prefix collision than collision for a full node's IP address. As such, you may want to change addresses frequently, or else brute force an address with more security bits (see: `misc/genkeys.go`).
|
||||
|
||||
2. The LAN depends on the router for cryptography.
|
||||
So while traffic going through the WAN is encrypted, the LAN is still just a LAN. You may want to secure your network.
|
||||
|
||||
3. Related to the above, the cryptography and I/O through the `tun` device both place additional load on the router, above what is normally present from forwarding packets between full nodes in the network, so the router may need more computing power to reach line rate.
|
||||
|
||||
## How does it work?
|
||||
|
||||
Consider the internet, which uses a network-of-networks model with address aggregation.
|
||||
Addresses are allocated by a central authority, as blocks of contiguous addresses with a matching prefix.
|
||||
Within a network, each node may represent one or more prefixes, with each prefix representing a network of one or more nodes.
|
||||
On the largest scale, BGP is used to route traffic between networks (autonomous systems), and other protocols can be used to route within a network.
|
||||
The effectiveness of such hierarchical addressing and routing strategies depend on network topology, with the internet's observed topology being the worst case of all known topologies from a scalability standpoint (see [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) for a better explanation of the issue, but the problem is essentially that address aggregation is ineffective in a network with a large number of nodes and a small diameter).
|
||||
|
||||
The routing scheme implemented by this code tries a different approach.
|
||||
Instead of using assigned addresses and a routing table based on prefixes and address aggregation, routing and addressing are handled through a combination of:
|
||||
|
||||
1. Self-assigned cryptographically generated addresses, to handle address allocation without a central authority.
|
||||
2. A kademlia-like distributed hash table, to look up a node's (name-dependent) routing information from their (name-independent routing) IP address.
|
||||
3. A name-dependent routing scheme based on greedy routing in a metric space, constructed from an arbitrarily rooted spanning tree, which gives a reasonable approximation of the true distance between nodes for certain network topologies (namely the scale-free topology that seems to emerge in many large graphs, including the internet). The spanning tree embedding takes stability into account when selecting which one-hop neighbor to use as a parent, and path selection uses (poorly) estimated available bandwidth as a criteria, subject to the constraint that metric space distances must decrease with each hop. Incidentally, the name `yggdrasil` was selected for this test code because that's obviously what you call an immense tree that connects worlds.
|
||||
|
||||
The network then presents itself as having a single "flat" address with no aggregation.
|
||||
Under the hood, it runs as an overlay on top of existing IP networks.
|
||||
Link-local IPv6 multicast traffic is used to advertise on the underlying networks, which can as easily be a wired or wireless LAN, a direct (e.g. ethernet) connection between two devices, a wireless ad-hoc network, etc.
|
||||
Additional connections can be added manually to peer over networks where link-local multicast is insufficient, which allows you to e.g. use the internet to bridge local networks.
|
||||
|
||||
The name-dependent routing layer uses cryptographically signed (`Ed25519`) path-vector-like routing messages, similar to S-BGP, which should prevent route poisoning and related attacks.
|
||||
For encryption, it uses the Go implementation of the `nacl/box` scheme, which is built from a Curve25519 key exchange with XSalsa20 as a stream cypher and Poly1305 for integrity and authentication.
|
||||
Permanent keys are used for protocol traffic, including the ephemeral key exchange, and a hash of a node's permanent public key is used to construct a node's address.
|
||||
Ephemeral keys are used for encapsulated IP(v6) traffic, which provides forward secrecy.
|
||||
Go's `crypto/rand` library is used for nonce generation.
|
||||
In short, I've tried to not make this a complete security disaster, but the code hasn't been independently audited and I'm nothing close to a security expert, so it should be considered a proof-of-principle rather than a safe implementation.
|
||||
At a minimum, I know of no way to prevent gray hole attacks.
|
||||
|
||||
I realize that this is a terribly short description of how it works, so I may elaborate further in another document if the need arises.
|
||||
Otherwise, I guess you could try to read my terrible and poorly documented code if you want to know more.
|
||||
|
||||
## Related work
|
||||
|
||||
A lot of inspiration comes from [cjdns](https://github.com/cjdelisle/cjdns).
|
||||
I'm a contributor to that project, and I wanted to test out some ideas that weren't convenient to prototype in the existing code base, which is why I wrote this toy.
|
||||
|
||||
On the routing side, a lot of influence came from compact routing.
|
||||
A number of compact routing schemes are evaluated in [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) and may be used as a basis for comparison.
|
||||
When tested in a simplified simulation environment on CAIDA's 9204-node "skitter" network graph used in that paper, I observed an average multiplicative stretch of about 1.08 with my routing scheme, as implemented here.
|
||||
This can be lowered to less than 1.02 using a source-routed version of the algorithm and including node degree as an additional parameter of the embedding, which is of academic interest, but degree's unverifiability makes it impractical for this implementation.
|
||||
In either case, this only requires 1 routing table entry per one-hop neighbor (this averages ~6 for in the skitter network graph), plus a logarithmic number of DHT entries (expected to be ~26, based on extrapolations from networks with a few hundred nodes--running the full implementation on the skitter graph is impractical on my machine).
|
||||
I don't think stretch is really an appropriate metric, as it doesn't consider the difference to total network cost from a high-stretch short path vs a high-stretch long path.
|
||||
In this scheme, and I believe in most compact routing schemes, longer paths tend to have lower multiplicative stretch, and shorter paths are more likely to have longer stretch.
|
||||
I would argue that this is preferable to the alternative.
|
||||
|
||||
While I use a slightly different approach, the idea to try a greedy routing scheme was inspired by the use of greedy routing on networks embedded in the hyperbolic plane (such as [Kleinberg's work](https://doi.org/10.1109%2FINFCOM.2007.221) and [Greedy Forwarding on the NDN Testbed](https://www.caida.org/research/routing/greedy_forwarding_ndn/)).
|
||||
I use distance on a spanning tree as the metric, as seems to work well on the types of networks I'm concerned with, and it simplifies other aspects of the implementation.
|
||||
The hyperbolic embedding algorithms I'm aware of, or specifically the distributed ones, operate by constructing a spanning tree of the network and then embedding the tree.
|
||||
So I don't see much harm, at present, of skipping the hyperbolic plane and directly using the tree for the metric space.
|
||||
|
||||
## Misc. notes
|
||||
|
||||
This is a toy experiment / proof-of-concept.
|
||||
It's only meant to test if / how well some ideas work.
|
||||
I have no idea what I'm doing, so for all I know it's entirely possible that it could crash your computer, eat your homework, or set fire to your house.
|
||||
Some parts are also written to be as bad as I could make them while still being technically correct, in an effort to make bugs obvious if they occur, which means that even when it does work it may be fragile and error prone.
|
||||
|
||||
In particular, you should expect it to perform poorly under mobility events, and to converge slowly in dynamic networks. All else being equal, this implementation should tend to prefer long-lived links over short-lived ones when embedding, and (poorly estimated) high bandwidth links over low bandwidth ones when forwarding traffic. As such, in multi-homed or mobile scenarios, there may be some tendency for it to make decisions you disagree with.
|
||||
|
||||
While stretch is low on internet-like graphs, the best upper bound I've established on the *additive* stretch of this scheme, after convergence, is the same as for tree routing: proportional to network diameter. For sparse graphs with a large diameter, the scheme may not find particularly efficient paths, even under ideal circumstances. I would argue that such networks tend not to grow large enough for scalability to be an issue, so another routing scheme is better suited to those networks.
|
||||
|
||||
Regarding the announce-able prefix thing, what I wanted to do is use `fc00::/7`, where `fc00::/8` is for nodes and `fd00::/8` is for prefixes.
|
||||
I would also possibly widen the prefixes to `/48`, to match [rfc4193](https://tools.ietf.org/html/rfc4193), and possibly provide an option to keep using a `/64` by splitting it into two `/9` blocks (where `/64` prefixes would continue to live in `fd80::/9`), or else convince myself that the security implications of another 16 bits don't matter (to avoid the complexity of splitting it into two `/9` ranges for prefixes).
|
||||
Using `fc00::/8` this way would cause issues if trying to also run cjdns.
|
||||
Since I like cjdns, and want the option of running it on the same nodes, I've decided not to do that.
|
||||
If I ever give up on avoiding cjdns conflicts, then I may change the addressing scheme to match the above.
|
||||
|
||||
Despite the tree being constructed from path-vector-like routing messages, there's no support for routing policy right now.
|
||||
As a result, peer relationships are bimodal: either you're not connected to someone, or you're connected and you'll route traffic *to* and *through* them.
|
||||
Nodes also accept all incoming connections, so if you want to limit who can connect then you'll need to provide some other kind of access controls.
|
||||
|
||||
The current implementation does all of its setup when the program starts, and then nothing can be reconfigured without restarting the program.
|
||||
At some point I may add a remote API, so a running node can be reconfigured (to e.g. add/remove peers) without restarting, or probe the internal state of the router to get useful debugging info.
|
||||
So far, things seem to work the way I want/expect without much trouble, so I haven't felt the need to do this yet.
|
||||
|
||||
Some parts of the implementation can take advantage of multiple cores, but other parts that could simply do not.
|
||||
Some parts are fast, but other parts are slower than they have any right to be, e.g. I can't figure out why some syscalls are as expensive as they are, so the `tun` in particular tends to be a CPU bottleneck (multi-queue could help in some cases, but that just spreads the cost around, and it doesn't help with single streams of traffic).
|
||||
The Go runtime's GC tends to have short pauses, but it does have pauses.
|
||||
So even if the ideas that went into this routing scheme turn out to be useful, this implementation is likely to remain mediocre at best for the foreseeable future.
|
||||
If the is thing works well and the protocol stabilizes, then it's worth considering re-implementation and/or a formal spec and RFC.
|
||||
In such a case, it's entirely reasonable to change parts of the spec purely to make the efficient implementation easier (e.g. it makes sense to want zero-copy networking, but a couple parts of the current protocol might make that impractical).
|
||||
|
@@ -2,133 +2,147 @@
|
||||
|
||||
Note: This is a very rough early draft.
|
||||
|
||||
Yggdrasil is a routing protocol designed for scalable name-independent routing on internet-like graphs.
|
||||
The design is built around a name-dependent routing scheme which uses distance on a spanning tree as a metric for greedy routing, and a kademlia-like distributed hash table to facilitate lookups of metric space routing information from static cryptographically generated identifiers.
|
||||
This approach can find routes on any network, as it reduces to spanning tree routing in the worst case, but is observed to be particularly efficient on internet-like graphs.
|
||||
In an effort to mitigate many forms of attacks, the routing scheme only uses information which is either cryptographically verifiable or based on observed local network state.
|
||||
The implementation is distributed and runs on dynamic graphs, though this implementation may not converge quickly enough to be practical on networks with high node mobility.
|
||||
This document attempts to give a rough overview of how some of the key parts of the protocol are implemented, as well as an explanation of why a few subtle points are handled the way they are.
|
||||
Yggdrasil is an encrypted IPv6 network running in the [`200::/7` address range](https://en.wikipedia.org/wiki/Unique_local_address).
|
||||
It is an experimental/toy network, so failure is acceptable, as long as it's instructive to see how it breaks if/when everything falls apart.
|
||||
|
||||
IP addresses are derived from cryptographic keys, to reduce the need for public key infrastructure.
|
||||
A form of locator/identifier separation (similar in goal to [LISP](https://en.wikipedia.org/wiki/Locator/Identifier_Separation_Protocol)) is used to map static identifiers (IP addresses) onto dynamic routing information (locators), using a [distributed hash table](https://en.wikipedia.org/wiki/Distributed_hash_table) (DHT).
|
||||
Locators are used to approximate the distance between nodes in the network, where the approximate distance is the length of a real worst-case-scenario path through the network.
|
||||
This is (arguably) easier to secure and requires less information about the network than commonly used routing schemes.
|
||||
|
||||
While not technically a [compact routing scheme](https://arxiv.org/abs/0708.2309), tests on real-world networks suggest that routing in this style incurs stretch comparable to the name-dependent compact routing schemes designed for static networks.
|
||||
Compared to compact routing schemes, Yggdrasil appears to have smaller average routing table sizes, works on dynamic networks, and is name-independent.
|
||||
It currently lacks the provable bounds of compact routing schemes, and there's a serious argument to be made that it cheats by stretching the definition of some of the above terms, but the main point to be emphasized is that there are trade-offs between different concerns when trying to route traffic, and we'd rather make every part *good* than try to make any one part *perfect*.
|
||||
In that sense, Yggdrasil seems to be competitive, on what are supposedly realistic networks, with compact routing schemes.
|
||||
|
||||
## Addressing
|
||||
|
||||
Addresses in Yggdrasil are derived from a truncated version of a `NodeID`.
|
||||
The `NodeID` itself is a sha512sum of a node's permanent public Curve25519 key.
|
||||
Each node's IPv6 address is then assigned from the lower half of the `fd00::/8` prefix using the following approach:
|
||||
Yggdrasil uses a truncated version of a `NodeID` to assign addresses.
|
||||
An address is assigned from the `200::/7` prefix, according to the following:
|
||||
|
||||
1. Begin with `0xfd` as the first byte of the address.
|
||||
1. Begin with `0x02` as the first byte of the address, or `0x03` if it's a `/64` prefix.
|
||||
2. Count the number of leading `1` bits in the NodeID.
|
||||
3. Set the second byte of the address to the number of leading `1` bits, subject to the constraint that this is still in the lower half of the address range (it is unlikely that a node will have 128 or more leading `1` bits in a sha512sum hash, for the foreseeable future).
|
||||
3. Set the second byte of the address to the number of leading `1` bits in the NodeID (8 bit unsigned integer, at most 255).
|
||||
4. Append the NodeID to the remaining bits of the address, truncating the leading `1` bits and the first `0` bit, to a total address size of 128 bits.
|
||||
|
||||
The last bit of the first byte is used to flag if an address is for a router (`200::/8`), or part of an advertised prefix (`300::/8`), where each router owns a `/64` that matches their address (except with the eight bit set to 1 instead of 0).
|
||||
This allows the prefix to be advertised to the router's LAN, so unsupported devices can still connect to the network (e.g. network printers).
|
||||
|
||||
The NodeID is a [sha512sum](https://en.wikipedia.org/wiki/SHA-512) of a node's public encryption key.
|
||||
Addresses are checked that they match NodeID, to prevent address spoofing.
|
||||
As such, while a 128 bit IPv6 address is likely too short to be considered secure by cryptographer standards, there is a significant cost in attempting to cause an address collision.
|
||||
Addresses can be made more secure by brute force generating a large number of leading `1` bits in the NodeID.
|
||||
|
||||
When connecting to a node, the IP address is unpacked into the known bits of the NodeID and a matching bitmask to track which bits are significant.
|
||||
A node is only communicated with if its `NodeID` matches its public key and the known `NodeID` bits from the address.
|
||||
|
||||
It is important to note that only `NodeID` is used internally for routing, so the addressing scheme could in theory be changed without breaking compatibility with intermediate routers.
|
||||
This may become useful if the IPv6 address range ever needs to be changed, or if a new addressing format that allows for more significant bits is ever implemented by the OS.
|
||||
This has been done once, when moving the address range from the `fd00::/8` ULA range to the reserved-but-[deprecated](https://tools.ietf.org/html/rfc4048) `200::/7` range.
|
||||
Further addressing scheme changes could occur if, for example, an IPv7 format ever emerges.
|
||||
|
||||
### Cryptography
|
||||
|
||||
Public key encryption is done using the `golang.org/x/crypto/nacl/box`, which uses Curve25519, XSalsa20, and Poly1305 for key exchange, encryption, and authentication.
|
||||
Public key encryption is done using the `golang.org/x/crypto/nacl/box`, which uses [Curve25519](https://en.wikipedia.org/wiki/Curve25519), [XSalsa20](https://en.wikipedia.org/wiki/Salsa20), and [Poly1305](https://en.wikipedia.org/wiki/Poly1305) for key exchange, encryption, and authentication (interoperable with [NaCl](https://en.wikipedia.org/wiki/NaCl_(software))).
|
||||
Permanent keys are used only for protocol traffic, with random nonces generated on a per-packet basis using `crypto/rand` from Go's standard library.
|
||||
Ephemeral session keys are generated for encapsulated IPv6 traffic, using the same set of primitives, with random initial nonces that are subsequently incremented.
|
||||
Ephemeral session keys (for [forward secrecy](https://en.wikipedia.org/wiki/Forward_secrecy)) are generated for encapsulated IPv6 traffic, using the same set of primitives, with random initial nonces that are subsequently incremented.
|
||||
A list of recently received session nonces is kept (as a bitmask) and checked to reject duplicated packets, in an effort to block duplicate packets and replay attacks.
|
||||
|
||||
A separate private key is generated and used for signing with Ed25519, which is used by the name-dependent routing layer to secure construction of the spanning tree, with a TreeID hash of a node's public Ed key being used to select the highest TreeID as the root of the tree.
|
||||
A separate set of keys are generated and used for signing with [Ed25519](https://en.wikipedia.org/wiki/Ed25519), which is used by the routing layer to secure construction of a spanning tree.
|
||||
|
||||
### Prefixes
|
||||
|
||||
Recall that each node's address is in the lower half of the address range, I.e. `fd00::/9`. A `/64` prefix is made available to each node under `fd80::/9`, where the remaining bits of the prefix match the node's address under `fd00::/9`.
|
||||
Recall that each node's address is in the lower half of the address range, I.e. `200::/8`. A `/64` prefix is made available to each node under `300::/8`, where the remaining bits of the prefix match the node's address under `200::/8`.
|
||||
A node may optionally advertise a prefix on their local area network, which allows unsupported or legacy devices with IPv6 support to connect to the network.
|
||||
Note that there are 64 fewer bits of `NodeID` available to check in each address from a routing prefix, so it makes sense to brute force a `NodeID` with more significant bits in the address if this approach is to be used.
|
||||
Running `genkeys.go` will do this by default.
|
||||
|
||||
## Name-independent routing
|
||||
## Locators and Routing
|
||||
|
||||
A distributed hash table is used to facilitate the lookup of a node's name-dependent routing `coords` from a `NodeID`.
|
||||
A kademlia-like peer structure and xor metric are used in the DHT layout, but only peering info is used--there is no key:value store.
|
||||
In contrast with standard kademlia, instead of using iterative parallel lookups, a recursive lookup strategy is used.
|
||||
This is an intentional design decision to make the DHT more fragile--the intent is for DHT inconsistencies to lead to lookup failures, because of concerns that the iterative parallel approach may hide DHT bugs.
|
||||
Locators are generated using information from a spanning tree (described below).
|
||||
The result is that each node has a set of [coordinates in a greedy metric space](https://en.wikipedia.org/wiki/Greedy_embedding).
|
||||
These coordinates are used as a distance label.
|
||||
Given the coordinates of any two nodes, it is possible to calculate the length of some real path through the network between the two nodes.
|
||||
|
||||
In particular, the DHT is bootstrapped off of a node's one-hop neighbors, and I've observed that this causes a standard kademlia implementation to diverge in the general case.
|
||||
To get around this, buckets are updated more aggressively, and the least recently pinged node from each bucket is flushed to make room for new nodes as soon as a response is heard from them.
|
||||
This appears to fix the bootstrapping issues on all networks where they had been observed in testing, but recursive lookups are kept for the time being to continue monitoring the issue.
|
||||
However, recursive lookups require fewer round trips, so they are expected to be lower latency.
|
||||
As such, even if a switch to iterative parallel lookups was made, the recursive lookup functionality may be kept and used optimistically to minimize handshake time in stable networks.
|
||||
Traffic is forwarded using a [greedy routing](https://en.wikipedia.org/wiki/Small-world_routing#Greedy_routing) scheme, where each node forwards the packet to a one-hop neighbor that is closer to the destination (according to this distance metric) than the current node.
|
||||
In particular, when a packet needs to be forward, a node will forward it to whatever peer is closest to the destination in the greedy [metric space](https://en.wikipedia.org/wiki/Metric_space) used by the network, provided that the peer is closer to the destination than the current node.
|
||||
|
||||
Other than these differences, the DHT is more-or-less what you might expect from a kad implementation.
|
||||
If no closer peers are idle, then the packet is queued in FIFO order, with separate queues per destination coords (currently, as a bit of a hack, IPv6 flow labels are embedeed after the end of the significant part of the coords, so queues distinguish between different traffic streams with the same destination).
|
||||
Whenever the node finishes forwarding a packet to a peer, it checks the queues, and will forward the first packet from the queue with the maximum `<age of first packet>/<queue size in bytes>`, i.e. the bandwidth the queue is attempting to use, subject to the constraint that the peer is a valid next hop (i.e. closer to the destination than the current node).
|
||||
If no non-empty queue is available, then the peer is added to the idle set, forward packets when the need arises.
|
||||
|
||||
## Name-dependent routing
|
||||
This acts as a crude approximation of backpressure routing, where the remote queue sizes are assumed to be equal to the distance of a node from a destination (rather than communicating queue size information), and packets are never forwarded "backwards" through the network, but congestion on a local link is routed around when possible.
|
||||
The queue selection strategy behaves similar to shortest-queue-first, in that a larger fration of available bandwith to sessions that attempt to use less bandwidth, and is loosely based on the rationale behind some proposed solutions to the [cake-cutting](https://en.wikipedia.org/wiki/Fair_cake-cutting) problem.
|
||||
|
||||
A spanning tree is constructed and used for name-dependent routing.
|
||||
The basic idea is to use the distance between nodes *on the tree* as a distance metric, and then perform greedy routing in that metric space.
|
||||
As the tree is constructed from a subset of the real links in the network, this distance metric (unlike the DHT's xor metric) has some relationship with the underlying physical network.
|
||||
In the worst case, greedy routing with this metric reduces to routing on the spanning tree, which should be comparable to ethernet.
|
||||
However, greedy routing can use any link, provided that the node on the other end of the link is closer to the destination, so this allows the use of off-tree shortcuts, with the possibility and effectiveness of this being topology dependent.
|
||||
The main assumption that Yggdrasil's performance hinges on, is that this distance metric is close to real network distance, on average, in realistic networks.
|
||||
|
||||
The name dependent scheme is implemented in roughly the following way:
|
||||
|
||||
1. Each node generates a set of Ed25519 keys for signing routing messages, with a `TreeID` defined as the sha512sum of a node's public signing key.
|
||||
2. If a node doesn't know a better (higher `TreeID`) root for the tree, then it makes itself the root of its own tree.
|
||||
3. Nodes periodically send announcement messages to neighbors, which specify a sequence number for that node's current locator in the tree.
|
||||
4. When a node A sees an unrecognized sequence number from a neighbor B, then A asks B to send them a locator.
|
||||
5. This locator is sent in the form of a path from the root, through B, and ending at A.
|
||||
6. Each hop in the path includes the public signing key of the next hop, and a signature for the full path from the root to the next hop, to prevent forgery of path information (similar to S-BGP).
|
||||
7. The first hop, from the root, includes a signed sequence number which must increase (implemented as a unix timestamp, for convenience), which is used to detect root timeouts and prevent replays.
|
||||
|
||||
The highest `TreeID` approach to root selection is just to ensure that nodes select the same root, otherwise distance calculations wouldn't work.
|
||||
Root selection has a minor effect on the stretch of the paths selected by the network, but this effect was seen to be small compared to the minimum stretch, for nearly all choices of root.
|
||||
|
||||
The current implementation tracks how long a neighbor has been advertising a locator for the same path, and it prefers to select a parent with a stable locator and a short distance to the root (maximize uptime/distance).
|
||||
When forwarding traffic, the next hop is selected taking bandwidth to the next hop and distance to the destination into account (maximize bandwidth/distance), subject to the requirement that distance must always decrease.
|
||||
The bandwidth estimation isn't very good, but it correlates well enough that e.g. when a slow wifi and a fast ethernet link to the same node are available, it typically uses the ethernet link.
|
||||
However, if the ethernet link comes up while the wifi link is under heavy use, then it tends to keep using the wifi link until things settle down, and only switches to ethernet after the wireless link is no longer overloaded.
|
||||
A better approach to bandwidth estimation could probably switch to the new link faster.
|
||||
The queue size is limited to 4 MB. If a packet is added to a queue and the total size of all queues is larger than this threshold, then a random queue is selected (with odds proportional to relative queue sizes), and the first packet from that queue is dropped, with the process repeated until the total queue size drops below the allowed threshold.
|
||||
|
||||
Note that this forwarding procedure generalizes to nodes that are not one-hop neighbors, but the current implementation omits the use of more distant neighbors, as this is expected to be a minor optimization (it would add per-link control traffic to pass path-vector-like information about a subset of the network, which is a lot of overhead compared to the current setup).
|
||||
|
||||
## Other implementation details
|
||||
### Spanning Tree
|
||||
|
||||
In case you hadn't noticed, this implementation is written in Go.
|
||||
That decision was made because the designer and initial author (@Arceliar) felt like learning a new language when the implementation was started, and the Go language seemed like an OK choice for prototyping a network application.
|
||||
While Go's GC pauses are small, they do exist, so this implementation probably isn't suited to applications that require very low latency and jitter.
|
||||
A [spanning tree](https://en.wikipedia.org/wiki/Spanning_tree) is constructed with the tree rooted at the highest TreeID, where TreeID is equal to a sha512sum of a node's public [Ed25519](https://en.wikipedia.org/wiki/Ed25519) key (used for signing).
|
||||
A node sends periodic advertisement messages to each neighbor.
|
||||
The advertisement contains the coords that match the path from the root through the node, plus one additional hop from the node to the neighbor being advertised to.
|
||||
Each hop in this advertisement includes a matching ed25519 signature.
|
||||
These signatures prevent nodes from forging arbitrary routing advertisements.
|
||||
|
||||
Aside from that, an effort was made to write each part of it to be as "bad" (i.e. fragile) as could be managed while still being technically correct.
|
||||
That's a decision made for debugging purposes: the intent is to make any bugs as obvious as possible, so they can more easily be found and fixed in a small or simulated network.
|
||||
The first hop, from the root, also includes a sequence number, which must be updated periodically.
|
||||
A node will blacklist the current root (keeping a record of the last sequence number observed) if the root fails to update for longer than some timeout (currently hard coded at 1 minute).
|
||||
Normally, a root node will update their sequence number for frequently than this (once every 30 seconds).
|
||||
Nodes are throttled to ignore updates with a new sequence number for some period after updating their most recently seen sequence number (currently this cooldown is 15 seconds).
|
||||
The implementation chooses to set the sequence number equal to the unix time on the root's clock, so that a new (higher) sequence number will be selected if the root is restarted and the clock is not set back.
|
||||
|
||||
This implementation runs as an overlay network on top of regular IPv4 or IPv6 traffic.
|
||||
It uses link-local IPv6 multicast traffic to automatically connect to devices on the same network, but it can also be fed a list of address:port pairs to connect to.
|
||||
This can be used to e.g. set up two local networks and bridge them over the internet.
|
||||
Other than the root node, every other node in the network must select one of its neighbors to use as their parent.
|
||||
This selection is done by tracking when each neighbor first sends us a message with a new timestamp from the root, to determine the ordering of the latency of each path from the root, to each neighbor, and then to the node that's searching for a parent.
|
||||
These relative latencies are tracked by, for each neighbor, keeping a score vs each other neighbor.
|
||||
If a neighbor sends a message with an updated timestamp before another neighbor, then the faster neighbor's score is increased by 1.
|
||||
If the neighbor sends a message slower, then the score is decreased by 2, to make sure that a node must be reliably faster (at least 2/3 of the time) to see a net score increase over time.
|
||||
If a node begins to advertise new coordinates, then its score vs all other nodes is reset to 0.
|
||||
A node switches to a new parent if a neighbor's score (vs the current parent) reaches some threshold, currently 240, which corresponds to about 2 hours of being a reliably faster path.
|
||||
The intended outcome of this process is that stable connections from fixed infrastructure near the "core" of the network should (eventually) select parents that minimize latency from the root to themselves, while the more dynamic parts of the network, presumably more towards the edges, will try to favor reliability when selecting a parent.
|
||||
|
||||
## Performance
|
||||
The distance metric between nodes is simply the distance between the nodes if they routed on the spanning tree.
|
||||
This is equal to the sum of the distance from each node to the last common ancestor of the two nodes being compared.
|
||||
The locator then consists of a root's key, timestamp, and coordinates representing each hop in the path from the root to the node.
|
||||
In practice, only the coords are used for routing, while the root and timestamp, along with all the per-hop signatures, are needed to securely construct the spanning tree.
|
||||
|
||||
This section compares Yggdrasil with the results in [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) (specifically table 1) from tests on the 9204-node [skitter](https://www.caida.org/tools/measurement/skitter/) network maps from [caida](https://www.caida.org/).
|
||||
## Name-independent routing
|
||||
|
||||
A [simplified version](misc/sim/treesim-forward.py) of this routing scheme was written (long before the Yggdrasil implementation was started), and tested for comparison with the results from the above paper.
|
||||
This version includes only the name-dependent part of the routing scheme, but the overhead of the name-independent portion is easy enough to check with the full implementation.
|
||||
In summary:
|
||||
A [Chord](https://en.wikipedia.org/wiki/Chord_(peer-to-peer))-like Distributed Hash Table (DHT) is used as a distributed database that maps NodeIDs onto coordinates in the spanning tree metric space.
|
||||
The DHT is Chord-like in that it uses a successor/predecessor structure to do lookups in `O(n)` time with `O(1)` entries, then augments this with some additional information, adding roughly `O(logn)` additional entries, to reduce the lookup time to something around `O(logn)`.
|
||||
In the long term, the idea is to favor spending our bandwidth making sure the minimum `O(1)` part is right, to prioritize correctness, and then try to conserve bandwidth (and power) by being a bit lazy about checking the remaining `O(logn)` portion when it's not in use.
|
||||
|
||||
1. Multiplicative stretch is approximately 1.08 with Yggdrasil, using unweighted links undirected links, as in the paper.
|
||||
2. A modified version can get this as low as 1.01, but it depends on knowing the degree of each one-hop neighbor, which it is not obviously possible to cryptographically secure, and it requires using source routing to find a path from A to B and from B to A, and then have both nodes use whichever path was observed to be shorter.
|
||||
3. In either case, approximately 6 routing table entries are needed, on average, for the name-dependent routing scheme, where each node needs one routing table entry per one-hop neighbor.
|
||||
4. Approximately 30 DHT entries are needed to facilitate name-independent routing.
|
||||
This requires a lookup and caches the results, so old information needs to time out to work on dynamic networks.
|
||||
The schemes it's being compared to only work on static networks, where a similar approach would be fine, so this seems like a reasonably fair comparison.
|
||||
The stretch of that initial lookup can be *very* high, but it's only for a couple of round trips to look up keys and then do the ephemeral key exchange, so this may be an acceptable tradeoff (it's probably more expensive than a DNS lookup, but is similar in principle and effect).
|
||||
5. Both the name-dependent and name-independent routing table entries are of a size proportional to the length of the path between the root and the node, which is at most the diameter of the network after things have fully converged, but name-dependent routing table entries tend to be much larger in practice due to the size of cryptographic signatures (64 bytes for a signature + 32 for the signing key).
|
||||
6. The name-dependent routing scheme only sends messages about one-hop neighbors on the link between those neighbors, so if you measure things by per *link* overhead instead of per *node*, then this doesn't seem so bad to me.
|
||||
7. The name-independent routing scheme scales like a DHT running as an overlay on top of the router-level topology, so the per-link and per-node overhead are going to be topology dependent.
|
||||
This hasn't been studied in a lot of detail, but for realistic topologies, where yggdrasil routing seems to approximate shortest path routing, academic research has shown that shortest path routing does not lead to congestion.
|
||||
To be specific, the DHT stores the immediate successor of a node, plus the next node it manages to find which is strictly closer (by the tree hop-count metric) than all previous nodes.
|
||||
The same process is repeated for predecessor nodes, and lookups walk the network in the predecessor direction, with each key being owned by its successor (to make sure defaulting to 0 for unknown bits of a `NodeID` doesn't cause us to overshoot the target during a lookup).
|
||||
In addition, all of a node's one-hop neighbors are included in the DHT, since we get this information "for free", and we must include it in our DHT to ensure that the network doesn't diverge to a broken state (though I suspect that only adding parents or parent-child relationships may be sufficient -- worth trying to prove or disprove, if somebody's bored).
|
||||
The DHT differs from Chord in that there are no values in the key:value store -- it only stores information about DHT peers -- and that it uses a [Kademlia](https://en.wikipedia.org/wiki/Kademlia)-inspired iterative-parallel lookup process.
|
||||
|
||||
The designer (@Arceliar) believes that the main reason Yggdrasil performs so well is because it stores information about all one-hop neighbors.
|
||||
Consider that, if Yggdrasil did not maintain state about all one-hop neighbors, but the protocol still had the ability to forward to all of them through some mechanism (i.e. source routing), then the OS still needs a way to forward traffic to them.
|
||||
In most cases, this would require some form of per-neighbor state to be stored by the OS, either because there's one dedicated interface per peer or because there are entries in an arp/NDP table to reach multiple devices over a shared switch.
|
||||
So while compact routing schemes have nice theoretical limits, which do not require even as much state as one entry per one-hop neighbor, that property does not seem realistic if the implementation is running at the router level (as opposed to the AS level).
|
||||
As such, keeping one entry per neighbor may be reasonable, especially if nodes with a high degree have proportionally more resources available to them, but it is possible that something may have been overlooked in the design.
|
||||
To summarize the entire routing procedure, when given only a node's IP address, the goal is to find a route to the destination.
|
||||
That happens through 3 steps:
|
||||
|
||||
## Disclaimer
|
||||
1. The address is unpacked into the known bits of a NodeID and a bitmask to signal which bits of the NodeID are known (the unknown bits are ignored).
|
||||
2. A DHT search is performed, which normally results in a response from the node closest in the DHT keyspace to the target `NodeID`. The response contains the node's curve25519 public key, which is checked to match the `NodeID` (and therefore the address), as well as the node's coordinates.
|
||||
3. Using the keys and coords from the above step, an ephemeral key exchange occurs between the source and destination nodes. These ephemeral session keys are used to encrypt any ordinary IPv6 traffic that may be encapsulated and sent between the nodes.
|
||||
|
||||
From that point, the session keys and coords are cached and used to encrypt and send traffic between nodes. This is *mostly* transparent to the user: the initial DHT lookup and key exchange takes at least 2 round trips, so there's some delay before session setup completes and normal IPv6 traffic can flow. This is similar to the delay caused by a DNS lookup, although it generally takes longer, as a DHT lookup requires multiple iterations to reach the destination.
|
||||
|
||||
## Project Status and Plans
|
||||
|
||||
The current (Go) implementation is considered alpha, so compatibility with future versions is neither guaranteed nor expected.
|
||||
While users are discouraged from running anything truly critical on top of it, as of writing, it seems reliable enough for day-to-day use.
|
||||
|
||||
As an "alpha" quality release, Yggdrasil *should* at least be able to detect incompatible versions when it sees them, and warn the users that an update may be needed.
|
||||
A "beta" quality release should know enough to be compatible in the face of wire format changes, and reasonably feature complete.
|
||||
A "stable" 1.0 release, if it ever happens, would probably be feature complete, with no expectation of future wire format changes, and free of known critical bugs.
|
||||
|
||||
Roughly speaking, there are a few obvious ways the project could turn out:
|
||||
|
||||
1. The developers could lose interest before it goes anywhere.
|
||||
2. The project could be reasonably complete (beta or stable), but never gain a significant number of users.
|
||||
3. The network may grow large enough that fundamental (non-fixable) design problems appear, which is hopefully a learning experience, but the project may die as a result.
|
||||
4. The network may grow large, but never hit any design problems, in which case we need to think about either moving the important parts into other projects ([cjdns](https://github.com/cjdelisle/cjdns)) or rewriting compatible implementations that are better optimized for the target platforms (e.g. a linux kernel module).
|
||||
|
||||
That last one is probably impossible, because the speed of light would *eventually* become a problem, for a sufficiently large network.
|
||||
If the only thing limiting network growth turns out to be the underlying physics, then that arguably counts as a win.
|
||||
|
||||
Also, note that some design decisions were made for ease-of-programming or ease-of-testing reasons, and likely need to be reconsidered at some point.
|
||||
In particular, Yggdrasil currently uses TCP for connections with one-hop neighbors, which introduces an additional layer of buffering that can lead to increased and/or unstable latency in congested areas of the network.
|
||||
|
||||
This is a draft version of documentation for a work-in-progress protocol.
|
||||
The design and implementation should be considered pre-alpha, with any and all aspects subject to change in light of ongoing R&D.
|
||||
It is possible that this document and the code base may fall out of sync with eachother.
|
||||
Some details that are known to be likely to change, packet formats in particular, have been omitted.
|
||||
|
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module github.com/yggdrasil-network/yggdrasil-go
|
||||
|
||||
require (
|
||||
github.com/docker/libcontainer v2.2.1+incompatible
|
||||
github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0
|
||||
github.com/mitchellh/mapstructure v1.1.2
|
||||
github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165
|
||||
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
|
||||
github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380
|
||||
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e
|
||||
golang.org/x/text v0.3.0
|
||||
)
|
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
||||
github.com/docker/libcontainer v2.2.1+incompatible h1:++SbbkCw+X8vAd4j2gOCzZ2Nn7s2xFALTf7LZKmM1/0=
|
||||
github.com/docker/libcontainer v2.2.1+incompatible/go.mod h1:osvj61pYsqhNCMLGX31xr7klUBhHb/ZBuXS0o1Fvwbw=
|
||||
github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0 h1:YnZmFjg0Nvk8851WTVWlqMC1ecJH07Ctz+Ezxx4u54g=
|
||||
github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0/go.mod h1:rUi0/YffDo1oXBOGn1KRq7Fr07LX48XEBecQnmwjsAo=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165 h1:Oo7Yfu5lEQLGvvh2p9Z8FRHJIsl7fdOCK9xXFNBkqmQ=
|
||||
github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165/go.mod h1:l+Zao6IpQ+6d/y7LnYnOfbfOeU/9xRiTi4HLVpnkcTg=
|
||||
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w=
|
||||
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY=
|
||||
github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae h1:MYCANF1kehCG6x6G+/9txLfq6n3lS5Vp0Mxn1hdiBAc=
|
||||
github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae/go.mod h1:R0SBCsugm+Sf1katgTb2t7GXMm+nRIv43tM4VDZbaOs=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0=
|
||||
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo=
|
||||
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
@@ -16,7 +16,7 @@ import "encoding/hex"
|
||||
import "flag"
|
||||
import "fmt"
|
||||
import "runtime"
|
||||
import . "yggdrasil"
|
||||
import . "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
|
||||
|
||||
var doSig = flag.Bool("sig", false, "generate new signing keys instead")
|
||||
|
||||
|
@@ -51,12 +51,12 @@ ip netns exec node4 ip link set lo up
|
||||
ip netns exec node5 ip link set lo up
|
||||
ip netns exec node6 ip link set lo up
|
||||
|
||||
ip netns exec node1 ./run --autoconf --pprof &> /dev/null &
|
||||
ip netns exec node2 ./run --autoconf --pprof &> /dev/null &
|
||||
ip netns exec node3 ./run --autoconf --pprof &> /dev/null &
|
||||
ip netns exec node4 ./run --autoconf --pprof &> /dev/null &
|
||||
ip netns exec node5 ./run --autoconf --pprof &> /dev/null &
|
||||
ip netns exec node6 ./run --autoconf --pprof &> /dev/null &
|
||||
ip netns exec node1 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
|
||||
ip netns exec node2 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
|
||||
ip netns exec node3 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
|
||||
ip netns exec node4 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
|
||||
ip netns exec node5 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
|
||||
ip netns exec node6 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
|
||||
|
||||
echo "Started, to continue you should (possibly w/ sudo):"
|
||||
echo "kill" $(jobs -p)
|
||||
|
@@ -1,4 +1,2 @@
|
||||
#!/bin/bash
|
||||
export GOPATH=$PWD
|
||||
go get -d yggdrasil
|
||||
go run -tags debug misc/sim/treesim.go
|
||||
go run -tags debug misc/sim/treesim.go "$@"
|
||||
|
@@ -8,10 +8,11 @@ import "strconv"
|
||||
import "time"
|
||||
import "log"
|
||||
|
||||
import "runtime"
|
||||
import "runtime/pprof"
|
||||
import "flag"
|
||||
|
||||
import . "yggdrasil"
|
||||
import . "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -267,6 +268,7 @@ func pingNodes(store map[[32]byte]*Node) {
|
||||
copy(packet[8:24], sourceAddr)
|
||||
copy(packet[24:40], destAddr)
|
||||
copy(packet[40:], bs)
|
||||
packet[0] = 6 << 4
|
||||
source.send <- packet
|
||||
}
|
||||
destCount := 0
|
||||
@@ -279,17 +281,7 @@ func pingNodes(store map[[32]byte]*Node) {
|
||||
}
|
||||
destAddr := dest.core.DEBUG_getAddr()[:]
|
||||
ticker := time.NewTicker(150 * time.Millisecond)
|
||||
ch := make(chan bool, 1)
|
||||
ch <- true
|
||||
doTicker := func() {
|
||||
for range ticker.C {
|
||||
select {
|
||||
case ch <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
go doTicker()
|
||||
sendTo(payload, destAddr)
|
||||
for loop := true; loop; {
|
||||
select {
|
||||
case packet := <-dest.recv:
|
||||
@@ -298,8 +290,9 @@ func pingNodes(store map[[32]byte]*Node) {
|
||||
loop = false
|
||||
}
|
||||
}
|
||||
case <-ch:
|
||||
case <-ticker.C:
|
||||
sendTo(payload, destAddr)
|
||||
//dumpDHTSize(store) // note that this uses racey functions to read things...
|
||||
}
|
||||
}
|
||||
ticker.Stop()
|
||||
@@ -386,7 +379,7 @@ func (n *Node) startTCP(listen string) {
|
||||
}
|
||||
|
||||
func (n *Node) connectTCP(remoteAddr string) {
|
||||
n.core.AddPeer(remoteAddr)
|
||||
n.core.AddPeer(remoteAddr, remoteAddr)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -437,7 +430,7 @@ func main() {
|
||||
pingNodes(kstore)
|
||||
//pingBench(kstore) // Only after disabling debug output
|
||||
//stressTest(kstore)
|
||||
//time.Sleep(120*time.Second)
|
||||
//time.Sleep(120 * time.Second)
|
||||
dumpDHTSize(kstore) // note that this uses racey functions to read things...
|
||||
if false {
|
||||
// This connects the sim to the local network
|
||||
@@ -456,4 +449,5 @@ func main() {
|
||||
var block chan struct{}
|
||||
<-block
|
||||
}
|
||||
runtime.GC()
|
||||
}
|
||||
|
53
src/config/config.go
Normal file
53
src/config/config.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package config
|
||||
|
||||
// NodeConfig defines all configuration values needed to run a signle yggdrasil node
|
||||
type NodeConfig struct {
|
||||
Listen string `comment:"Listen address for peer connections. Default is to listen for all\nTCP connections over IPv4 and IPv6 with a random port."`
|
||||
AdminListen string `comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."`
|
||||
Peers []string `comment:"List of connection strings for static peers in URI format, e.g.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j."`
|
||||
InterfacePeers map[string][]string `comment:"List of connection strings for static peers in URI format, arranged\nby source interface, e.g. { \"eth0\": [ tcp://a.b.c.d:e ] }. Note that\nSOCKS peerings will NOT be affected by this option and should go in\nthe \"Peers\" section instead."`
|
||||
ReadTimeout int32 `comment:"Read timeout for connections, specified in milliseconds. If less\nthan 6000 and not negative, 6000 (the default) is used. If negative,\nreads won't time out."`
|
||||
AllowedEncryptionPublicKeys []string `comment:"List of peer encryption public keys to allow or incoming TCP\nconnections from. If left empty/undefined then all connections\nwill be allowed by default."`
|
||||
EncryptionPublicKey string `comment:"Your public encryption key. Your peers may ask you for this to put\ninto their AllowedEncryptionPublicKeys configuration."`
|
||||
EncryptionPrivateKey string `comment:"Your private encryption key. DO NOT share this with anyone!"`
|
||||
SigningPublicKey string `comment:"Your public signing key. You should not ordinarily need to share\nthis with anyone."`
|
||||
SigningPrivateKey string `comment:"Your private signing key. DO NOT share this with anyone!"`
|
||||
MulticastInterfaces []string `comment:"Regular expressions for which interfaces multicast peer discovery\nshould be enabled on. If none specified, multicast peer discovery is\ndisabled. The default value is .* which uses all interfaces."`
|
||||
IfName string `comment:"Local network interface name for TUN/TAP adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN/TAP."`
|
||||
IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."`
|
||||
IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
|
||||
SessionFirewall SessionFirewall `comment:"The session firewall controls who can send/receive network traffic\nto/from. This is useful if you want to protect this node without\nresorting to using a real firewall. This does not affect traffic\nbeing routed via this node to somewhere else. Rules are prioritised as\nfollows: blacklist, whitelist, always allow outgoing, direct, remote."`
|
||||
TunnelRouting TunnelRouting `comment:"Allow tunneling non-Yggdrasil traffic over Yggdrasil. This effectively\nallows you to use Yggdrasil to route to, or to bridge other networks,\nsimilar to a VPN tunnel. Tunnelling works between any two nodes and\ndoes not require them to be directly peered."`
|
||||
SwitchOptions SwitchOptions `comment:"Advanced options for tuning the switch. Normally you will not need\nto edit these options."`
|
||||
//Net NetConfig `comment:"Extended options for connecting to peers over other networks."`
|
||||
}
|
||||
|
||||
// NetConfig defines network/proxy related configuration values
|
||||
type NetConfig struct {
|
||||
Tor TorConfig `comment:"Experimental options for configuring peerings over Tor."`
|
||||
I2P I2PConfig `comment:"Experimental options for configuring peerings over I2P."`
|
||||
}
|
||||
|
||||
// SessionFirewall controls the session firewall configuration
|
||||
type SessionFirewall struct {
|
||||
Enable bool `comment:"Enable or disable the session firewall. If disabled, network traffic\nfrom any node will be allowed. If enabled, the below rules apply."`
|
||||
AllowFromDirect bool `comment:"Allow network traffic from directly connected peers."`
|
||||
AllowFromRemote bool `comment:"Allow network traffic from remote nodes on the network that you are\nnot directly peered with."`
|
||||
AlwaysAllowOutbound bool `comment:"Allow outbound network traffic regardless of AllowFromDirect or\nAllowFromRemote. This does allow a remote node to send unsolicited\ntraffic back to you for the length of the session."`
|
||||
WhitelistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always accepted,\nregardless of AllowFromDirect or AllowFromRemote."`
|
||||
BlacklistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always rejected,\nregardless of the whitelist, AllowFromDirect or AllowFromRemote."`
|
||||
}
|
||||
|
||||
// TunnelRouting contains the crypto-key routing tables for tunneling
|
||||
type TunnelRouting struct {
|
||||
Enable bool `comment:"Enable or disable tunnel routing."`
|
||||
IPv6Destinations map[string]string `comment:"IPv6 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"aaaa:bbbb:cccc::/e\": \"boxpubkey\", ... }"`
|
||||
IPv6Sources []string `comment:"Optional IPv6 source subnets which are allowed to be tunnelled in\naddition to this node's Yggdrasil address/subnet. If not\nspecified, only traffic originating from this node's Yggdrasil\naddress or subnet will be tunnelled."`
|
||||
IPv4Destinations map[string]string `comment:"IPv4 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"a.b.c.d/e\": \"boxpubkey\", ... }"`
|
||||
IPv4Sources []string `comment:"IPv4 source subnets which are allowed to be tunnelled. Unlike for\nIPv6, this option is required for bridging IPv4 traffic. Only\ntraffic with a source matching these subnets will be tunnelled."`
|
||||
}
|
||||
|
||||
// SwitchOptions contains tuning options for the switch
|
||||
type SwitchOptions struct {
|
||||
MaxTotalQueueSize uint64 `comment:"Maximum size of all switch queues combined (in bytes)."`
|
||||
}
|
18
src/defaults/defaults.go
Normal file
18
src/defaults/defaults.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package defaults
|
||||
|
||||
// Defines which parameters are expected by default for configuration on a
|
||||
// specific platform. These values are populated in the relevant defaults_*.go
|
||||
// for the platform being targeted. They must be set.
|
||||
type platformDefaultParameters struct {
|
||||
// Admin socket
|
||||
DefaultAdminListen string
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile string
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU int
|
||||
DefaultIfMTU int
|
||||
DefaultIfName string
|
||||
DefaultIfTAPMode bool
|
||||
}
|
21
src/defaults/defaults_darwin.go
Normal file
21
src/defaults/defaults_darwin.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build darwin
|
||||
|
||||
package defaults
|
||||
|
||||
// Sane defaults for the macOS/Darwin platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func GetDefaults() platformDefaultParameters {
|
||||
return platformDefaultParameters{
|
||||
// Admin
|
||||
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile: "/etc/yggdrasil.conf",
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU: 65535,
|
||||
DefaultIfMTU: 65535,
|
||||
DefaultIfName: "auto",
|
||||
DefaultIfTAPMode: false,
|
||||
}
|
||||
}
|
21
src/defaults/defaults_freebsd.go
Normal file
21
src/defaults/defaults_freebsd.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build freebsd
|
||||
|
||||
package defaults
|
||||
|
||||
// Sane defaults for the BSD platforms. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func GetDefaults() platformDefaultParameters {
|
||||
return platformDefaultParameters{
|
||||
// Admin
|
||||
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile: "/etc/yggdrasil.conf",
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU: 32767,
|
||||
DefaultIfMTU: 32767,
|
||||
DefaultIfName: "/dev/tap0",
|
||||
DefaultIfTAPMode: true,
|
||||
}
|
||||
}
|
21
src/defaults/defaults_linux.go
Normal file
21
src/defaults/defaults_linux.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build linux
|
||||
|
||||
package defaults
|
||||
|
||||
// Sane defaults for the Linux platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func GetDefaults() platformDefaultParameters {
|
||||
return platformDefaultParameters{
|
||||
// Admin
|
||||
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile: "/etc/yggdrasil.conf",
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU: 65535,
|
||||
DefaultIfMTU: 65535,
|
||||
DefaultIfName: "auto",
|
||||
DefaultIfTAPMode: false,
|
||||
}
|
||||
}
|
21
src/defaults/defaults_netbsd.go
Normal file
21
src/defaults/defaults_netbsd.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build netbsd
|
||||
|
||||
package defaults
|
||||
|
||||
// Sane defaults for the BSD platforms. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func GetDefaults() platformDefaultParameters {
|
||||
return platformDefaultParameters{
|
||||
// Admin
|
||||
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile: "/etc/yggdrasil.conf",
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU: 9000,
|
||||
DefaultIfMTU: 9000,
|
||||
DefaultIfName: "/dev/tap0",
|
||||
DefaultIfTAPMode: true,
|
||||
}
|
||||
}
|
21
src/defaults/defaults_openbsd.go
Normal file
21
src/defaults/defaults_openbsd.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build openbsd
|
||||
|
||||
package defaults
|
||||
|
||||
// Sane defaults for the BSD platforms. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func GetDefaults() platformDefaultParameters {
|
||||
return platformDefaultParameters{
|
||||
// Admin
|
||||
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile: "/etc/yggdrasil.conf",
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU: 16384,
|
||||
DefaultIfMTU: 16384,
|
||||
DefaultIfName: "/dev/tap0",
|
||||
DefaultIfTAPMode: true,
|
||||
}
|
||||
}
|
21
src/defaults/defaults_other.go
Normal file
21
src/defaults/defaults_other.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build !linux,!darwin,!windows,!openbsd,!freebsd,!netbsd
|
||||
|
||||
package defaults
|
||||
|
||||
// Sane defaults for the other platforms. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func GetDefaults() platformDefaultParameters {
|
||||
return platformDefaultParameters{
|
||||
// Admin
|
||||
DefaultAdminListen: "tcp://localhost:9001",
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile: "/etc/yggdrasil.conf",
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU: 65535,
|
||||
DefaultIfMTU: 65535,
|
||||
DefaultIfName: "none",
|
||||
DefaultIfTAPMode: false,
|
||||
}
|
||||
}
|
21
src/defaults/defaults_windows.go
Normal file
21
src/defaults/defaults_windows.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// +build windows
|
||||
|
||||
package defaults
|
||||
|
||||
// Sane defaults for the Windows platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func GetDefaults() platformDefaultParameters {
|
||||
return platformDefaultParameters{
|
||||
// Admin
|
||||
DefaultAdminListen: "tcp://localhost:9001",
|
||||
|
||||
// Configuration (used for yggdrasilctl)
|
||||
DefaultConfigFile: "C:\\Program Files\\Yggdrasil\\yggdrasil.conf",
|
||||
|
||||
// TUN/TAP
|
||||
MaximumIfMTU: 65535,
|
||||
DefaultIfMTU: 65535,
|
||||
DefaultIfName: "auto",
|
||||
DefaultIfTAPMode: true,
|
||||
}
|
||||
}
|
@@ -7,9 +7,10 @@ type address [16]byte
|
||||
type subnet [8]byte
|
||||
|
||||
// address_prefix is the prefix used for all addresses and subnets in the network.
|
||||
// The current implementation requires this to be a multiple of 8 bits.
|
||||
// The current implementation requires this to be a muliple of 8 bits + 7 bits.
|
||||
// The 8th bit of the last byte is used to signal nodes (0) or /64 prefixes (1).
|
||||
// Nodes that configure this differently will be unable to communicate with eachother, though routing and the DHT machinery *should* still work.
|
||||
var address_prefix = [...]byte{0xfd}
|
||||
var address_prefix = [...]byte{0x02}
|
||||
|
||||
// isValid returns true if an address falls within the range used by nodes in the network.
|
||||
func (a *address) isValid() bool {
|
||||
@@ -18,24 +19,24 @@ func (a *address) isValid() bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (*a)[len(address_prefix)]&0x80 == 0
|
||||
return true
|
||||
}
|
||||
|
||||
// isValid returns true if a prefix falls within the range usable by the network.
|
||||
func (s *subnet) isValid() bool {
|
||||
for idx := range address_prefix {
|
||||
l := len(address_prefix)
|
||||
for idx := range address_prefix[:l-1] {
|
||||
if (*s)[idx] != address_prefix[idx] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return (*s)[len(address_prefix)]&0x80 != 0
|
||||
return (*s)[l-1] == address_prefix[l-1]|0x01
|
||||
}
|
||||
|
||||
// address_addrForNodeID takes a *NodeID as an argument and returns an *address.
|
||||
// This address begins with the address prefix.
|
||||
// The next bit is 0 for an address, and 1 for a subnet.
|
||||
// The following 7 bits are set to the number of leading 1 bits in the NodeID.
|
||||
// The NodeID, excluding the leading 1 bits and the first leading 1 bit, is truncated to the appropriate length and makes up the remainder of the address.
|
||||
// This subnet begins with the address prefix, with the last bit set to 0 to indicate an address.
|
||||
// The following 8 bits are set to the number of leading 1 bits in the NodeID.
|
||||
// The NodeID, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the address.
|
||||
func address_addrForNodeID(nid *NodeID) *address {
|
||||
// 128 bit address
|
||||
// Begins with prefix
|
||||
@@ -67,16 +68,15 @@ func address_addrForNodeID(nid *NodeID) *address {
|
||||
}
|
||||
}
|
||||
copy(addr[:], address_prefix[:])
|
||||
addr[len(address_prefix)] = ones & 0x7f
|
||||
addr[len(address_prefix)] = ones
|
||||
copy(addr[len(address_prefix)+1:], temp)
|
||||
return &addr
|
||||
}
|
||||
|
||||
// address_subnetForNodeID takes a *NodeID as an argument and returns a *subnet.
|
||||
// This subnet begins with the address prefix.
|
||||
// The next bit is 0 for an address, and 1 for a subnet.
|
||||
// The following 7 bits are set to the number of leading 1 bits in the NodeID.
|
||||
// The NodeID, excluding the leading 1 bits and the first leading 1 bit, is truncated to the appropriate length and makes up the remainder of the subnet.
|
||||
// This subnet begins with the address prefix, with the last bit set to 1 to indicate a prefix.
|
||||
// The following 8 bits are set to the number of leading 1 bits in the NodeID.
|
||||
// The NodeID, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the subnet.
|
||||
func address_subnetForNodeID(nid *NodeID) *subnet {
|
||||
// Exactly as the address version, with two exceptions:
|
||||
// 1) The first bit after the fixed prefix is a 1 instead of a 0
|
||||
@@ -84,7 +84,7 @@ func address_subnetForNodeID(nid *NodeID) *subnet {
|
||||
addr := *address_addrForNodeID(nid)
|
||||
var snet subnet
|
||||
copy(snet[:], addr[:])
|
||||
snet[len(address_prefix)] |= 0x80
|
||||
snet[len(address_prefix)-1] |= 0x01
|
||||
return &snet
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func (a *address) getNodeIDandMask() (*NodeID, *NodeID) {
|
||||
// This means truncated leading 1s, first leading 0, and visible part of addr
|
||||
var nid NodeID
|
||||
var mask NodeID
|
||||
ones := int(a[len(address_prefix)] & 0x7f)
|
||||
ones := int(a[len(address_prefix)])
|
||||
for idx := 0; idx < ones; idx++ {
|
||||
nid[idx/8] |= 0x80 >> byte(idx%8)
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func (s *subnet) getNodeIDandMask() (*NodeID, *NodeID) {
|
||||
// As with the address version, but visible parts of the subnet prefix instead
|
||||
var nid NodeID
|
||||
var mask NodeID
|
||||
ones := int(s[len(address_prefix)] & 0x7f)
|
||||
ones := int(s[len(address_prefix)])
|
||||
for idx := 0; idx < ones; idx++ {
|
||||
nid[idx/8] |= 0x80 >> byte(idx%8)
|
||||
}
|
||||
|
@@ -13,6 +13,8 @@ import (
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
|
||||
)
|
||||
|
||||
// TODO: Add authentication
|
||||
@@ -20,6 +22,7 @@ import (
|
||||
type admin struct {
|
||||
core *Core
|
||||
listenaddr string
|
||||
listener net.Listener
|
||||
handlers []admin_handlerInfo
|
||||
}
|
||||
|
||||
@@ -49,12 +52,12 @@ func (a *admin) addHandler(name string, args []string, handler func(admin_info)
|
||||
func (a *admin) init(c *Core, listenaddr string) {
|
||||
a.core = c
|
||||
a.listenaddr = listenaddr
|
||||
a.addHandler("help", nil, func(in admin_info) (admin_info, error) {
|
||||
a.addHandler("list", []string{}, func(in admin_info) (admin_info, error) {
|
||||
handlers := make(map[string]interface{})
|
||||
for _, handler := range a.handlers {
|
||||
handlers[handler.name] = admin_info{"fields": handler.args}
|
||||
}
|
||||
return admin_info{"help": handlers}, nil
|
||||
return admin_info{"list": handlers}, nil
|
||||
})
|
||||
a.addHandler("dot", []string{}, func(in admin_info) (admin_info, error) {
|
||||
return admin_info{"dot": string(a.getResponse_dot())}, nil
|
||||
@@ -87,6 +90,10 @@ func (a *admin) init(c *Core, listenaddr string) {
|
||||
}
|
||||
return admin_info{"switchpeers": switchpeers}, nil
|
||||
})
|
||||
a.addHandler("getSwitchQueues", []string{}, func(in admin_info) (admin_info, error) {
|
||||
queues := a.getData_getSwitchQueues()
|
||||
return admin_info{"switchqueues": queues.asMap()}, nil
|
||||
})
|
||||
a.addHandler("getDHT", []string{}, func(in admin_info) (admin_info, error) {
|
||||
sort := "ip"
|
||||
dht := make(admin_info)
|
||||
@@ -109,8 +116,14 @@ func (a *admin) init(c *Core, listenaddr string) {
|
||||
}
|
||||
return admin_info{"sessions": sessions}, nil
|
||||
})
|
||||
a.addHandler("addPeer", []string{"uri"}, func(in admin_info) (admin_info, error) {
|
||||
if a.addPeer(in["uri"].(string)) == nil {
|
||||
a.addHandler("addPeer", []string{"uri", "[interface]"}, func(in admin_info) (admin_info, error) {
|
||||
// Set sane defaults
|
||||
intf := ""
|
||||
// Has interface been specified?
|
||||
if itf, ok := in["interface"]; ok {
|
||||
intf = itf.(string)
|
||||
}
|
||||
if a.addPeer(in["uri"].(string), intf) == nil {
|
||||
return admin_info{
|
||||
"added": []string{
|
||||
in["uri"].(string),
|
||||
@@ -155,15 +168,15 @@ func (a *admin) init(c *Core, listenaddr string) {
|
||||
})
|
||||
a.addHandler("setTunTap", []string{"name", "[tap_mode]", "[mtu]"}, func(in admin_info) (admin_info, error) {
|
||||
// Set sane defaults
|
||||
iftapmode := getDefaults().defaultIfTAPMode
|
||||
ifmtu := getDefaults().defaultIfMTU
|
||||
iftapmode := defaults.GetDefaults().DefaultIfTAPMode
|
||||
ifmtu := defaults.GetDefaults().DefaultIfMTU
|
||||
// Has TAP mode been specified?
|
||||
if tap, ok := in["tap_mode"]; ok {
|
||||
iftapmode = tap.(bool)
|
||||
}
|
||||
// Check we have enough params for MTU
|
||||
if mtu, ok := in["mtu"]; ok {
|
||||
if mtu.(float64) >= 1280 && ifmtu <= getDefaults().maximumIfMTU {
|
||||
if mtu.(float64) >= 1280 && ifmtu <= defaults.GetDefaults().MaximumIfMTU {
|
||||
ifmtu = int(in["mtu"].(float64))
|
||||
}
|
||||
}
|
||||
@@ -201,7 +214,7 @@ func (a *admin) init(c *Core, listenaddr string) {
|
||||
"not_added": []string{
|
||||
in["box_pub_key"].(string),
|
||||
},
|
||||
}, errors.New("Failed to add allowed box pub key")
|
||||
}, errors.New("Failed to add allowed key")
|
||||
}
|
||||
})
|
||||
a.addHandler("removeAllowedEncryptionPublicKey", []string{"box_pub_key"}, func(in admin_info) (admin_info, error) {
|
||||
@@ -216,28 +229,152 @@ func (a *admin) init(c *Core, listenaddr string) {
|
||||
"not_removed": []string{
|
||||
in["box_pub_key"].(string),
|
||||
},
|
||||
}, errors.New("Failed to remove allowed box pub key")
|
||||
}, errors.New("Failed to remove allowed key")
|
||||
}
|
||||
})
|
||||
a.addHandler("addSourceSubnet", []string{"subnet"}, func(in admin_info) (admin_info, error) {
|
||||
var err error
|
||||
a.core.router.doAdmin(func() {
|
||||
err = a.core.router.cryptokey.addSourceSubnet(in["subnet"].(string))
|
||||
})
|
||||
if err == nil {
|
||||
return admin_info{"added": []string{in["subnet"].(string)}}, nil
|
||||
} else {
|
||||
return admin_info{"not_added": []string{in["subnet"].(string)}}, errors.New("Failed to add source subnet")
|
||||
}
|
||||
})
|
||||
a.addHandler("addRoute", []string{"subnet", "box_pub_key"}, func(in admin_info) (admin_info, error) {
|
||||
var err error
|
||||
a.core.router.doAdmin(func() {
|
||||
err = a.core.router.cryptokey.addRoute(in["subnet"].(string), in["box_pub_key"].(string))
|
||||
})
|
||||
if err == nil {
|
||||
return admin_info{"added": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, nil
|
||||
} else {
|
||||
return admin_info{"not_added": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, errors.New("Failed to add route")
|
||||
}
|
||||
})
|
||||
a.addHandler("getSourceSubnets", []string{}, func(in admin_info) (admin_info, error) {
|
||||
var subnets []string
|
||||
a.core.router.doAdmin(func() {
|
||||
getSourceSubnets := func(snets []net.IPNet) {
|
||||
for _, subnet := range snets {
|
||||
subnets = append(subnets, subnet.String())
|
||||
}
|
||||
}
|
||||
getSourceSubnets(a.core.router.cryptokey.ipv4sources)
|
||||
getSourceSubnets(a.core.router.cryptokey.ipv6sources)
|
||||
})
|
||||
return admin_info{"source_subnets": subnets}, nil
|
||||
})
|
||||
a.addHandler("getRoutes", []string{}, func(in admin_info) (admin_info, error) {
|
||||
routes := make(admin_info)
|
||||
a.core.router.doAdmin(func() {
|
||||
getRoutes := func(ckrs []cryptokey_route) {
|
||||
for _, ckr := range ckrs {
|
||||
routes[ckr.subnet.String()] = hex.EncodeToString(ckr.destination[:])
|
||||
}
|
||||
}
|
||||
getRoutes(a.core.router.cryptokey.ipv4routes)
|
||||
getRoutes(a.core.router.cryptokey.ipv6routes)
|
||||
})
|
||||
return admin_info{"routes": routes}, nil
|
||||
})
|
||||
a.addHandler("removeSourceSubnet", []string{"subnet"}, func(in admin_info) (admin_info, error) {
|
||||
var err error
|
||||
a.core.router.doAdmin(func() {
|
||||
err = a.core.router.cryptokey.removeSourceSubnet(in["subnet"].(string))
|
||||
})
|
||||
if err == nil {
|
||||
return admin_info{"removed": []string{in["subnet"].(string)}}, nil
|
||||
} else {
|
||||
return admin_info{"not_removed": []string{in["subnet"].(string)}}, errors.New("Failed to remove source subnet")
|
||||
}
|
||||
})
|
||||
a.addHandler("removeRoute", []string{"subnet", "box_pub_key"}, func(in admin_info) (admin_info, error) {
|
||||
var err error
|
||||
a.core.router.doAdmin(func() {
|
||||
err = a.core.router.cryptokey.removeRoute(in["subnet"].(string), in["box_pub_key"].(string))
|
||||
})
|
||||
if err == nil {
|
||||
return admin_info{"removed": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, nil
|
||||
} else {
|
||||
return admin_info{"not_removed": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, errors.New("Failed to remove route")
|
||||
}
|
||||
})
|
||||
a.addHandler("dhtPing", []string{"box_pub_key", "coords", "[target]"}, func(in admin_info) (admin_info, error) {
|
||||
if in["target"] == nil {
|
||||
in["target"] = "none"
|
||||
}
|
||||
result, err := a.admin_dhtPing(in["box_pub_key"].(string), in["coords"].(string), in["target"].(string))
|
||||
if err == nil {
|
||||
infos := make(map[string]map[string]string, len(result.Infos))
|
||||
for _, dinfo := range result.Infos {
|
||||
info := map[string]string{
|
||||
"box_pub_key": hex.EncodeToString(dinfo.key[:]),
|
||||
"coords": fmt.Sprintf("%v", dinfo.coords),
|
||||
}
|
||||
addr := net.IP(address_addrForNodeID(getNodeID(&dinfo.key))[:]).String()
|
||||
infos[addr] = info
|
||||
}
|
||||
return admin_info{"nodes": infos}, nil
|
||||
} else {
|
||||
return admin_info{}, err
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// start runs the admin API socket to listen for / respond to admin API calls.
|
||||
func (a *admin) start() error {
|
||||
go a.listen()
|
||||
if a.listenaddr != "none" && a.listenaddr != "" {
|
||||
go a.listen()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleans up when stopping
|
||||
func (a *admin) close() error {
|
||||
return a.listener.Close()
|
||||
}
|
||||
|
||||
// listen is run by start and manages API connections.
|
||||
func (a *admin) listen() {
|
||||
l, err := net.Listen("tcp", a.listenaddr)
|
||||
u, err := url.Parse(a.listenaddr)
|
||||
if err == nil {
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "unix":
|
||||
if _, err := os.Stat(a.listenaddr[7:]); err == nil {
|
||||
a.core.log.Println("WARNING:", a.listenaddr[7:], "already exists and may be in use by another process")
|
||||
}
|
||||
a.listener, err = net.Listen("unix", a.listenaddr[7:])
|
||||
if err == nil {
|
||||
switch a.listenaddr[7:8] {
|
||||
case "@": // maybe abstract namespace
|
||||
default:
|
||||
if err := os.Chmod(a.listenaddr[7:], 0660); err != nil {
|
||||
a.core.log.Println("WARNING:", a.listenaddr[:7], "may have unsafe permissions!")
|
||||
}
|
||||
}
|
||||
}
|
||||
case "tcp":
|
||||
a.listener, err = net.Listen("tcp", u.Host)
|
||||
default:
|
||||
// err = errors.New(fmt.Sprint("protocol not supported: ", u.Scheme))
|
||||
a.listener, err = net.Listen("tcp", a.listenaddr)
|
||||
}
|
||||
} else {
|
||||
a.listener, err = net.Listen("tcp", a.listenaddr)
|
||||
}
|
||||
if err != nil {
|
||||
a.core.log.Printf("Admin socket failed to listen: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer l.Close()
|
||||
a.core.log.Printf("Admin socket listening on %s", l.Addr().String())
|
||||
a.core.log.Printf("%s admin socket listening on %s",
|
||||
strings.ToUpper(a.listener.Addr().Network()),
|
||||
a.listener.Addr().String())
|
||||
defer a.listener.Close()
|
||||
for {
|
||||
conn, err := l.Accept()
|
||||
conn, err := a.listener.Accept()
|
||||
if err == nil {
|
||||
a.handleRequest(conn)
|
||||
}
|
||||
@@ -286,7 +423,7 @@ func (a *admin) handleRequest(conn net.Conn) {
|
||||
handlers:
|
||||
for _, handler := range a.handlers {
|
||||
// We've found the handler that matches the request
|
||||
if recv["request"] == handler.name {
|
||||
if strings.ToLower(recv["request"].(string)) == strings.ToLower(handler.name) {
|
||||
// Check that we have all the required arguments
|
||||
for _, arg := range handler.args {
|
||||
// An argument in [square brackets] is optional and not required,
|
||||
@@ -353,7 +490,6 @@ func (n *admin_nodeInfo) toString() string {
|
||||
out = append(out, fmt.Sprintf("%v: %v", p.key, p.val))
|
||||
}
|
||||
return strings.Join(out, ", ")
|
||||
return fmt.Sprint(*n)
|
||||
}
|
||||
|
||||
// printInfos returns a newline separated list of strings from admin_nodeInfos, e.g. a printable string of info about all peers.
|
||||
@@ -367,12 +503,12 @@ func (a *admin) printInfos(infos []admin_nodeInfo) string {
|
||||
}
|
||||
|
||||
// addPeer triggers a connection attempt to a node.
|
||||
func (a *admin) addPeer(addr string) error {
|
||||
func (a *admin) addPeer(addr string, sintf string) error {
|
||||
u, err := url.Parse(addr)
|
||||
if err == nil {
|
||||
switch strings.ToLower(u.Scheme) {
|
||||
case "tcp":
|
||||
a.core.tcp.connect(u.Host)
|
||||
a.core.tcp.connect(u.Host, sintf)
|
||||
case "socks":
|
||||
a.core.tcp.connectSOCKS(u.Host, u.Path[1:])
|
||||
default:
|
||||
@@ -384,7 +520,7 @@ func (a *admin) addPeer(addr string) error {
|
||||
if strings.HasPrefix(addr, "tcp:") {
|
||||
addr = addr[4:]
|
||||
}
|
||||
a.core.tcp.connect(addr)
|
||||
a.core.tcp.connect(addr, "")
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
@@ -406,7 +542,7 @@ func (a *admin) startTunWithMTU(ifname string, iftapmode bool, ifmtu int) error
|
||||
_ = a.core.tun.close()
|
||||
// Then reconfigure and start it
|
||||
addr := a.core.router.addr
|
||||
straddr := fmt.Sprintf("%s/%v", net.IP(addr[:]).String(), 8*len(address_prefix))
|
||||
straddr := fmt.Sprintf("%s/%v", net.IP(addr[:]).String(), 8*len(address_prefix)-1)
|
||||
if ifname != "none" {
|
||||
err := a.core.tun.setup(ifname, iftapmode, straddr, ifmtu)
|
||||
if err != nil {
|
||||
@@ -434,10 +570,18 @@ func (a *admin) getData_getSelf() *admin_nodeInfo {
|
||||
table := a.core.switchTable.table.Load().(lookupTable)
|
||||
coords := table.self.getCoords()
|
||||
self := admin_nodeInfo{
|
||||
{"box_pub_key", hex.EncodeToString(a.core.boxPub[:])},
|
||||
{"ip", a.core.GetAddress().String()},
|
||||
{"subnet", a.core.GetSubnet().String()},
|
||||
{"coords", fmt.Sprint(coords)},
|
||||
}
|
||||
if name := GetBuildName(); name != "unknown" {
|
||||
self = append(self, admin_pair{"build_name", name})
|
||||
}
|
||||
if version := GetBuildVersion(); version != "unknown" {
|
||||
self = append(self, admin_pair{"build_version", version})
|
||||
}
|
||||
|
||||
return &self
|
||||
}
|
||||
|
||||
@@ -459,6 +603,8 @@ func (a *admin) getData_getPeers() []admin_nodeInfo {
|
||||
{"uptime", int(time.Since(p.firstSeen).Seconds())},
|
||||
{"bytes_sent", atomic.LoadUint64(&p.bytesSent)},
|
||||
{"bytes_recvd", atomic.LoadUint64(&p.bytesRecvd)},
|
||||
{"endpoint", p.endpoint},
|
||||
{"box_pub_key", hex.EncodeToString(p.box[:])},
|
||||
}
|
||||
peerInfos = append(peerInfos, info)
|
||||
}
|
||||
@@ -481,34 +627,66 @@ func (a *admin) getData_getSwitchPeers() []admin_nodeInfo {
|
||||
{"ip", net.IP(addr[:]).String()},
|
||||
{"coords", fmt.Sprint(coords)},
|
||||
{"port", elem.port},
|
||||
{"bytes_sent", atomic.LoadUint64(&peer.bytesSent)},
|
||||
{"bytes_recvd", atomic.LoadUint64(&peer.bytesRecvd)},
|
||||
{"endpoint", peer.endpoint},
|
||||
{"box_pub_key", hex.EncodeToString(peer.box[:])},
|
||||
}
|
||||
peerInfos = append(peerInfos, info)
|
||||
}
|
||||
return peerInfos
|
||||
}
|
||||
|
||||
// getData_getSwitchQueues returns info from Core.switchTable for an queue data.
|
||||
func (a *admin) getData_getSwitchQueues() admin_nodeInfo {
|
||||
var peerInfos admin_nodeInfo
|
||||
switchTable := &a.core.switchTable
|
||||
getSwitchQueues := func() {
|
||||
queues := make([]map[string]interface{}, 0)
|
||||
for k, v := range switchTable.queues.bufs {
|
||||
nexthop := switchTable.bestPortForCoords([]byte(k))
|
||||
queue := map[string]interface{}{
|
||||
"queue_id": k,
|
||||
"queue_size": v.size,
|
||||
"queue_packets": len(v.packets),
|
||||
"queue_port": nexthop,
|
||||
}
|
||||
queues = append(queues, queue)
|
||||
}
|
||||
peerInfos = admin_nodeInfo{
|
||||
{"queues", queues},
|
||||
{"queues_count", len(switchTable.queues.bufs)},
|
||||
{"queues_size", switchTable.queues.size},
|
||||
{"highest_queues_count", switchTable.queues.maxbufs},
|
||||
{"highest_queues_size", switchTable.queues.maxsize},
|
||||
{"maximum_queues_size", switchTable.queueTotalMaxSize},
|
||||
}
|
||||
}
|
||||
a.core.switchTable.doAdmin(getSwitchQueues)
|
||||
return peerInfos
|
||||
}
|
||||
|
||||
// getData_getDHT returns info from Core.dht for an admin response.
|
||||
func (a *admin) getData_getDHT() []admin_nodeInfo {
|
||||
var infos []admin_nodeInfo
|
||||
now := time.Now()
|
||||
getDHT := func() {
|
||||
for i := 0; i < a.core.dht.nBuckets(); i++ {
|
||||
b := a.core.dht.getBucket(i)
|
||||
getInfo := func(vs []*dhtInfo, isPeer bool) {
|
||||
for _, v := range vs {
|
||||
addr := *address_addrForNodeID(v.getNodeID())
|
||||
info := admin_nodeInfo{
|
||||
{"ip", net.IP(addr[:]).String()},
|
||||
{"coords", fmt.Sprint(v.coords)},
|
||||
{"bucket", i},
|
||||
{"peer_only", isPeer},
|
||||
{"last_seen", int(now.Sub(v.recv).Seconds())},
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
now := time.Now()
|
||||
var dhtInfos []*dhtInfo
|
||||
for _, v := range a.core.dht.table {
|
||||
dhtInfos = append(dhtInfos, v)
|
||||
}
|
||||
sort.SliceStable(dhtInfos, func(i, j int) bool {
|
||||
return dht_ordered(&a.core.dht.nodeID, dhtInfos[i].getNodeID(), dhtInfos[j].getNodeID())
|
||||
})
|
||||
for _, v := range dhtInfos {
|
||||
addr := *address_addrForNodeID(v.getNodeID())
|
||||
info := admin_nodeInfo{
|
||||
{"ip", net.IP(addr[:]).String()},
|
||||
{"coords", fmt.Sprint(v.coords)},
|
||||
{"last_seen", int(now.Sub(v.recv).Seconds())},
|
||||
{"box_pub_key", hex.EncodeToString(v.key[:])},
|
||||
}
|
||||
getInfo(b.other, false)
|
||||
getInfo(b.peers, true)
|
||||
infos = append(infos, info)
|
||||
}
|
||||
}
|
||||
a.core.router.doAdmin(getDHT)
|
||||
@@ -528,6 +706,7 @@ func (a *admin) getData_getSessions() []admin_nodeInfo {
|
||||
{"was_mtu_fixed", sinfo.wasMTUFixed},
|
||||
{"bytes_sent", sinfo.bytesSent},
|
||||
{"bytes_recvd", sinfo.bytesRecvd},
|
||||
{"box_pub_key", hex.EncodeToString(sinfo.theirPermPub[:])},
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
@@ -569,6 +748,64 @@ func (a *admin) removeAllowedEncryptionPublicKey(bstr string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Send a DHT ping to the node with the provided key and coords, optionally looking up the specified target NodeID.
|
||||
func (a *admin) admin_dhtPing(keyString, coordString, targetString string) (dhtRes, error) {
|
||||
var key boxPubKey
|
||||
if keyBytes, err := hex.DecodeString(keyString); err != nil {
|
||||
return dhtRes{}, err
|
||||
} else {
|
||||
copy(key[:], keyBytes)
|
||||
}
|
||||
var coords []byte
|
||||
for _, cstr := range strings.Split(strings.Trim(coordString, "[]"), " ") {
|
||||
if cstr == "" {
|
||||
// Special case, happens if trimmed is the empty string, e.g. this is the root
|
||||
continue
|
||||
}
|
||||
if u64, err := strconv.ParseUint(cstr, 10, 8); err != nil {
|
||||
return dhtRes{}, err
|
||||
} else {
|
||||
coords = append(coords, uint8(u64))
|
||||
}
|
||||
}
|
||||
resCh := make(chan *dhtRes, 1)
|
||||
info := dhtInfo{
|
||||
key: key,
|
||||
coords: coords,
|
||||
}
|
||||
target := *info.getNodeID()
|
||||
if targetString == "none" {
|
||||
// Leave the default target in place
|
||||
} else if targetBytes, err := hex.DecodeString(targetString); err != nil {
|
||||
return dhtRes{}, err
|
||||
} else if len(targetBytes) != len(target) {
|
||||
return dhtRes{}, errors.New("Incorrect target NodeID length")
|
||||
} else {
|
||||
target = NodeID{}
|
||||
copy(target[:], targetBytes)
|
||||
}
|
||||
rq := dhtReqKey{info.key, target}
|
||||
sendPing := func() {
|
||||
a.core.dht.addCallback(&rq, func(res *dhtRes) {
|
||||
defer func() { recover() }()
|
||||
select {
|
||||
case resCh <- res:
|
||||
default:
|
||||
}
|
||||
})
|
||||
a.core.dht.ping(&info, &target)
|
||||
}
|
||||
a.core.router.doAdmin(sendPing)
|
||||
go func() {
|
||||
time.Sleep(6 * time.Second)
|
||||
close(resCh)
|
||||
}()
|
||||
for res := range resCh {
|
||||
return *res, nil
|
||||
}
|
||||
return dhtRes{}, errors.New(fmt.Sprintf("DHT ping timeout: %s", keyString))
|
||||
}
|
||||
|
||||
// getResponse_dot returns a response for a graphviz dot formatted representation of the known parts of the network.
|
||||
// This is color-coded and labeled, and includes the self node, switch peers, nodes known to the DHT, and nodes with open sessions.
|
||||
// The graph is structured as a tree with directed links leading away from the root.
|
||||
@@ -582,9 +819,16 @@ func (a *admin) getResponse_dot() []byte {
|
||||
name string
|
||||
key string
|
||||
parent string
|
||||
port switchPort
|
||||
options string
|
||||
}
|
||||
infos := make(map[string]nodeInfo)
|
||||
// Get coords as a slice of strings, FIXME? this looks very fragile
|
||||
coordSlice := func(coords string) []string {
|
||||
tmp := strings.Replace(coords, "[", "", -1)
|
||||
tmp = strings.Replace(tmp, "]", "", -1)
|
||||
return strings.Split(tmp, " ")
|
||||
}
|
||||
// First fill the tree with all known nodes, no parents
|
||||
addInfo := func(nodes []admin_nodeInfo, options string, tag string) {
|
||||
for _, node := range nodes {
|
||||
@@ -598,6 +842,14 @@ func (a *admin) getResponse_dot() []byte {
|
||||
} else {
|
||||
info.name = n["ip"].(string)
|
||||
}
|
||||
coordsSplit := coordSlice(info.key)
|
||||
if len(coordsSplit) != 0 {
|
||||
portStr := coordsSplit[len(coordsSplit)-1]
|
||||
portUint, err := strconv.ParseUint(portStr, 10, 64)
|
||||
if err == nil {
|
||||
info.port = switchPort(portUint)
|
||||
}
|
||||
}
|
||||
infos[info.key] = info
|
||||
}
|
||||
}
|
||||
@@ -605,12 +857,6 @@ func (a *admin) getResponse_dot() []byte {
|
||||
addInfo(sessions, "fillcolor=\"#acf3fd\" style=filled fontname=\"sans serif\"", "Open session") // blue
|
||||
addInfo(peers, "fillcolor=\"#ffffb5\" style=filled fontname=\"sans serif\"", "Connected peer") // yellow
|
||||
addInfo(append([]admin_nodeInfo(nil), *self), "fillcolor=\"#a5ff8a\" style=filled fontname=\"sans serif\"", "This node") // green
|
||||
// Get coords as a slice of strings, FIXME? this looks very fragile
|
||||
coordSlice := func(coords string) []string {
|
||||
tmp := strings.Replace(coords, "[", "", -1)
|
||||
tmp = strings.Replace(tmp, "]", "", -1)
|
||||
return strings.Split(tmp, " ")
|
||||
}
|
||||
// Now go through and create placeholders for any missing nodes
|
||||
for _, info := range infos {
|
||||
// This is ugly string manipulation
|
||||
@@ -624,6 +870,16 @@ func (a *admin) getResponse_dot() []byte {
|
||||
newInfo.name = "?"
|
||||
newInfo.key = key
|
||||
newInfo.options = "fontname=\"sans serif\" style=dashed color=\"#999999\" fontcolor=\"#999999\""
|
||||
|
||||
coordsSplit := coordSlice(newInfo.key)
|
||||
if len(coordsSplit) != 0 {
|
||||
portStr := coordsSplit[len(coordsSplit)-1]
|
||||
portUint, err := strconv.ParseUint(portStr, 10, 64)
|
||||
if err == nil {
|
||||
newInfo.port = switchPort(portUint)
|
||||
}
|
||||
}
|
||||
|
||||
infos[key] = newInfo
|
||||
}
|
||||
}
|
||||
@@ -642,10 +898,12 @@ func (a *admin) getResponse_dot() []byte {
|
||||
keys = append(keys, info.key)
|
||||
}
|
||||
// sort
|
||||
less := func(i, j int) bool {
|
||||
sort.SliceStable(keys, func(i, j int) bool {
|
||||
return keys[i] < keys[j]
|
||||
}
|
||||
sort.Slice(keys, less)
|
||||
})
|
||||
sort.SliceStable(keys, func(i, j int) bool {
|
||||
return infos[keys[i]].port < infos[keys[j]].port
|
||||
})
|
||||
// Now print it all out
|
||||
var out []byte
|
||||
put := func(s string) {
|
||||
@@ -663,11 +921,7 @@ func (a *admin) getResponse_dot() []byte {
|
||||
if info.key == info.parent {
|
||||
continue
|
||||
} // happens for the root, skip it
|
||||
coordsSplit := coordSlice(key)
|
||||
if len(coordsSplit) == 0 {
|
||||
continue
|
||||
}
|
||||
port := coordsSplit[len(coordsSplit)-1]
|
||||
port := fmt.Sprint(info.port)
|
||||
style := "fontname=\"sans serif\""
|
||||
if infos[info.parent].name == "?" || infos[info.key].name == "?" {
|
||||
style = "fontname=\"sans serif\" style=dashed color=\"#999999\" fontcolor=\"#999999\""
|
||||
|
348
src/yggdrasil/ckr.go
Normal file
348
src/yggdrasil/ckr.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package yggdrasil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// This module implements crypto-key routing, similar to Wireguard, where we
|
||||
// allow traffic for non-Yggdrasil ranges to be routed over Yggdrasil.
|
||||
|
||||
type cryptokey struct {
|
||||
core *Core
|
||||
enabled bool
|
||||
ipv4routes []cryptokey_route
|
||||
ipv6routes []cryptokey_route
|
||||
ipv4cache map[address]cryptokey_route
|
||||
ipv6cache map[address]cryptokey_route
|
||||
ipv4sources []net.IPNet
|
||||
ipv6sources []net.IPNet
|
||||
}
|
||||
|
||||
type cryptokey_route struct {
|
||||
subnet net.IPNet
|
||||
destination boxPubKey
|
||||
}
|
||||
|
||||
// Initialise crypto-key routing. This must be done before any other CKR calls.
|
||||
func (c *cryptokey) init(core *Core) {
|
||||
c.core = core
|
||||
c.ipv4routes = make([]cryptokey_route, 0)
|
||||
c.ipv6routes = make([]cryptokey_route, 0)
|
||||
c.ipv4cache = make(map[address]cryptokey_route, 0)
|
||||
c.ipv6cache = make(map[address]cryptokey_route, 0)
|
||||
c.ipv4sources = make([]net.IPNet, 0)
|
||||
c.ipv6sources = make([]net.IPNet, 0)
|
||||
}
|
||||
|
||||
// Enable or disable crypto-key routing.
|
||||
func (c *cryptokey) setEnabled(enabled bool) {
|
||||
c.enabled = enabled
|
||||
}
|
||||
|
||||
// Check if crypto-key routing is enabled.
|
||||
func (c *cryptokey) isEnabled() bool {
|
||||
return c.enabled
|
||||
}
|
||||
|
||||
// Check whether the given address (with the address length specified in bytes)
|
||||
// matches either the current node's address, the node's routed subnet or the
|
||||
// list of subnets specified in IPv4Sources/IPv6Sources.
|
||||
func (c *cryptokey) isValidSource(addr address, addrlen int) bool {
|
||||
ip := net.IP(addr[:addrlen])
|
||||
|
||||
if addrlen == net.IPv6len {
|
||||
// Does this match our node's address?
|
||||
if bytes.Equal(addr[:16], c.core.router.addr[:16]) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Does this match our node's subnet?
|
||||
if bytes.Equal(addr[:8], c.core.router.subnet[:8]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Does it match a configured CKR source?
|
||||
if c.isEnabled() {
|
||||
// Build our references to the routing sources
|
||||
var routingsources *[]net.IPNet
|
||||
|
||||
// Check if the prefix is IPv4 or IPv6
|
||||
if addrlen == net.IPv6len {
|
||||
routingsources = &c.ipv6sources
|
||||
} else if addrlen == net.IPv4len {
|
||||
routingsources = &c.ipv4sources
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, subnet := range *routingsources {
|
||||
if subnet.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Doesn't match any of the above
|
||||
return false
|
||||
}
|
||||
|
||||
// Adds a source subnet, which allows traffic with these source addresses to
|
||||
// be tunnelled using crypto-key routing.
|
||||
func (c *cryptokey) addSourceSubnet(cidr string) error {
|
||||
// Is the CIDR we've been given valid?
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the prefix length and size
|
||||
_, prefixsize := ipnet.Mask.Size()
|
||||
|
||||
// Build our references to the routing sources
|
||||
var routingsources *[]net.IPNet
|
||||
|
||||
// Check if the prefix is IPv4 or IPv6
|
||||
if prefixsize == net.IPv6len*8 {
|
||||
routingsources = &c.ipv6sources
|
||||
} else if prefixsize == net.IPv4len*8 {
|
||||
routingsources = &c.ipv4sources
|
||||
} else {
|
||||
return errors.New("Unexpected prefix size")
|
||||
}
|
||||
|
||||
// Check if we already have this CIDR
|
||||
for _, subnet := range *routingsources {
|
||||
if subnet.String() == ipnet.String() {
|
||||
return errors.New("Source subnet already configured")
|
||||
}
|
||||
}
|
||||
|
||||
// Add the source subnet
|
||||
*routingsources = append(*routingsources, *ipnet)
|
||||
c.core.log.Println("Added CKR source subnet", cidr)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Adds a destination route for the given CIDR to be tunnelled to the node
|
||||
// with the given BoxPubKey.
|
||||
func (c *cryptokey) addRoute(cidr string, dest string) error {
|
||||
// Is the CIDR we've been given valid?
|
||||
ipaddr, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the prefix length and size
|
||||
_, prefixsize := ipnet.Mask.Size()
|
||||
|
||||
// Build our references to the routing table and cache
|
||||
var routingtable *[]cryptokey_route
|
||||
var routingcache *map[address]cryptokey_route
|
||||
|
||||
// Check if the prefix is IPv4 or IPv6
|
||||
if prefixsize == net.IPv6len*8 {
|
||||
routingtable = &c.ipv6routes
|
||||
routingcache = &c.ipv6cache
|
||||
} else if prefixsize == net.IPv4len*8 {
|
||||
routingtable = &c.ipv4routes
|
||||
routingcache = &c.ipv4cache
|
||||
} else {
|
||||
return errors.New("Unexpected prefix size")
|
||||
}
|
||||
|
||||
// Is the route an Yggdrasil destination?
|
||||
var addr address
|
||||
var snet subnet
|
||||
copy(addr[:], ipaddr)
|
||||
copy(snet[:], ipnet.IP)
|
||||
if addr.isValid() || snet.isValid() {
|
||||
return errors.New("Can't specify Yggdrasil destination as crypto-key route")
|
||||
}
|
||||
// Do we already have a route for this subnet?
|
||||
for _, route := range *routingtable {
|
||||
if route.subnet.String() == ipnet.String() {
|
||||
return errors.New(fmt.Sprintf("Route already exists for %s", cidr))
|
||||
}
|
||||
}
|
||||
// Decode the public key
|
||||
if bpk, err := hex.DecodeString(dest); err != nil {
|
||||
return err
|
||||
} else if len(bpk) != boxPubKeyLen {
|
||||
return errors.New(fmt.Sprintf("Incorrect key length for %s", dest))
|
||||
} else {
|
||||
// Add the new crypto-key route
|
||||
var key boxPubKey
|
||||
copy(key[:], bpk)
|
||||
*routingtable = append(*routingtable, cryptokey_route{
|
||||
subnet: *ipnet,
|
||||
destination: key,
|
||||
})
|
||||
|
||||
// Sort so most specific routes are first
|
||||
sort.Slice(*routingtable, func(i, j int) bool {
|
||||
im, _ := (*routingtable)[i].subnet.Mask.Size()
|
||||
jm, _ := (*routingtable)[j].subnet.Mask.Size()
|
||||
return im > jm
|
||||
})
|
||||
|
||||
// Clear the cache as this route might change future routing
|
||||
// Setting an empty slice keeps the memory whereas nil invokes GC
|
||||
for k := range *routingcache {
|
||||
delete(*routingcache, k)
|
||||
}
|
||||
|
||||
c.core.log.Println("Added CKR destination subnet", cidr)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Looks up the most specific route for the given address (with the address
|
||||
// length specified in bytes) from the crypto-key routing table. An error is
|
||||
// returned if the address is not suitable or no route was found.
|
||||
func (c *cryptokey) getPublicKeyForAddress(addr address, addrlen int) (boxPubKey, error) {
|
||||
// Check if the address is a valid Yggdrasil address - if so it
|
||||
// is exempt from all CKR checking
|
||||
if addr.isValid() {
|
||||
return boxPubKey{}, errors.New("Cannot look up CKR for Yggdrasil addresses")
|
||||
}
|
||||
|
||||
// Build our references to the routing table and cache
|
||||
var routingtable *[]cryptokey_route
|
||||
var routingcache *map[address]cryptokey_route
|
||||
|
||||
// Check if the prefix is IPv4 or IPv6
|
||||
if addrlen == net.IPv6len {
|
||||
routingtable = &c.ipv6routes
|
||||
routingcache = &c.ipv6cache
|
||||
} else if addrlen == net.IPv4len {
|
||||
routingtable = &c.ipv4routes
|
||||
routingcache = &c.ipv4cache
|
||||
} else {
|
||||
return boxPubKey{}, errors.New("Unexpected prefix size")
|
||||
}
|
||||
|
||||
// Check if there's a cache entry for this addr
|
||||
if route, ok := (*routingcache)[addr]; ok {
|
||||
return route.destination, nil
|
||||
}
|
||||
|
||||
// No cache was found - start by converting the address into a net.IP
|
||||
ip := make(net.IP, addrlen)
|
||||
copy(ip[:addrlen], addr[:])
|
||||
|
||||
// Check if we have a route. At this point c.ipv6routes should be
|
||||
// pre-sorted so that the most specific routes are first
|
||||
for _, route := range *routingtable {
|
||||
// Does this subnet match the given IP?
|
||||
if route.subnet.Contains(ip) {
|
||||
// Check if the routing cache is above a certain size, if it is evict
|
||||
// a random entry so we can make room for this one. We take advantage
|
||||
// of the fact that the iteration order is random here
|
||||
for k := range *routingcache {
|
||||
if len(*routingcache) < 1024 {
|
||||
break
|
||||
}
|
||||
delete(*routingcache, k)
|
||||
}
|
||||
|
||||
// Cache the entry for future packets to get a faster lookup
|
||||
(*routingcache)[addr] = route
|
||||
|
||||
// Return the boxPubKey
|
||||
return route.destination, nil
|
||||
}
|
||||
}
|
||||
|
||||
// No route was found if we got to this point
|
||||
return boxPubKey{}, errors.New(fmt.Sprintf("No route to %s", ip.String()))
|
||||
}
|
||||
|
||||
// Removes a source subnet, which allows traffic with these source addresses to
|
||||
// be tunnelled using crypto-key routing.
|
||||
func (c *cryptokey) removeSourceSubnet(cidr string) error {
|
||||
// Is the CIDR we've been given valid?
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the prefix length and size
|
||||
_, prefixsize := ipnet.Mask.Size()
|
||||
|
||||
// Build our references to the routing sources
|
||||
var routingsources *[]net.IPNet
|
||||
|
||||
// Check if the prefix is IPv4 or IPv6
|
||||
if prefixsize == net.IPv6len*8 {
|
||||
routingsources = &c.ipv6sources
|
||||
} else if prefixsize == net.IPv4len*8 {
|
||||
routingsources = &c.ipv4sources
|
||||
} else {
|
||||
return errors.New("Unexpected prefix size")
|
||||
}
|
||||
|
||||
// Check if we already have this CIDR
|
||||
for idx, subnet := range *routingsources {
|
||||
if subnet.String() == ipnet.String() {
|
||||
*routingsources = append((*routingsources)[:idx], (*routingsources)[idx+1:]...)
|
||||
c.core.log.Println("Removed CKR source subnet", cidr)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("Source subnet not found")
|
||||
}
|
||||
|
||||
// Removes a destination route for the given CIDR to be tunnelled to the node
|
||||
// with the given BoxPubKey.
|
||||
func (c *cryptokey) removeRoute(cidr string, dest string) error {
|
||||
// Is the CIDR we've been given valid?
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the prefix length and size
|
||||
_, prefixsize := ipnet.Mask.Size()
|
||||
|
||||
// Build our references to the routing table and cache
|
||||
var routingtable *[]cryptokey_route
|
||||
var routingcache *map[address]cryptokey_route
|
||||
|
||||
// Check if the prefix is IPv4 or IPv6
|
||||
if prefixsize == net.IPv6len*8 {
|
||||
routingtable = &c.ipv6routes
|
||||
routingcache = &c.ipv6cache
|
||||
} else if prefixsize == net.IPv4len*8 {
|
||||
routingtable = &c.ipv4routes
|
||||
routingcache = &c.ipv4cache
|
||||
} else {
|
||||
return errors.New("Unexpected prefix size")
|
||||
}
|
||||
|
||||
// Decode the public key
|
||||
bpk, err := hex.DecodeString(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if len(bpk) != boxPubKeyLen {
|
||||
return errors.New(fmt.Sprintf("Incorrect key length for %s", dest))
|
||||
}
|
||||
netStr := ipnet.String()
|
||||
|
||||
for idx, route := range *routingtable {
|
||||
if bytes.Equal(route.destination[:], bpk) && route.subnet.String() == netStr {
|
||||
*routingtable = append((*routingtable)[:idx], (*routingtable)[idx+1:]...)
|
||||
for k := range *routingcache {
|
||||
delete(*routingcache, k)
|
||||
}
|
||||
c.core.log.Printf("Removed CKR destination subnet %s via %s\n", cidr, dest)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New(fmt.Sprintf("Route does not exists for %s", cidr))
|
||||
}
|
@@ -1,24 +0,0 @@
|
||||
package config
|
||||
|
||||
// NodeConfig defines all configuration values needed to run a signle yggdrasil node
|
||||
type NodeConfig struct {
|
||||
Listen string `comment:"Listen address for peer connections. Default is to listen for all\nTCP connections over IPv4 and IPv6 with a random port."`
|
||||
AdminListen string `comment:"Listen address for admin connections Default is to listen for local\nconnections only on TCP port 9001."`
|
||||
Peers []string `comment:"List of connection strings for static peers in URI format, i.e.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j"`
|
||||
AllowedEncryptionPublicKeys []string `comment:"List of peer encryption public keys to allow or incoming TCP\nconnections from. If left empty/undefined then all connections\nwill be allowed by default."`
|
||||
EncryptionPublicKey string `comment:"Your public encryption key. Your peers may ask you for this to put\ninto their AllowedEncryptionPublicKeys configuration."`
|
||||
EncryptionPrivateKey string `comment:"Your private encryption key. DO NOT share this with anyone!"`
|
||||
SigningPublicKey string `comment:"Your public signing key. You should not ordinarily need to share\nthis with anyone."`
|
||||
SigningPrivateKey string `comment:"Your private signing key. DO NOT share this with anyone!"`
|
||||
MulticastInterfaces []string `comment:"Regular expressions for which interfaces multicast peer discovery\nshould be enabled on. If none specified, multicast peer discovery is\ndisabled. The default value is .* which uses all interfaces."`
|
||||
IfName string `comment:"Local network interface name for TUN/TAP adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN/TAP."`
|
||||
IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."`
|
||||
IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
|
||||
//Net NetConfig `comment:"Extended options for connecting to peers over other networks."`
|
||||
}
|
||||
|
||||
// NetConfig defines network/proxy related configuration values
|
||||
type NetConfig struct {
|
||||
Tor TorConfig `comment:"Experimental options for configuring peerings over Tor."`
|
||||
I2P I2PConfig `comment:"Experimental options for configuring peerings over I2P."`
|
||||
}
|
@@ -8,9 +8,13 @@ import (
|
||||
"net"
|
||||
"regexp"
|
||||
|
||||
"yggdrasil/config"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/config"
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
|
||||
)
|
||||
|
||||
var buildName string
|
||||
var buildVersion string
|
||||
|
||||
// The Core object represents the Yggdrasil node. You should create a Core
|
||||
// object for each Yggdrasil node you plan to run.
|
||||
type Core struct {
|
||||
@@ -21,7 +25,6 @@ type Core struct {
|
||||
sigPriv sigPrivKey
|
||||
switchTable switchTable
|
||||
peers peers
|
||||
sigs sigManager
|
||||
sessions sessions
|
||||
router router
|
||||
dht dht
|
||||
@@ -49,7 +52,6 @@ func (c *Core) init(bpub *boxPubKey,
|
||||
c.boxPub, c.boxPriv = *bpub, *bpriv
|
||||
c.sigPub, c.sigPriv = *spub, *spriv
|
||||
c.admin.core = c
|
||||
c.sigs.init()
|
||||
c.searches.init(c)
|
||||
c.dht.init(c)
|
||||
c.sessions.init(c)
|
||||
@@ -60,12 +62,38 @@ func (c *Core) init(bpub *boxPubKey,
|
||||
c.tun.init(c)
|
||||
}
|
||||
|
||||
// Get the current build name. This is usually injected if built from git,
|
||||
// or returns "unknown" otherwise.
|
||||
func GetBuildName() string {
|
||||
if buildName == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return buildName
|
||||
}
|
||||
|
||||
// Get the current build version. This is usually injected if built from git,
|
||||
// or returns "unknown" otherwise.
|
||||
func GetBuildVersion() string {
|
||||
if buildVersion == "" {
|
||||
return "unknown"
|
||||
}
|
||||
return buildVersion
|
||||
}
|
||||
|
||||
// Starts up Yggdrasil using the provided NodeConfig, and outputs debug logging
|
||||
// through the provided log.Logger. The started stack will include TCP and UDP
|
||||
// sockets, a multicast discovery socket, an admin socket, router, switch and
|
||||
// DHT node.
|
||||
func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
|
||||
c.log = log
|
||||
|
||||
if name := GetBuildName(); name != "unknown" {
|
||||
c.log.Println("Build name:", name)
|
||||
}
|
||||
if version := GetBuildVersion(); version != "unknown" {
|
||||
c.log.Println("Build version:", version)
|
||||
}
|
||||
|
||||
c.log.Println("Starting up...")
|
||||
|
||||
var boxPub boxPubKey
|
||||
@@ -96,16 +124,59 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
|
||||
c.init(&boxPub, &boxPriv, &sigPub, &sigPriv)
|
||||
c.admin.init(c, nc.AdminListen)
|
||||
|
||||
if err := c.tcp.init(c, nc.Listen); err != nil {
|
||||
if err := c.tcp.init(c, nc.Listen, nc.ReadTimeout); err != nil {
|
||||
c.log.Println("Failed to start TCP interface")
|
||||
return err
|
||||
}
|
||||
|
||||
if nc.SwitchOptions.MaxTotalQueueSize >= SwitchQueueTotalMinSize {
|
||||
c.switchTable.queueTotalMaxSize = nc.SwitchOptions.MaxTotalQueueSize
|
||||
}
|
||||
|
||||
if err := c.switchTable.start(); err != nil {
|
||||
c.log.Println("Failed to start switch")
|
||||
return err
|
||||
}
|
||||
|
||||
c.sessions.setSessionFirewallState(nc.SessionFirewall.Enable)
|
||||
c.sessions.setSessionFirewallDefaults(
|
||||
nc.SessionFirewall.AllowFromDirect,
|
||||
nc.SessionFirewall.AllowFromRemote,
|
||||
nc.SessionFirewall.AlwaysAllowOutbound,
|
||||
)
|
||||
c.sessions.setSessionFirewallWhitelist(nc.SessionFirewall.WhitelistEncryptionPublicKeys)
|
||||
c.sessions.setSessionFirewallBlacklist(nc.SessionFirewall.BlacklistEncryptionPublicKeys)
|
||||
|
||||
if err := c.router.start(); err != nil {
|
||||
c.log.Println("Failed to start router")
|
||||
return err
|
||||
}
|
||||
|
||||
c.router.cryptokey.setEnabled(nc.TunnelRouting.Enable)
|
||||
if c.router.cryptokey.isEnabled() {
|
||||
c.log.Println("Crypto-key routing enabled")
|
||||
for ipv6, pubkey := range nc.TunnelRouting.IPv6Destinations {
|
||||
if err := c.router.cryptokey.addRoute(ipv6, pubkey); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
for _, source := range nc.TunnelRouting.IPv6Sources {
|
||||
if c.router.cryptokey.addSourceSubnet(source); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
for ipv4, pubkey := range nc.TunnelRouting.IPv4Destinations {
|
||||
if err := c.router.cryptokey.addRoute(ipv4, pubkey); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
for _, source := range nc.TunnelRouting.IPv4Sources {
|
||||
if c.router.cryptokey.addSourceSubnet(source); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.admin.start(); err != nil {
|
||||
c.log.Println("Failed to start admin socket")
|
||||
return err
|
||||
@@ -117,7 +188,7 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
|
||||
}
|
||||
|
||||
ip := net.IP(c.router.addr[:]).String()
|
||||
if err := c.tun.start(nc.IfName, nc.IfTAPMode, fmt.Sprintf("%s/8", ip), nc.IfMTU); err != nil {
|
||||
if err := c.tun.start(nc.IfName, nc.IfTAPMode, fmt.Sprintf("%s/%d", ip, 8*len(address_prefix)-1), nc.IfMTU); err != nil {
|
||||
c.log.Println("Failed to start TUN/TAP")
|
||||
return err
|
||||
}
|
||||
@@ -130,6 +201,7 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
|
||||
func (c *Core) Stop() {
|
||||
c.log.Println("Stopping...")
|
||||
c.tun.close()
|
||||
c.admin.close()
|
||||
}
|
||||
|
||||
// Generates a new encryption keypair. The encryption keys are used to
|
||||
@@ -175,8 +247,8 @@ func (c *Core) SetLogger(log *log.Logger) {
|
||||
|
||||
// Adds a peer. This should be specified in the peer URI format, i.e.
|
||||
// tcp://a.b.c.d:e, udp://a.b.c.d:e, socks://a.b.c.d:e/f.g.h.i:j
|
||||
func (c *Core) AddPeer(addr string) error {
|
||||
return c.admin.addPeer(addr)
|
||||
func (c *Core) AddPeer(addr string, sintf string) error {
|
||||
return c.admin.addPeer(addr, sintf)
|
||||
}
|
||||
|
||||
// Adds an expression to select multicast interfaces for peer discovery. This
|
||||
@@ -192,26 +264,31 @@ func (c *Core) AddAllowedEncryptionPublicKey(boxStr string) error {
|
||||
return c.admin.addAllowedEncryptionPublicKey(boxStr)
|
||||
}
|
||||
|
||||
// Gets the default admin listen address for your platform.
|
||||
func (c *Core) GetAdminDefaultListen() string {
|
||||
return defaults.GetDefaults().DefaultAdminListen
|
||||
}
|
||||
|
||||
// Gets the default TUN/TAP interface name for your platform.
|
||||
func (c *Core) GetTUNDefaultIfName() string {
|
||||
return getDefaults().defaultIfName
|
||||
return defaults.GetDefaults().DefaultIfName
|
||||
}
|
||||
|
||||
// Gets the default TUN/TAP interface MTU for your platform. This can be as high
|
||||
// as 65535, depending on platform, but is never lower than 1280.
|
||||
func (c *Core) GetTUNDefaultIfMTU() int {
|
||||
return getDefaults().defaultIfMTU
|
||||
return defaults.GetDefaults().DefaultIfMTU
|
||||
}
|
||||
|
||||
// Gets the maximum supported TUN/TAP interface MTU for your platform. This
|
||||
// can be as high as 65535, depending on platform, but is never lower than 1280.
|
||||
func (c *Core) GetTUNMaximumIfMTU() int {
|
||||
return getDefaults().maximumIfMTU
|
||||
return defaults.GetDefaults().MaximumIfMTU
|
||||
}
|
||||
|
||||
// Gets the default TUN/TAP interface mode for your platform.
|
||||
func (c *Core) GetTUNDefaultIfTAPMode() bool {
|
||||
return getDefaults().defaultIfTAPMode
|
||||
return defaults.GetDefaults().DefaultIfTAPMode
|
||||
}
|
||||
|
||||
// Gets the current TUN/TAP interface name.
|
||||
|
@@ -10,7 +10,7 @@ package yggdrasil
|
||||
|
||||
import _ "golang.org/x/net/ipv6" // TODO put this somewhere better
|
||||
|
||||
import "golang.org/x/net/proxy"
|
||||
//import "golang.org/x/net/proxy"
|
||||
|
||||
import "fmt"
|
||||
import "net"
|
||||
@@ -20,6 +20,22 @@ import "regexp"
|
||||
import _ "net/http/pprof"
|
||||
import "net/http"
|
||||
import "runtime"
|
||||
import "os"
|
||||
|
||||
import "github.com/yggdrasil-network/yggdrasil-go/src/defaults"
|
||||
|
||||
// Start the profiler in debug builds, if the required environment variable is set.
|
||||
func init() {
|
||||
envVarName := "PPROFLISTEN"
|
||||
hostPort := os.Getenv(envVarName)
|
||||
switch {
|
||||
case hostPort == "":
|
||||
fmt.Printf("DEBUG: %s not set, profiler not started.\n", envVarName)
|
||||
default:
|
||||
fmt.Printf("DEBUG: Starting pprof on %s\n", hostPort)
|
||||
go func() { fmt.Println(http.ListenAndServe(hostPort, nil)) }()
|
||||
}
|
||||
}
|
||||
|
||||
// Starts the function profiler. This is only supported when built with
|
||||
// '-tags build'.
|
||||
@@ -35,6 +51,7 @@ func (c *Core) Init() {
|
||||
bpub, bpriv := newBoxKeys()
|
||||
spub, spriv := newSigKeys()
|
||||
c.init(bpub, bpriv, spub, spriv)
|
||||
c.switchTable.start()
|
||||
c.router.start()
|
||||
}
|
||||
|
||||
@@ -67,7 +84,7 @@ func (c *Core) DEBUG_getPeers() *peers {
|
||||
func (ps *peers) DEBUG_newPeer(box boxPubKey, sig sigPubKey, link boxSharedKey) *peer {
|
||||
//in <-chan []byte,
|
||||
//out chan<- []byte) *peer {
|
||||
return ps.newPeer(&box, &sig, &link) //, in, out)
|
||||
return ps.newPeer(&box, &sig, &link, "(simulator)") //, in, out)
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -126,7 +143,42 @@ func (l *switchLocator) DEBUG_getCoords() []byte {
|
||||
}
|
||||
|
||||
func (c *Core) DEBUG_switchLookup(dest []byte) switchPort {
|
||||
return c.switchTable.lookup(dest)
|
||||
return c.switchTable.DEBUG_lookup(dest)
|
||||
}
|
||||
|
||||
// This does the switch layer lookups that decide how to route traffic.
|
||||
// Traffic uses greedy routing in a metric space, where the metric distance between nodes is equal to the distance between them on the tree.
|
||||
// Traffic must be routed to a node that is closer to the destination via the metric space distance.
|
||||
// In the event that two nodes are equally close, it gets routed to the one with the longest uptime (due to the order that things are iterated over).
|
||||
// The size of the outgoing packet queue is added to a node's tree distance when the cost of forwarding to a node, subject to the constraint that the real tree distance puts them closer to the destination than ourself.
|
||||
// Doing so adds a limited form of backpressure routing, based on local information, which allows us to forward traffic around *local* bottlenecks, provided that another greedy path exists.
|
||||
func (t *switchTable) DEBUG_lookup(dest []byte) switchPort {
|
||||
table := t.getTable()
|
||||
myDist := table.self.dist(dest)
|
||||
if myDist == 0 {
|
||||
return 0
|
||||
}
|
||||
// cost is in units of (expected distance) + (expected queue size), where expected distance is used as an approximation of the minimum backpressure gradient needed for packets to flow
|
||||
ports := t.core.peers.getPorts()
|
||||
var best switchPort
|
||||
bestCost := int64(^uint64(0) >> 1)
|
||||
for _, info := range table.elems {
|
||||
dist := info.locator.dist(dest)
|
||||
if !(dist < myDist) {
|
||||
continue
|
||||
}
|
||||
//p, isIn := ports[info.port]
|
||||
_, isIn := ports[info.port]
|
||||
if !isIn {
|
||||
continue
|
||||
}
|
||||
cost := int64(dist) // + p.getQueueSize()
|
||||
if cost < bestCost {
|
||||
best = info.port
|
||||
bestCost = cost
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -177,27 +229,25 @@ func DEBUG_wire_encode_coords(coords []byte) []byte {
|
||||
// DHT, via core
|
||||
|
||||
func (c *Core) DEBUG_getDHTSize() int {
|
||||
total := 0
|
||||
for bidx := 0; bidx < c.dht.nBuckets(); bidx++ {
|
||||
b := c.dht.getBucket(bidx)
|
||||
total += len(b.peers)
|
||||
total += len(b.other)
|
||||
}
|
||||
var total int
|
||||
c.router.doAdmin(func() {
|
||||
total = len(c.dht.table)
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
// TUN defaults
|
||||
|
||||
func (c *Core) DEBUG_GetTUNDefaultIfName() string {
|
||||
return getDefaults().defaultIfName
|
||||
return defaults.GetDefaults().DefaultIfName
|
||||
}
|
||||
|
||||
func (c *Core) DEBUG_GetTUNDefaultIfMTU() int {
|
||||
return getDefaults().defaultIfMTU
|
||||
return defaults.GetDefaults().DefaultIfMTU
|
||||
}
|
||||
|
||||
func (c *Core) DEBUG_GetTUNDefaultIfTAPMode() bool {
|
||||
return getDefaults().defaultIfTAPMode
|
||||
return defaults.GetDefaults().DefaultIfTAPMode
|
||||
}
|
||||
|
||||
// udpInterface
|
||||
@@ -347,12 +397,13 @@ func (c *Core) DEBUG_maybeSendUDPKeys(saddr string) {
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (c *Core) DEBUG_addPeer(addr string) {
|
||||
err := c.admin.addPeer(addr)
|
||||
err := c.admin.addPeer(addr, "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
func (c *Core) DEBUG_addSOCKSConn(socksaddr, peeraddr string) {
|
||||
go func() {
|
||||
dialer, err := proxy.SOCKS5("tcp", socksaddr, nil, proxy.Direct)
|
||||
@@ -370,10 +421,11 @@ func (c *Core) DEBUG_addSOCKSConn(socksaddr, peeraddr string) {
|
||||
}
|
||||
}()
|
||||
}
|
||||
*/
|
||||
|
||||
//*
|
||||
func (c *Core) DEBUG_setupAndStartGlobalTCPInterface(addrport string) {
|
||||
if err := c.tcp.init(c, addrport); err != nil {
|
||||
if err := c.tcp.init(c, addrport, 0); err != nil {
|
||||
c.log.Println("Failed to start TCP interface:", err)
|
||||
panic(err)
|
||||
}
|
||||
@@ -384,7 +436,7 @@ func (c *Core) DEBUG_getGlobalTCPAddr() *net.TCPAddr {
|
||||
}
|
||||
|
||||
func (c *Core) DEBUG_addTCPConn(saddr string) {
|
||||
c.tcp.call(saddr)
|
||||
c.tcp.call(saddr, nil, "")
|
||||
}
|
||||
|
||||
//*/
|
||||
@@ -452,25 +504,45 @@ func (c *Core) DEBUG_addAllowedEncryptionPublicKey(boxStr string) {
|
||||
|
||||
func DEBUG_simLinkPeers(p, q *peer) {
|
||||
// Sets q.out() to point to p and starts p.linkLoop()
|
||||
p.linkOut, q.linkOut = make(chan []byte, 1), make(chan []byte, 1)
|
||||
go func() {
|
||||
for bs := range p.linkOut {
|
||||
q.handlePacket(bs)
|
||||
goWorkers := func(source, dest *peer) {
|
||||
source.linkOut = make(chan []byte, 1)
|
||||
send := make(chan []byte, 1)
|
||||
source.out = func(bs []byte) {
|
||||
send <- bs
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for bs := range q.linkOut {
|
||||
p.handlePacket(bs)
|
||||
}
|
||||
}()
|
||||
p.out = func(bs []byte) {
|
||||
go q.handlePacket(bs)
|
||||
go source.linkLoop()
|
||||
go func() {
|
||||
var packets [][]byte
|
||||
for {
|
||||
select {
|
||||
case packet := <-source.linkOut:
|
||||
packets = append(packets, packet)
|
||||
continue
|
||||
case packet := <-send:
|
||||
packets = append(packets, packet)
|
||||
source.core.switchTable.idleIn <- source.port
|
||||
continue
|
||||
default:
|
||||
}
|
||||
if len(packets) > 0 {
|
||||
dest.handlePacket(packets[0])
|
||||
packets = packets[1:]
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case packet := <-source.linkOut:
|
||||
packets = append(packets, packet)
|
||||
case packet := <-send:
|
||||
packets = append(packets, packet)
|
||||
source.core.switchTable.idleIn <- source.port
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
q.out = func(bs []byte) {
|
||||
go p.handlePacket(bs)
|
||||
}
|
||||
go p.linkLoop()
|
||||
go q.linkLoop()
|
||||
goWorkers(p, q)
|
||||
goWorkers(q, p)
|
||||
p.core.switchTable.idleIn <- p.port
|
||||
q.core.switchTable.idleIn <- q.port
|
||||
}
|
||||
|
||||
func (c *Core) DEBUG_simFixMTU() {
|
||||
|
@@ -1,38 +1,15 @@
|
||||
package yggdrasil
|
||||
|
||||
/*
|
||||
|
||||
This part has the (kademlia-like) distributed hash table
|
||||
|
||||
It's used to look up coords for a NodeID
|
||||
|
||||
Every node participates in the DHT, and the DHT stores no real keys/values
|
||||
(Only the peer relationships / lookups are needed)
|
||||
|
||||
This version is intentionally fragile, by being recursive instead of iterative
|
||||
(it's also not parallel, as a result)
|
||||
This is to make sure that DHT black holes are visible if they exist
|
||||
(the iterative parallel approach tends to get around them sometimes)
|
||||
I haven't seen this get stuck on blackholes, but I also haven't proven it can't
|
||||
Slight changes *do* make it blackhole hard, bootstrapping isn't an easy problem
|
||||
|
||||
*/
|
||||
// A chord-like Distributed Hash Table (DHT).
|
||||
// Used to look up coords given a NodeID and bitmask (taken from an IPv6 address).
|
||||
// Keeps track of immediate successor, predecessor, and all peers.
|
||||
// Also keeps track of other nodes if they're closer in tree space than all other known nodes encountered when heading in either direction to that point, under the hypothesis that, for the kinds of networks we care about, this should probabilistically include the node needed to keep lookups to near O(logn) steps.
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Number of DHT buckets, equal to the number of bits in a NodeID.
|
||||
// Note that, in practice, nearly all of these will be empty.
|
||||
const dht_bucket_number = 8 * NodeIDLen
|
||||
|
||||
// Number of nodes to keep in each DHT bucket.
|
||||
// Additional entries may be kept for peers, for bootstrapping reasons, if they don't already have an entry in the bucket.
|
||||
const dht_bucket_size = 2
|
||||
|
||||
// Number of responses to include in a lookup.
|
||||
// If extras are given, they will be truncated from the response handler to prevent abuse.
|
||||
const dht_lookup_size = 16
|
||||
|
||||
// dhtInfo represents everything we know about a node in the DHT.
|
||||
@@ -41,11 +18,9 @@ type dhtInfo struct {
|
||||
nodeID_hidden *NodeID
|
||||
key boxPubKey
|
||||
coords []byte
|
||||
send time.Time // When we last sent a message
|
||||
recv time.Time // When we last received a message
|
||||
pings int // Decide when to drop
|
||||
throttle time.Duration // Time to wait before pinging a node to bootstrap buckets, increases exponentially from 1 second to 1 minute
|
||||
bootstrapSend time.Time // The time checked/updated as part of throttle checks
|
||||
recv time.Time // When we last received a message
|
||||
pings int // Time out if at least 3 consecutive maintenance pings drop
|
||||
throttle time.Duration
|
||||
}
|
||||
|
||||
// Returns the *NodeID associated with dhtInfo.key, calculating it on the fly the first time or from a cache all subsequent times.
|
||||
@@ -56,12 +31,6 @@ func (info *dhtInfo) getNodeID() *NodeID {
|
||||
return info.nodeID_hidden
|
||||
}
|
||||
|
||||
// The nodes we known in a bucket (a region of keyspace with a matching prefix of some length).
|
||||
type bucket struct {
|
||||
peers []*dhtInfo
|
||||
other []*dhtInfo
|
||||
}
|
||||
|
||||
// Request for a node to do a lookup.
|
||||
// Includes our key and coords so they can send a response back, and the destination NodeID we want to ask about.
|
||||
type dhtReq struct {
|
||||
@@ -74,30 +43,28 @@ type dhtReq struct {
|
||||
// Includes the key and coords of the node that's responding, and the destination they were asked about.
|
||||
// The main part is Infos []*dhtInfo, the lookup response.
|
||||
type dhtRes struct {
|
||||
Key boxPubKey // key to respond to
|
||||
Coords []byte // coords to respond to
|
||||
Key boxPubKey // key of the sender
|
||||
Coords []byte // coords of the sender
|
||||
Dest NodeID
|
||||
Infos []*dhtInfo // response
|
||||
}
|
||||
|
||||
// Information about a node, either taken from our table or from a lookup response.
|
||||
// Used to schedule pings at a later time (they're throttled to 1/second for background maintenance traffic).
|
||||
type dht_rumor struct {
|
||||
info *dhtInfo
|
||||
target *NodeID
|
||||
// Parts of a DHT req usable as a key in a map.
|
||||
type dhtReqKey struct {
|
||||
key boxPubKey
|
||||
dest NodeID
|
||||
}
|
||||
|
||||
// The main DHT struct.
|
||||
// Includes a slice of buckets, to organize known nodes based on their region of keyspace.
|
||||
// Also includes information about outstanding DHT requests and the rumor mill of nodes to ping at some point.
|
||||
type dht struct {
|
||||
core *Core
|
||||
nodeID NodeID
|
||||
buckets_hidden [dht_bucket_number]bucket // Extra is for the self-bucket
|
||||
peers chan *dhtInfo // other goroutines put incoming dht updates here
|
||||
reqs map[boxPubKey]map[NodeID]time.Time
|
||||
offset int
|
||||
rumorMill []dht_rumor
|
||||
core *Core
|
||||
nodeID NodeID
|
||||
peers chan *dhtInfo // other goroutines put incoming dht updates here
|
||||
reqs map[dhtReqKey]time.Time // Keeps track of recent outstanding requests
|
||||
callbacks map[dhtReqKey]dht_callbackInfo // Search and admin lookup callbacks
|
||||
// These next two could be replaced by a single linked list or similar...
|
||||
table map[NodeID]*dhtInfo
|
||||
imp []*dhtInfo
|
||||
}
|
||||
|
||||
// Initializes the DHT.
|
||||
@@ -105,11 +72,98 @@ func (t *dht) init(c *Core) {
|
||||
t.core = c
|
||||
t.nodeID = *t.core.GetNodeID()
|
||||
t.peers = make(chan *dhtInfo, 1024)
|
||||
t.reqs = make(map[boxPubKey]map[NodeID]time.Time)
|
||||
t.callbacks = make(map[dhtReqKey]dht_callbackInfo)
|
||||
t.reset()
|
||||
}
|
||||
|
||||
// Resets the DHT in response to coord changes.
|
||||
// This empties all info from the DHT and drops outstanding requests.
|
||||
func (t *dht) reset() {
|
||||
t.reqs = make(map[dhtReqKey]time.Time)
|
||||
t.table = make(map[NodeID]*dhtInfo)
|
||||
t.imp = nil
|
||||
}
|
||||
|
||||
// Does a DHT lookup and returns up to dht_lookup_size results.
|
||||
func (t *dht) lookup(nodeID *NodeID, everything bool) []*dhtInfo {
|
||||
results := make([]*dhtInfo, 0, len(t.table))
|
||||
for _, info := range t.table {
|
||||
results = append(results, info)
|
||||
}
|
||||
if len(results) > dht_lookup_size {
|
||||
// Drop the middle part, so we keep some nodes before and after.
|
||||
// This should help to bootstrap / recover more quickly.
|
||||
sort.SliceStable(results, func(i, j int) bool {
|
||||
return dht_ordered(nodeID, results[i].getNodeID(), results[j].getNodeID())
|
||||
})
|
||||
newRes := make([]*dhtInfo, 0, len(results))
|
||||
newRes = append(newRes, results[len(results)-dht_lookup_size/2:]...)
|
||||
newRes = append(newRes, results[:len(results)-dht_lookup_size/2]...)
|
||||
results = newRes
|
||||
results = results[:dht_lookup_size]
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// Insert into table, preserving the time we last sent a packet if the node was already in the table, otherwise setting that time to now.
|
||||
func (t *dht) insert(info *dhtInfo) {
|
||||
if *info.getNodeID() == t.nodeID {
|
||||
// This shouldn't happen, but don't add it if it does
|
||||
return
|
||||
}
|
||||
info.recv = time.Now()
|
||||
if oldInfo, isIn := t.table[*info.getNodeID()]; isIn {
|
||||
sameCoords := true
|
||||
if len(info.coords) != len(oldInfo.coords) {
|
||||
sameCoords = false
|
||||
} else {
|
||||
for idx := 0; idx < len(info.coords); idx++ {
|
||||
if info.coords[idx] != oldInfo.coords[idx] {
|
||||
sameCoords = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if sameCoords {
|
||||
info.throttle = oldInfo.throttle
|
||||
}
|
||||
}
|
||||
t.imp = nil // It needs to update to get a pointer to the new info
|
||||
t.table[*info.getNodeID()] = info
|
||||
}
|
||||
|
||||
// Return true if first/second/third are (partially) ordered correctly.
|
||||
func dht_ordered(first, second, third *NodeID) bool {
|
||||
lessOrEqual := func(first, second *NodeID) bool {
|
||||
for idx := 0; idx < NodeIDLen; idx++ {
|
||||
if first[idx] > second[idx] {
|
||||
return false
|
||||
}
|
||||
if first[idx] < second[idx] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
firstLessThanSecond := lessOrEqual(first, second)
|
||||
secondLessThanThird := lessOrEqual(second, third)
|
||||
thirdLessThanFirst := lessOrEqual(third, first)
|
||||
switch {
|
||||
case firstLessThanSecond && secondLessThanThird:
|
||||
// Nothing wrapped around 0, the easy case
|
||||
return true
|
||||
case thirdLessThanFirst && firstLessThanSecond:
|
||||
// Third wrapped around 0
|
||||
return true
|
||||
case secondLessThanThird && thirdLessThanFirst:
|
||||
// Second (and third) wrapped around 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Reads a request, performs a lookup, and responds.
|
||||
// If the node that sent the request isn't in our DHT, but should be, then we add them.
|
||||
// Update info about the node that sent the request.
|
||||
func (t *dht) handleReq(req *dhtReq) {
|
||||
// Send them what they asked for
|
||||
loc := t.core.switchTable.getLocator()
|
||||
@@ -121,270 +175,14 @@ func (t *dht) handleReq(req *dhtReq) {
|
||||
Infos: t.lookup(&req.Dest, false),
|
||||
}
|
||||
t.sendRes(&res, req)
|
||||
// Also (possibly) add them to our DHT
|
||||
// Also add them to our DHT
|
||||
info := dhtInfo{
|
||||
key: req.Key,
|
||||
coords: req.Coords,
|
||||
}
|
||||
t.insertIfNew(&info, false) // This seems DoSable (we just trust their coords...)
|
||||
//if req.dest != t.nodeID { t.ping(&info, info.getNodeID()) } // Or spam...
|
||||
}
|
||||
|
||||
// Reads a lookup response, checks that we had sent a matching request, and processes the response info.
|
||||
// This mainly consists of updating the node we asked in our DHT (they responded, so we know they're still alive), and adding the response info to the rumor mill.
|
||||
func (t *dht) handleRes(res *dhtRes) {
|
||||
t.core.searches.handleDHTRes(res)
|
||||
reqs, isIn := t.reqs[res.Key]
|
||||
if !isIn {
|
||||
return
|
||||
if _, isIn := t.table[*info.getNodeID()]; !isIn && t.isImportant(&info) {
|
||||
t.ping(&info, nil)
|
||||
}
|
||||
_, isIn = reqs[res.Dest]
|
||||
if !isIn {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
rinfo := dhtInfo{
|
||||
key: res.Key,
|
||||
coords: res.Coords,
|
||||
send: now, // Technically wrong but should be OK...
|
||||
recv: now,
|
||||
throttle: time.Second,
|
||||
bootstrapSend: now,
|
||||
}
|
||||
// If they're already in the table, then keep the correct send time
|
||||
bidx, isOK := t.getBucketIndex(rinfo.getNodeID())
|
||||
if !isOK {
|
||||
return
|
||||
}
|
||||
b := t.getBucket(bidx)
|
||||
for _, oldinfo := range b.peers {
|
||||
if oldinfo.key == rinfo.key {
|
||||
rinfo.send = oldinfo.send
|
||||
rinfo.throttle = oldinfo.throttle
|
||||
rinfo.bootstrapSend = oldinfo.bootstrapSend
|
||||
}
|
||||
}
|
||||
for _, oldinfo := range b.other {
|
||||
if oldinfo.key == rinfo.key {
|
||||
rinfo.send = oldinfo.send
|
||||
rinfo.throttle = oldinfo.throttle
|
||||
rinfo.bootstrapSend = oldinfo.bootstrapSend
|
||||
}
|
||||
}
|
||||
// Insert into table
|
||||
t.insert(&rinfo, false)
|
||||
if res.Dest == *rinfo.getNodeID() {
|
||||
return
|
||||
} // No infinite recursions
|
||||
if len(res.Infos) > dht_lookup_size {
|
||||
// Ignore any "extra" lookup results
|
||||
res.Infos = res.Infos[:dht_lookup_size]
|
||||
}
|
||||
for _, info := range res.Infos {
|
||||
if dht_firstCloserThanThird(info.getNodeID(), &res.Dest, rinfo.getNodeID()) {
|
||||
t.addToMill(info, info.getNodeID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Does a DHT lookup and returns the results, sorted in ascending order of distance from the destination.
|
||||
func (t *dht) lookup(nodeID *NodeID, allowCloser bool) []*dhtInfo {
|
||||
// FIXME this allocates a bunch, sorts, and keeps the part it likes
|
||||
// It would be better to only track the part it likes to begin with
|
||||
addInfos := func(res []*dhtInfo, infos []*dhtInfo) []*dhtInfo {
|
||||
for _, info := range infos {
|
||||
if info == nil {
|
||||
panic("Should never happen!")
|
||||
}
|
||||
if allowCloser || dht_firstCloserThanThird(info.getNodeID(), nodeID, &t.nodeID) {
|
||||
res = append(res, info)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
var res []*dhtInfo
|
||||
for bidx := 0; bidx < t.nBuckets(); bidx++ {
|
||||
b := t.getBucket(bidx)
|
||||
res = addInfos(res, b.peers)
|
||||
res = addInfos(res, b.other)
|
||||
}
|
||||
doSort := func(infos []*dhtInfo) {
|
||||
less := func(i, j int) bool {
|
||||
return dht_firstCloserThanThird(infos[i].getNodeID(),
|
||||
nodeID,
|
||||
infos[j].getNodeID())
|
||||
}
|
||||
sort.SliceStable(infos, less)
|
||||
}
|
||||
doSort(res)
|
||||
if len(res) > dht_lookup_size {
|
||||
res = res[:dht_lookup_size]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Gets the bucket for a specified matching prefix length.
|
||||
func (t *dht) getBucket(bidx int) *bucket {
|
||||
return &t.buckets_hidden[bidx]
|
||||
}
|
||||
|
||||
// Lists the number of buckets.
|
||||
func (t *dht) nBuckets() int {
|
||||
return len(t.buckets_hidden)
|
||||
}
|
||||
|
||||
// Inserts a node into the DHT if they meet certain requirements.
|
||||
// In particular, they must either be a peer that's not already in the DHT, or else be someone we should insert into the DHT (see: shouldInsert).
|
||||
func (t *dht) insertIfNew(info *dhtInfo, isPeer bool) {
|
||||
// Insert if no "other" entry already exists
|
||||
nodeID := info.getNodeID()
|
||||
bidx, isOK := t.getBucketIndex(nodeID)
|
||||
if !isOK {
|
||||
return
|
||||
}
|
||||
b := t.getBucket(bidx)
|
||||
if (isPeer && !b.containsOther(info)) || t.shouldInsert(info) {
|
||||
// We've never heard this node before
|
||||
// TODO is there a better time than "now" to set send/recv to?
|
||||
// (Is there another "natural" choice that bootstraps faster?)
|
||||
info.send = time.Now()
|
||||
info.recv = info.send
|
||||
t.insert(info, isPeer)
|
||||
}
|
||||
}
|
||||
|
||||
// Adds a node to the DHT, possibly removing another node in the process.
|
||||
func (t *dht) insert(info *dhtInfo, isPeer bool) {
|
||||
// First update the time on this info
|
||||
info.recv = time.Now()
|
||||
// Get the bucket for this node
|
||||
nodeID := info.getNodeID()
|
||||
bidx, isOK := t.getBucketIndex(nodeID)
|
||||
if !isOK {
|
||||
return
|
||||
}
|
||||
b := t.getBucket(bidx)
|
||||
if !isPeer && !b.containsOther(info) {
|
||||
// This is a new entry, give it an old age so it's pinged sooner
|
||||
// This speeds up bootstrapping
|
||||
info.recv = info.recv.Add(-time.Hour)
|
||||
}
|
||||
if isPeer || info.throttle > time.Minute {
|
||||
info.throttle = time.Minute
|
||||
}
|
||||
// First drop any existing entry from the bucket
|
||||
b.drop(&info.key)
|
||||
// Now add to the *end* of the bucket
|
||||
if isPeer {
|
||||
// TODO make sure we don't duplicate peers in b.other too
|
||||
b.peers = append(b.peers, info)
|
||||
return
|
||||
}
|
||||
b.other = append(b.other, info)
|
||||
// Shrink from the *front* to requied size
|
||||
for len(b.other) > dht_bucket_size {
|
||||
b.other = b.other[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the bucket index for the bucket where we would put the given NodeID.
|
||||
func (t *dht) getBucketIndex(nodeID *NodeID) (int, bool) {
|
||||
for bidx := 0; bidx < t.nBuckets(); bidx++ {
|
||||
them := nodeID[bidx/8] & (0x80 >> byte(bidx%8))
|
||||
me := t.nodeID[bidx/8] & (0x80 >> byte(bidx%8))
|
||||
if them != me {
|
||||
return bidx, true
|
||||
}
|
||||
}
|
||||
return t.nBuckets(), false
|
||||
}
|
||||
|
||||
// Helper called by containsPeer, containsOther, and contains.
|
||||
// Returns true if a node with the same ID *and coords* is already in the given part of the bucket.
|
||||
func dht_bucket_check(newInfo *dhtInfo, infos []*dhtInfo) bool {
|
||||
// Compares if key and coords match
|
||||
if newInfo == nil {
|
||||
panic("Should never happen")
|
||||
}
|
||||
for _, info := range infos {
|
||||
if info == nil {
|
||||
panic("Should never happen")
|
||||
}
|
||||
if info.key != newInfo.key {
|
||||
continue
|
||||
}
|
||||
if len(info.coords) != len(newInfo.coords) {
|
||||
continue
|
||||
}
|
||||
match := true
|
||||
for idx := 0; idx < len(info.coords); idx++ {
|
||||
if info.coords[idx] != newInfo.coords[idx] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Calls bucket_check over the bucket's peers infos.
|
||||
func (b *bucket) containsPeer(info *dhtInfo) bool {
|
||||
return dht_bucket_check(info, b.peers)
|
||||
}
|
||||
|
||||
// Calls bucket_check over the bucket's other info.
|
||||
func (b *bucket) containsOther(info *dhtInfo) bool {
|
||||
return dht_bucket_check(info, b.other)
|
||||
}
|
||||
|
||||
// returns containsPeer || containsOther
|
||||
func (b *bucket) contains(info *dhtInfo) bool {
|
||||
return b.containsPeer(info) || b.containsOther(info)
|
||||
}
|
||||
|
||||
// Removes a node with the corresponding key, if any, from a bucket.
|
||||
func (b *bucket) drop(key *boxPubKey) {
|
||||
clean := func(infos []*dhtInfo) []*dhtInfo {
|
||||
cleaned := infos[:0]
|
||||
for _, info := range infos {
|
||||
if info.key == *key {
|
||||
continue
|
||||
}
|
||||
cleaned = append(cleaned, info)
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
b.peers = clean(b.peers)
|
||||
b.other = clean(b.other)
|
||||
}
|
||||
|
||||
// Sends a lookup request to the specified node.
|
||||
func (t *dht) sendReq(req *dhtReq, dest *dhtInfo) {
|
||||
// Send a dhtReq to the node in dhtInfo
|
||||
bs := req.encode()
|
||||
shared := t.core.sessions.getSharedKey(&t.core.boxPriv, &dest.key)
|
||||
payload, nonce := boxSeal(shared, bs, nil)
|
||||
p := wire_protoTrafficPacket{
|
||||
Coords: dest.coords,
|
||||
ToKey: dest.key,
|
||||
FromKey: t.core.boxPub,
|
||||
Nonce: *nonce,
|
||||
Payload: payload,
|
||||
}
|
||||
packet := p.encode()
|
||||
t.core.router.out(packet)
|
||||
reqsToDest, isIn := t.reqs[dest.key]
|
||||
if !isIn {
|
||||
t.reqs[dest.key] = make(map[NodeID]time.Time)
|
||||
reqsToDest, isIn = t.reqs[dest.key]
|
||||
if !isIn {
|
||||
panic("This should never happen")
|
||||
}
|
||||
}
|
||||
reqsToDest[req.Dest] = time.Now()
|
||||
}
|
||||
|
||||
// Sends a lookup response to the specified node.
|
||||
@@ -404,56 +202,73 @@ func (t *dht) sendRes(res *dhtRes, req *dhtReq) {
|
||||
t.core.router.out(packet)
|
||||
}
|
||||
|
||||
// Returns true of a bucket contains no peers and no other nodes.
|
||||
func (b *bucket) isEmpty() bool {
|
||||
return len(b.peers)+len(b.other) == 0
|
||||
type dht_callbackInfo struct {
|
||||
f func(*dhtRes)
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// Gets the next node that should be pinged from the bucket.
|
||||
// There's a cooldown of 6 seconds between ping attempts for each node, to give them time to respond.
|
||||
// It returns the least recently pinged node, subject to that send cooldown.
|
||||
func (b *bucket) nextToPing() *dhtInfo {
|
||||
// Check the nodes in the bucket
|
||||
// Return whichever one responded least recently
|
||||
// Delay of 6 seconds between pinging the same node
|
||||
// Gives them time to respond
|
||||
// And time between traffic loss from short term congestion in the network
|
||||
var toPing *dhtInfo
|
||||
update := func(infos []*dhtInfo) {
|
||||
for _, next := range infos {
|
||||
if time.Since(next.send) < 6*time.Second {
|
||||
continue
|
||||
}
|
||||
if toPing == nil || next.recv.Before(toPing.recv) {
|
||||
toPing = next
|
||||
}
|
||||
}
|
||||
// Adds a callback and removes it after some timeout.
|
||||
func (t *dht) addCallback(rq *dhtReqKey, callback func(*dhtRes)) {
|
||||
info := dht_callbackInfo{callback, time.Now().Add(6 * time.Second)}
|
||||
t.callbacks[*rq] = info
|
||||
}
|
||||
|
||||
// Reads a lookup response, checks that we had sent a matching request, and processes the response info.
|
||||
// This mainly consists of updating the node we asked in our DHT (they responded, so we know they're still alive), and deciding if we want to do anything with their responses
|
||||
func (t *dht) handleRes(res *dhtRes) {
|
||||
rq := dhtReqKey{res.Key, res.Dest}
|
||||
if callback, isIn := t.callbacks[rq]; isIn {
|
||||
callback.f(res)
|
||||
delete(t.callbacks, rq)
|
||||
}
|
||||
update(b.peers)
|
||||
update(b.other)
|
||||
return toPing
|
||||
}
|
||||
|
||||
// Returns a useful target address to ask about for pings.
|
||||
// Equal to the our node's ID, except for exactly 1 bit at the bucket index.
|
||||
func (t *dht) getTarget(bidx int) *NodeID {
|
||||
targetID := t.nodeID
|
||||
targetID[bidx/8] ^= 0x80 >> byte(bidx%8)
|
||||
return &targetID
|
||||
}
|
||||
|
||||
// Sends a ping to a node, or removes the node if it has failed to respond to too many pings.
|
||||
// If target is nil, we will ask the node about our own NodeID.
|
||||
func (t *dht) ping(info *dhtInfo, target *NodeID) {
|
||||
if info.pings > 2 {
|
||||
bidx, isOK := t.getBucketIndex(info.getNodeID())
|
||||
if !isOK {
|
||||
panic("This should never happen")
|
||||
}
|
||||
b := t.getBucket(bidx)
|
||||
b.drop(&info.key)
|
||||
_, isIn := t.reqs[rq]
|
||||
if !isIn {
|
||||
return
|
||||
}
|
||||
delete(t.reqs, rq)
|
||||
rinfo := dhtInfo{
|
||||
key: res.Key,
|
||||
coords: res.Coords,
|
||||
}
|
||||
if t.isImportant(&rinfo) {
|
||||
t.insert(&rinfo)
|
||||
}
|
||||
for _, info := range res.Infos {
|
||||
if *info.getNodeID() == t.nodeID {
|
||||
continue
|
||||
} // Skip self
|
||||
if _, isIn := t.table[*info.getNodeID()]; isIn {
|
||||
// TODO? don't skip if coords are different?
|
||||
continue
|
||||
}
|
||||
if t.isImportant(info) {
|
||||
t.ping(info, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sends a lookup request to the specified node.
|
||||
func (t *dht) sendReq(req *dhtReq, dest *dhtInfo) {
|
||||
// Send a dhtReq to the node in dhtInfo
|
||||
bs := req.encode()
|
||||
shared := t.core.sessions.getSharedKey(&t.core.boxPriv, &dest.key)
|
||||
payload, nonce := boxSeal(shared, bs, nil)
|
||||
p := wire_protoTrafficPacket{
|
||||
Coords: dest.coords,
|
||||
ToKey: dest.key,
|
||||
FromKey: t.core.boxPub,
|
||||
Nonce: *nonce,
|
||||
Payload: payload,
|
||||
}
|
||||
packet := p.encode()
|
||||
t.core.router.out(packet)
|
||||
rq := dhtReqKey{dest.key, req.Dest}
|
||||
t.reqs[rq] = time.Now()
|
||||
}
|
||||
|
||||
// Sends a lookup to this info, looking for the target.
|
||||
func (t *dht) ping(info *dhtInfo, target *NodeID) {
|
||||
// Creates a req for the node at dhtInfo, asking them about the target (if one is given) or themself (if no target is given)
|
||||
if target == nil {
|
||||
target = &t.nodeID
|
||||
}
|
||||
@@ -464,160 +279,120 @@ func (t *dht) ping(info *dhtInfo, target *NodeID) {
|
||||
Coords: coords,
|
||||
Dest: *target,
|
||||
}
|
||||
info.pings++
|
||||
info.send = time.Now()
|
||||
t.sendReq(&req, info)
|
||||
}
|
||||
|
||||
// Adds a node info and target to the rumor mill.
|
||||
// The node will be asked about the target at a later point, if doing so would still be useful at the time.
|
||||
func (t *dht) addToMill(info *dhtInfo, target *NodeID) {
|
||||
rumor := dht_rumor{
|
||||
info: info,
|
||||
target: target,
|
||||
}
|
||||
t.rumorMill = append(t.rumorMill, rumor)
|
||||
}
|
||||
|
||||
// Regular periodic maintenance.
|
||||
// If the mill is empty, it adds two pings to the rumor mill.
|
||||
// The first is to the node that responded least recently, provided that it's been at least 1 minute, to make sure we eventually detect and remove unresponsive nodes.
|
||||
// The second is used for bootstrapping, and attempts to fill some bucket, iterating over buckets and resetting after it hits the last non-empty one.
|
||||
// If the mill is not empty, it pops nodes from the mill until it finds one that would be useful to ping (see: shouldInsert), and then pings it.
|
||||
// Periodic maintenance work to keep important DHT nodes alive.
|
||||
func (t *dht) doMaintenance() {
|
||||
// First clean up reqs
|
||||
for key, reqs := range t.reqs {
|
||||
for target, timeout := range reqs {
|
||||
if time.Since(timeout) > time.Minute {
|
||||
delete(reqs, target)
|
||||
}
|
||||
}
|
||||
if len(reqs) == 0 {
|
||||
delete(t.reqs, key)
|
||||
now := time.Now()
|
||||
newReqs := make(map[dhtReqKey]time.Time, len(t.reqs))
|
||||
for key, start := range t.reqs {
|
||||
if now.Sub(start) < 6*time.Second {
|
||||
newReqs[key] = start
|
||||
}
|
||||
}
|
||||
if len(t.rumorMill) == 0 {
|
||||
// Ping the least recently contacted node
|
||||
// This is to make sure we eventually notice when someone times out
|
||||
var oldest *dhtInfo
|
||||
last := 0
|
||||
for bidx := 0; bidx < t.nBuckets(); bidx++ {
|
||||
b := t.getBucket(bidx)
|
||||
if !b.isEmpty() {
|
||||
last = bidx
|
||||
toPing := b.nextToPing()
|
||||
if toPing == nil {
|
||||
continue
|
||||
} // We've recently pinged everyone in b
|
||||
if oldest == nil || toPing.recv.Before(oldest.recv) {
|
||||
oldest = toPing
|
||||
}
|
||||
}
|
||||
t.reqs = newReqs
|
||||
newCallbacks := make(map[dhtReqKey]dht_callbackInfo, len(t.callbacks))
|
||||
for key, callback := range t.callbacks {
|
||||
if now.Before(callback.time) {
|
||||
newCallbacks[key] = callback
|
||||
}
|
||||
if oldest != nil && time.Since(oldest.recv) > time.Minute {
|
||||
// Ping the oldest node in the DHT, but don't ping nodes that have been checked within the last minute
|
||||
t.addToMill(oldest, nil)
|
||||
}
|
||||
// Refresh buckets
|
||||
if t.offset > last {
|
||||
t.offset = 0
|
||||
}
|
||||
target := t.getTarget(t.offset)
|
||||
func() {
|
||||
closer := t.lookup(target, false)
|
||||
for _, info := range closer {
|
||||
// Throttled ping of a node that's closer to the destination
|
||||
if time.Since(info.recv) > info.throttle {
|
||||
t.addToMill(info, target)
|
||||
t.offset++
|
||||
info.bootstrapSend = time.Now()
|
||||
info.throttle *= 2
|
||||
if info.throttle > time.Minute {
|
||||
info.throttle = time.Minute
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(closer) == 0 {
|
||||
// If we don't know of anyone closer at all, then there's a hole in our dht
|
||||
// Ping the closest node we know and ignore the throttle, to try to fill it
|
||||
for _, info := range t.lookup(target, true) {
|
||||
t.addToMill(info, target)
|
||||
t.offset++
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
//t.offset++
|
||||
}
|
||||
for len(t.rumorMill) > 0 {
|
||||
var rumor dht_rumor
|
||||
rumor, t.rumorMill = t.rumorMill[0], t.rumorMill[1:]
|
||||
if rumor.target == rumor.info.getNodeID() {
|
||||
// Note that the above is a pointer comparison, and target can be nil
|
||||
// This is only for adding new nodes (learned from other lookups)
|
||||
// It only makes sense to ping if the node isn't already in the table
|
||||
if !t.shouldInsert(rumor.info) {
|
||||
continue
|
||||
t.callbacks = newCallbacks
|
||||
for infoID, info := range t.table {
|
||||
if now.Sub(info.recv) > time.Minute || info.pings > 3 {
|
||||
delete(t.table, infoID)
|
||||
t.imp = nil
|
||||
}
|
||||
}
|
||||
for _, info := range t.getImportant() {
|
||||
if now.Sub(info.recv) > info.throttle {
|
||||
t.ping(info, nil)
|
||||
info.pings++
|
||||
info.throttle += time.Second
|
||||
if info.throttle > 30*time.Second {
|
||||
info.throttle = 30 * time.Second
|
||||
}
|
||||
}
|
||||
t.ping(rumor.info, rumor.target)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if it would be worth pinging the specified node.
|
||||
// This requires that the bucket doesn't already contain the node, and that either the bucket isn't full yet or the node is closer to us in keyspace than some other node in that bucket.
|
||||
func (t *dht) shouldInsert(info *dhtInfo) bool {
|
||||
bidx, isOK := t.getBucketIndex(info.getNodeID())
|
||||
if !isOK {
|
||||
// Gets a list of important nodes, used by isImportant.
|
||||
func (t *dht) getImportant() []*dhtInfo {
|
||||
if t.imp == nil {
|
||||
// Get a list of all known nodes
|
||||
infos := make([]*dhtInfo, 0, len(t.table))
|
||||
for _, info := range t.table {
|
||||
infos = append(infos, info)
|
||||
}
|
||||
// Sort them by increasing order in distance along the ring
|
||||
sort.SliceStable(infos, func(i, j int) bool {
|
||||
// Sort in order of predecessors (!), reverse from chord normal, because it plays nicer with zero bits for unknown parts of target addresses
|
||||
return dht_ordered(infos[j].getNodeID(), infos[i].getNodeID(), &t.nodeID)
|
||||
})
|
||||
// Keep the ones that are no further than the closest seen so far
|
||||
minDist := ^uint64(0)
|
||||
loc := t.core.switchTable.getLocator()
|
||||
important := infos[:0]
|
||||
for _, info := range infos {
|
||||
dist := uint64(loc.dist(info.coords))
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
important = append(important, info)
|
||||
}
|
||||
}
|
||||
var temp []*dhtInfo
|
||||
minDist = ^uint64(0)
|
||||
for idx := len(infos) - 1; idx >= 0; idx-- {
|
||||
info := infos[idx]
|
||||
dist := uint64(loc.dist(info.coords))
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
temp = append(temp, info)
|
||||
}
|
||||
}
|
||||
for idx := len(temp) - 1; idx >= 0; idx-- {
|
||||
important = append(important, temp[idx])
|
||||
}
|
||||
t.imp = important
|
||||
}
|
||||
return t.imp
|
||||
}
|
||||
|
||||
// Returns true if this is a node we need to keep track of for the DHT to work.
|
||||
func (t *dht) isImportant(ninfo *dhtInfo) bool {
|
||||
if ninfo.key == t.core.boxPub {
|
||||
return false
|
||||
}
|
||||
b := t.getBucket(bidx)
|
||||
if b.containsOther(info) {
|
||||
return false
|
||||
}
|
||||
if len(b.other) < dht_bucket_size {
|
||||
return true
|
||||
}
|
||||
for _, other := range b.other {
|
||||
if dht_firstCloserThanThird(info.getNodeID(), &t.nodeID, other.getNodeID()) {
|
||||
important := t.getImportant()
|
||||
// Check if ninfo is of equal or greater importance to what we already know
|
||||
loc := t.core.switchTable.getLocator()
|
||||
ndist := uint64(loc.dist(ninfo.coords))
|
||||
minDist := ^uint64(0)
|
||||
for _, info := range important {
|
||||
if (*info.getNodeID() == *ninfo.getNodeID()) ||
|
||||
(ndist < minDist && dht_ordered(info.getNodeID(), ninfo.getNodeID(), &t.nodeID)) {
|
||||
// Either the same node, or a better one
|
||||
return true
|
||||
}
|
||||
dist := uint64(loc.dist(info.coords))
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
}
|
||||
}
|
||||
minDist = ^uint64(0)
|
||||
for idx := len(important) - 1; idx >= 0; idx-- {
|
||||
info := important[idx]
|
||||
if (*info.getNodeID() == *ninfo.getNodeID()) ||
|
||||
(ndist < minDist && dht_ordered(&t.nodeID, ninfo.getNodeID(), info.getNodeID())) {
|
||||
// Either the same node, or a better one
|
||||
return true
|
||||
}
|
||||
dist := uint64(loc.dist(info.coords))
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
}
|
||||
}
|
||||
// We didn't find any important node that ninfo is better than
|
||||
return false
|
||||
}
|
||||
|
||||
// Returns true if the keyspace distance between the first and second node is smaller than the keyspace distance between the second and third node.
|
||||
func dht_firstCloserThanThird(first *NodeID,
|
||||
second *NodeID,
|
||||
third *NodeID) bool {
|
||||
for idx := 0; idx < NodeIDLen; idx++ {
|
||||
f := first[idx] ^ second[idx]
|
||||
t := third[idx] ^ second[idx]
|
||||
if f == t {
|
||||
continue
|
||||
}
|
||||
return f < t
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Resets the DHT in response to coord changes.
|
||||
// This empties all buckets, resets the bootstrapping cycle to 0, and empties the rumor mill.
|
||||
// It adds all old "other" node info to the rumor mill, so they'll be pinged quickly.
|
||||
// If those nodes haven't also changed coords, then this is a relatively quick way to notify those nodes of our new coords and re-add them to our own DHT if they respond.
|
||||
func (t *dht) reset() {
|
||||
// This is mostly so bootstrapping will reset to resend coords into the network
|
||||
t.offset = 0
|
||||
t.rumorMill = nil // reset mill
|
||||
for _, b := range t.buckets_hidden {
|
||||
b.peers = b.peers[:0]
|
||||
for _, info := range b.other {
|
||||
// Add other nodes to the rumor mill so they'll be pinged soon
|
||||
// This will hopefully tell them our coords and re-learn theirs quickly if they haven't changed
|
||||
t.addToMill(info, info.getNodeID())
|
||||
}
|
||||
b.other = b.other[:0]
|
||||
}
|
||||
}
|
||||
|
@@ -13,6 +13,7 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv6"
|
||||
@@ -23,11 +24,17 @@ type macAddress [6]byte
|
||||
const len_ETHER = 14
|
||||
|
||||
type icmpv6 struct {
|
||||
tun *tunDevice
|
||||
peermac macAddress
|
||||
peerlladdr net.IP
|
||||
mylladdr net.IP
|
||||
mymac macAddress
|
||||
tun *tunDevice
|
||||
mylladdr net.IP
|
||||
mymac macAddress
|
||||
peermacs map[address]neighbor
|
||||
}
|
||||
|
||||
type neighbor struct {
|
||||
mac macAddress
|
||||
learned bool
|
||||
lastadvertisement time.Time
|
||||
lastsolicitation time.Time
|
||||
}
|
||||
|
||||
// Marshal returns the binary encoding of h.
|
||||
@@ -52,13 +59,16 @@ func ipv6Header_Marshal(h *ipv6.Header) ([]byte, error) {
|
||||
// addresses.
|
||||
func (i *icmpv6) init(t *tunDevice) {
|
||||
i.tun = t
|
||||
i.peermacs = make(map[address]neighbor)
|
||||
|
||||
// Our MAC address and link-local address
|
||||
copy(i.mymac[:], []byte{
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x02})
|
||||
i.mymac = macAddress{
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x02}
|
||||
i.mylladdr = net.IP{
|
||||
0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFE}
|
||||
copy(i.mymac[:], i.tun.core.router.addr[:])
|
||||
copy(i.mylladdr[9:], i.tun.core.router.addr[1:])
|
||||
}
|
||||
|
||||
// Parses an incoming ICMPv6 packet. The packet provided may be either an
|
||||
@@ -73,7 +83,7 @@ func (i *icmpv6) parse_packet(datain []byte) {
|
||||
if i.tun.iface.IsTAP() {
|
||||
response, err = i.parse_packet_tap(datain)
|
||||
} else {
|
||||
response, err = i.parse_packet_tun(datain)
|
||||
response, err = i.parse_packet_tun(datain, nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -89,16 +99,14 @@ func (i *icmpv6) parse_packet(datain []byte) {
|
||||
// A response buffer is also created for the response message, also complete
|
||||
// with ethernet headers.
|
||||
func (i *icmpv6) parse_packet_tap(datain []byte) ([]byte, error) {
|
||||
// Store the peer MAC address
|
||||
copy(i.peermac[:6], datain[6:12])
|
||||
|
||||
// Ignore non-IPv6 frames
|
||||
if binary.BigEndian.Uint16(datain[12:14]) != uint16(0x86DD) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Hand over to parse_packet_tun to interpret the IPv6 packet
|
||||
ipv6packet, err := i.parse_packet_tun(datain[len_ETHER:])
|
||||
mac := datain[6:12]
|
||||
ipv6packet, err := i.parse_packet_tun(datain[len_ETHER:], &mac)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -120,7 +128,7 @@ func (i *icmpv6) parse_packet_tap(datain []byte) ([]byte, error) {
|
||||
// sanity checks on the packet - i.e. is the packet an ICMPv6 packet, does the
|
||||
// ICMPv6 message match a known expected type. The relevant handler function
|
||||
// is then called and a response packet may be returned.
|
||||
func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) {
|
||||
func (i *icmpv6) parse_packet_tun(datain []byte, datamac *[]byte) ([]byte, error) {
|
||||
// Parse the IPv6 packet headers
|
||||
ipv6Header, err := ipv6.ParseHeader(datain[:ipv6.HeaderLen])
|
||||
if err != nil {
|
||||
@@ -137,9 +145,6 @@ func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Store the peer link local address, it will come in useful later
|
||||
copy(i.peerlladdr[:], ipv6Header.Src[:])
|
||||
|
||||
// Parse the ICMPv6 message contents
|
||||
icmpv6Header, err := icmp.ParseMessage(58, datain[ipv6.HeaderLen:])
|
||||
if err != nil {
|
||||
@@ -149,24 +154,35 @@ func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) {
|
||||
// Check for a supported message type
|
||||
switch icmpv6Header.Type {
|
||||
case ipv6.ICMPTypeNeighborSolicitation:
|
||||
{
|
||||
response, err := i.handle_ndp(datain[ipv6.HeaderLen:])
|
||||
if err == nil {
|
||||
// Create our ICMPv6 response
|
||||
responsePacket, err := i.create_icmpv6_tun(
|
||||
ipv6Header.Src, i.mylladdr,
|
||||
ipv6.ICMPTypeNeighborAdvertisement, 0,
|
||||
&icmp.DefaultMessageBody{Data: response})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send it back
|
||||
return responsePacket, nil
|
||||
} else {
|
||||
response, err := i.handle_ndp(datain[ipv6.HeaderLen:])
|
||||
if err == nil {
|
||||
// Create our ICMPv6 response
|
||||
responsePacket, err := i.create_icmpv6_tun(
|
||||
ipv6Header.Src, i.mylladdr,
|
||||
ipv6.ICMPTypeNeighborAdvertisement, 0,
|
||||
&icmp.DefaultMessageBody{Data: response})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send it back
|
||||
return responsePacket, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
case ipv6.ICMPTypeNeighborAdvertisement:
|
||||
if datamac != nil {
|
||||
var addr address
|
||||
var mac macAddress
|
||||
copy(addr[:], ipv6Header.Src[:])
|
||||
copy(mac[:], (*datamac)[:])
|
||||
neighbor := i.peermacs[addr]
|
||||
neighbor.mac = mac
|
||||
neighbor.learned = true
|
||||
neighbor.lastadvertisement = time.Now()
|
||||
i.peermacs[addr] = neighbor
|
||||
}
|
||||
return nil, errors.New("No response needed")
|
||||
}
|
||||
|
||||
return nil, errors.New("ICMPv6 type not matched")
|
||||
@@ -238,6 +254,42 @@ func (i *icmpv6) create_icmpv6_tun(dst net.IP, src net.IP, mtype ipv6.ICMPType,
|
||||
return responsePacket, nil
|
||||
}
|
||||
|
||||
func (i *icmpv6) create_ndp_tap(dst address) ([]byte, error) {
|
||||
// Create the ND payload
|
||||
var payload [28]byte
|
||||
copy(payload[:4], []byte{0x00, 0x00, 0x00, 0x00})
|
||||
copy(payload[4:20], dst[:])
|
||||
copy(payload[20:22], []byte{0x01, 0x01})
|
||||
copy(payload[22:28], i.mymac[:6])
|
||||
|
||||
// Create the ICMPv6 solicited-node address
|
||||
var dstaddr address
|
||||
copy(dstaddr[:13], []byte{
|
||||
0xFF, 0x02, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x01, 0xFF})
|
||||
copy(dstaddr[13:], dst[13:16])
|
||||
|
||||
// Create the multicast MAC
|
||||
var dstmac macAddress
|
||||
copy(dstmac[:2], []byte{0x33, 0x33})
|
||||
copy(dstmac[2:6], dstaddr[12:16])
|
||||
|
||||
// Create the ND request
|
||||
requestPacket, err := i.create_icmpv6_tap(
|
||||
dstmac, dstaddr[:], i.mylladdr,
|
||||
ipv6.ICMPTypeNeighborSolicitation, 0,
|
||||
&icmp.DefaultMessageBody{Data: payload[:]})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
neighbor := i.peermacs[dstaddr]
|
||||
neighbor.lastsolicitation = time.Now()
|
||||
i.peermacs[dstaddr] = neighbor
|
||||
|
||||
return requestPacket, nil
|
||||
}
|
||||
|
||||
// Generates a response to an NDP discovery packet. This is effectively called
|
||||
// when the host operating system generates an NDP request for any address in
|
||||
// the fd00::/8 range, so that the operating system knows to route that traffic
|
||||
@@ -252,7 +304,7 @@ func (i *icmpv6) handle_ndp(in []byte) ([]byte, error) {
|
||||
case source.isValid():
|
||||
case snet.isValid():
|
||||
default:
|
||||
return nil, errors.New("Not an NDP for fd00::/8")
|
||||
return nil, errors.New("Not an NDP for 0200::/7")
|
||||
}
|
||||
|
||||
// Create our NDP message body response
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package yggdrasil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
@@ -35,7 +36,10 @@ func (m *multicast) start() error {
|
||||
return err
|
||||
}
|
||||
listenString := fmt.Sprintf("[::]:%v", addr.Port)
|
||||
conn, err := net.ListenPacket("udp6", listenString)
|
||||
lc := net.ListenConfig{
|
||||
Control: multicastReuse,
|
||||
}
|
||||
conn, err := lc.ListenPacket(context.Background(), "udp6", listenString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -153,6 +157,6 @@ func (m *multicast) listen() {
|
||||
}
|
||||
addr.Zone = from.Zone
|
||||
saddr := addr.String()
|
||||
m.core.tcp.connect(saddr)
|
||||
m.core.tcp.connect(saddr, "")
|
||||
}
|
||||
}
|
||||
|
9
src/yggdrasil/multicast_other.go
Normal file
9
src/yggdrasil/multicast_other.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build !linux,!darwin,!netbsd,!freebsd,!openbsd,!dragonflybsd,!windows
|
||||
|
||||
package yggdrasil
|
||||
|
||||
import "syscall"
|
||||
|
||||
func multicastReuse(network string, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
22
src/yggdrasil/multicast_unix.go
Normal file
22
src/yggdrasil/multicast_unix.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// +build linux darwin netbsd freebsd openbsd dragonflybsd
|
||||
|
||||
package yggdrasil
|
||||
|
||||
import "syscall"
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func multicastReuse(network string, address string, c syscall.RawConn) error {
|
||||
var control error
|
||||
var reuseport error
|
||||
|
||||
control = c.Control(func(fd uintptr) {
|
||||
reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
})
|
||||
|
||||
switch {
|
||||
case reuseport != nil:
|
||||
return reuseport
|
||||
default:
|
||||
return control
|
||||
}
|
||||
}
|
22
src/yggdrasil/multicast_windows.go
Normal file
22
src/yggdrasil/multicast_windows.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// +build windows
|
||||
|
||||
package yggdrasil
|
||||
|
||||
import "syscall"
|
||||
import "golang.org/x/sys/windows"
|
||||
|
||||
func multicastReuse(network string, address string, c syscall.RawConn) error {
|
||||
var control error
|
||||
var reuseaddr error
|
||||
|
||||
control = c.Control(func(fd uintptr) {
|
||||
reuseaddr = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
|
||||
})
|
||||
|
||||
switch {
|
||||
case reuseaddr != nil:
|
||||
return reuseaddr
|
||||
default:
|
||||
return control
|
||||
}
|
||||
}
|
@@ -17,7 +17,7 @@ import (
|
||||
type peers struct {
|
||||
core *Core
|
||||
mutex sync.Mutex // Synchronize writes to atomic
|
||||
ports atomic.Value //map[Port]*peer, use CoW semantics
|
||||
ports atomic.Value //map[switchPort]*peer, use CoW semantics
|
||||
authMutex sync.RWMutex
|
||||
allowedEncryptionPublicKeys map[boxPubKey]struct{}
|
||||
}
|
||||
@@ -74,43 +74,35 @@ func (ps *peers) putPorts(ports map[switchPort]*peer) {
|
||||
ps.ports.Store(ports)
|
||||
}
|
||||
|
||||
// Information known about a peer, including thier box/sig keys, precomputed shared keys (static and ephemeral), a handler for their outgoing traffic, and queue sizes for local backpressure.
|
||||
// Information known about a peer, including thier box/sig keys, precomputed shared keys (static and ephemeral) and a handler for their outgoing traffic
|
||||
type peer struct {
|
||||
queueSize int64 // used to track local backpressure
|
||||
bytesSent uint64 // To track bandwidth usage for getPeers
|
||||
bytesRecvd uint64 // To track bandwidth usage for getPeers
|
||||
// BUG: sync/atomic, 32 bit platforms need the above to be the first element
|
||||
core *Core
|
||||
port switchPort
|
||||
box boxPubKey
|
||||
sig sigPubKey
|
||||
shared boxSharedKey
|
||||
linkShared boxSharedKey
|
||||
firstSeen time.Time // To track uptime for getPeers
|
||||
linkOut (chan []byte) // used for protocol traffic (to bypass queues)
|
||||
doSend (chan struct{}) // tell the linkLoop to send a switchMsg
|
||||
dinfo *dhtInfo // used to keep the DHT working
|
||||
out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes
|
||||
close func() // Called when a peer is removed, to close the underlying connection, or via admin api
|
||||
}
|
||||
|
||||
// Size of the queue of packets to be sent to the node.
|
||||
func (p *peer) getQueueSize() int64 {
|
||||
return atomic.LoadInt64(&p.queueSize)
|
||||
}
|
||||
|
||||
// Used to increment or decrement the queue.
|
||||
func (p *peer) updateQueueSize(delta int64) {
|
||||
atomic.AddInt64(&p.queueSize, delta)
|
||||
core *Core
|
||||
port switchPort
|
||||
box boxPubKey
|
||||
sig sigPubKey
|
||||
shared boxSharedKey
|
||||
linkShared boxSharedKey
|
||||
endpoint string
|
||||
friendlyName string
|
||||
firstSeen time.Time // To track uptime for getPeers
|
||||
linkOut (chan []byte) // used for protocol traffic (to bypass queues)
|
||||
doSend (chan struct{}) // tell the linkLoop to send a switchMsg
|
||||
dinfo *dhtInfo // used to keep the DHT working
|
||||
out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes
|
||||
close func() // Called when a peer is removed, to close the underlying connection, or via admin api
|
||||
}
|
||||
|
||||
// Creates a new peer with the specified box, sig, and linkShared keys, using the lowest unocupied port number.
|
||||
func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey) *peer {
|
||||
func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey, endpoint string) *peer {
|
||||
now := time.Now()
|
||||
p := peer{box: *box,
|
||||
sig: *sig,
|
||||
shared: *getSharedKey(&ps.core.boxPriv, box),
|
||||
linkShared: *linkShared,
|
||||
endpoint: endpoint,
|
||||
firstSeen: now,
|
||||
doSend: make(chan struct{}, 1),
|
||||
core: ps.core}
|
||||
@@ -138,7 +130,7 @@ func (ps *peers) removePeer(port switchPort) {
|
||||
return
|
||||
} // Can't remove self peer
|
||||
ps.core.router.doAdmin(func() {
|
||||
ps.core.switchTable.unlockedRemovePeer(port)
|
||||
ps.core.switchTable.forgetPeer(port)
|
||||
})
|
||||
ps.mutex.Lock()
|
||||
oldPorts := ps.getPorts()
|
||||
@@ -183,7 +175,6 @@ func (p *peer) doSendSwitchMsgs() {
|
||||
// This must be launched in a separate goroutine by whatever sets up the peer struct.
|
||||
// It handles link protocol traffic.
|
||||
func (p *peer) linkLoop() {
|
||||
go p.doSendSwitchMsgs()
|
||||
tick := time.NewTicker(time.Second)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
@@ -194,8 +185,11 @@ func (p *peer) linkLoop() {
|
||||
}
|
||||
p.sendSwitchMsg()
|
||||
case _ = <-tick.C:
|
||||
if p.dinfo != nil {
|
||||
p.core.dht.peers <- p.dinfo
|
||||
//break // FIXME disabled the below completely to test something
|
||||
pdinfo := p.dinfo // FIXME this is a bad workarond NPE on the next line
|
||||
if pdinfo != nil {
|
||||
dinfo := *pdinfo
|
||||
p.core.dht.peers <- &dinfo
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,19 +223,7 @@ func (p *peer) handleTraffic(packet []byte, pTypeLen int) {
|
||||
// Drop traffic until the peer manages to send us at least one good switchMsg
|
||||
return
|
||||
}
|
||||
coords, coordLen := wire_decode_coords(packet[pTypeLen:])
|
||||
if coordLen >= len(packet) {
|
||||
return
|
||||
} // No payload
|
||||
toPort := p.core.switchTable.lookup(coords)
|
||||
if toPort == p.port {
|
||||
return
|
||||
}
|
||||
to := p.core.peers.getPorts()[toPort]
|
||||
if to == nil {
|
||||
return
|
||||
}
|
||||
to.sendPacket(packet)
|
||||
p.core.switchTable.packetIn <- packet
|
||||
}
|
||||
|
||||
// This just calls p.out(packet) for now.
|
||||
@@ -334,7 +316,7 @@ func (p *peer) handleSwitchMsg(packet []byte) {
|
||||
sigMsg.Hops = msg.Hops[:idx]
|
||||
loc.coords = append(loc.coords, hop.Port)
|
||||
bs := getBytesForSig(&hop.Next, &sigMsg)
|
||||
if !p.core.sigs.check(&prevKey, &hop.Sig, bs) {
|
||||
if !verify(&prevKey, bs, &hop.Sig) {
|
||||
p.core.peers.removePeer(p.port)
|
||||
}
|
||||
prevKey = hop.Next
|
||||
@@ -353,7 +335,7 @@ func (p *peer) handleSwitchMsg(packet []byte) {
|
||||
key: p.box,
|
||||
coords: loc.getCoords(),
|
||||
}
|
||||
p.core.dht.peers <- &dinfo
|
||||
//p.core.dht.peers <- &dinfo
|
||||
p.dinfo = &dinfo
|
||||
}
|
||||
|
||||
|
@@ -1,14 +0,0 @@
|
||||
// +build !debug
|
||||
|
||||
package yggdrasil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Starts the function profiler. This is only supported when built with
|
||||
// '-tags build'.
|
||||
func StartProfiler(_ *log.Logger) error {
|
||||
return errors.New("Release builds do not support -pprof, build using '-tags debug'")
|
||||
}
|
@@ -23,6 +23,7 @@ package yggdrasil
|
||||
// The router then runs some sanity checks before passing it to the tun
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
@@ -32,22 +33,32 @@ import (
|
||||
// The router struct has channels to/from the tun/tap device and a self peer (0), which is how messages are passed between this node and the peers/switch layer.
|
||||
// The router's mainLoop goroutine is responsible for managing all information related to the dht, searches, and crypto sessions.
|
||||
type router struct {
|
||||
core *Core
|
||||
addr address
|
||||
in <-chan []byte // packets we received from the network, link to peer's "out"
|
||||
out func([]byte) // packets we're sending to the network, link to peer's "in"
|
||||
recv chan<- []byte // place where the tun pulls received packets from
|
||||
send <-chan []byte // place where the tun puts outgoing packets
|
||||
reset chan struct{} // signal that coords changed (re-init sessions/dht)
|
||||
admin chan func() // pass a lambda for the admin socket to query stuff
|
||||
core *Core
|
||||
addr address
|
||||
subnet subnet
|
||||
in <-chan []byte // packets we received from the network, link to peer's "out"
|
||||
out func([]byte) // packets we're sending to the network, link to peer's "in"
|
||||
toRecv chan router_recvPacket // packets to handle via recvPacket()
|
||||
recv chan<- []byte // place where the tun pulls received packets from
|
||||
send <-chan []byte // place where the tun puts outgoing packets
|
||||
reset chan struct{} // signal that coords changed (re-init sessions/dht)
|
||||
admin chan func() // pass a lambda for the admin socket to query stuff
|
||||
cryptokey cryptokey
|
||||
}
|
||||
|
||||
// Packet and session info, used to check that the packet matches a valid IP range or CKR prefix before sending to the tun.
|
||||
type router_recvPacket struct {
|
||||
bs []byte
|
||||
sinfo *sessionInfo
|
||||
}
|
||||
|
||||
// Initializes the router struct, which includes setting up channels to/from the tun/tap.
|
||||
func (r *router) init(core *Core) {
|
||||
r.core = core
|
||||
r.addr = *address_addrForNodeID(&r.core.dht.nodeID)
|
||||
r.subnet = *address_subnetForNodeID(&r.core.dht.nodeID)
|
||||
in := make(chan []byte, 32) // TODO something better than this...
|
||||
p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{})
|
||||
p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{}, "(self)")
|
||||
p.out = func(packet []byte) {
|
||||
// This is to make very sure it never blocks
|
||||
select {
|
||||
@@ -59,6 +70,7 @@ func (r *router) init(core *Core) {
|
||||
}
|
||||
r.in = in
|
||||
r.out = func(packet []byte) { p.handlePacket(packet) } // The caller is responsible for go-ing if it needs to not block
|
||||
r.toRecv = make(chan router_recvPacket, 32)
|
||||
recv := make(chan []byte, 32)
|
||||
send := make(chan []byte, 32)
|
||||
r.recv = recv
|
||||
@@ -66,7 +78,8 @@ func (r *router) init(core *Core) {
|
||||
r.core.tun.recv = recv
|
||||
r.core.tun.send = send
|
||||
r.reset = make(chan struct{}, 1)
|
||||
r.admin = make(chan func())
|
||||
r.admin = make(chan func(), 32)
|
||||
r.cryptokey.init(r.core)
|
||||
// go r.mainLoop()
|
||||
}
|
||||
|
||||
@@ -86,13 +99,19 @@ func (r *router) mainLoop() {
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case rp := <-r.toRecv:
|
||||
r.recvPacket(rp.bs, rp.sinfo)
|
||||
case p := <-r.in:
|
||||
r.handleIn(p)
|
||||
case p := <-r.send:
|
||||
r.sendPacket(p)
|
||||
case info := <-r.core.dht.peers:
|
||||
r.core.dht.insertIfNew(info, false) // Insert as a normal node
|
||||
r.core.dht.insertIfNew(info, true) // Insert as a peer
|
||||
now := time.Now()
|
||||
oldInfo, isIn := r.core.dht.table[*info.getNodeID()]
|
||||
r.core.dht.insert(info)
|
||||
if isIn && now.Sub(oldInfo.recv) < 45*time.Second {
|
||||
info.recv = oldInfo.recv
|
||||
}
|
||||
case <-r.reset:
|
||||
r.core.sessions.resetInits()
|
||||
r.core.dht.reset()
|
||||
@@ -101,6 +120,7 @@ func (r *router) mainLoop() {
|
||||
// Any periodic maintenance stuff goes here
|
||||
r.core.switchTable.doMaintenance()
|
||||
r.core.dht.doMaintenance()
|
||||
r.core.sessions.cleanup()
|
||||
util_getBytes() // To slowly drain things
|
||||
}
|
||||
case f := <-r.admin:
|
||||
@@ -115,30 +135,82 @@ func (r *router) mainLoop() {
|
||||
// If the session hasn't responded recently, it triggers a ping or search to keep things alive or deal with broken coords *relatively* quickly.
|
||||
// It also deals with oversized packets if there are MTU issues by calling into icmpv6.go to spoof PacketTooBig traffic, or DestinationUnreachable if the other side has their tun/tap disabled.
|
||||
func (r *router) sendPacket(bs []byte) {
|
||||
if len(bs) < 40 {
|
||||
panic("Tried to send a packet shorter than a header...")
|
||||
}
|
||||
var sourceAddr address
|
||||
var sourceSubnet subnet
|
||||
copy(sourceAddr[:], bs[8:])
|
||||
copy(sourceSubnet[:], bs[8:])
|
||||
if !sourceAddr.isValid() && !sourceSubnet.isValid() {
|
||||
var destAddr address
|
||||
var destSnet subnet
|
||||
var destPubKey *boxPubKey
|
||||
var destNodeID *NodeID
|
||||
var addrlen int
|
||||
if bs[0]&0xf0 == 0x60 {
|
||||
// Check if we have a fully-sized header
|
||||
if len(bs) < 40 {
|
||||
panic("Tried to send a packet shorter than an IPv6 header...")
|
||||
}
|
||||
// IPv6 address
|
||||
addrlen = 16
|
||||
copy(sourceAddr[:addrlen], bs[8:])
|
||||
copy(destAddr[:addrlen], bs[24:])
|
||||
copy(destSnet[:addrlen/2], bs[24:])
|
||||
} else if bs[0]&0xf0 == 0x40 {
|
||||
// Check if we have a fully-sized header
|
||||
if len(bs) < 20 {
|
||||
panic("Tried to send a packet shorter than an IPv4 header...")
|
||||
}
|
||||
// IPv4 address
|
||||
addrlen = 4
|
||||
copy(sourceAddr[:addrlen], bs[12:])
|
||||
copy(destAddr[:addrlen], bs[16:])
|
||||
} else {
|
||||
// Unknown address length
|
||||
return
|
||||
}
|
||||
var dest address
|
||||
copy(dest[:], bs[24:])
|
||||
var snet subnet
|
||||
copy(snet[:], bs[24:])
|
||||
if !dest.isValid() && !snet.isValid() {
|
||||
if !r.cryptokey.isValidSource(sourceAddr, addrlen) {
|
||||
// The packet had a source address that doesn't belong to us or our
|
||||
// configured crypto-key routing source subnets
|
||||
return
|
||||
}
|
||||
if !destAddr.isValid() && !destSnet.isValid() {
|
||||
// The addresses didn't match valid Yggdrasil node addresses so let's see
|
||||
// whether it matches a crypto-key routing range instead
|
||||
if key, err := r.cryptokey.getPublicKeyForAddress(destAddr, addrlen); err == nil {
|
||||
// A public key was found, get the node ID for the search
|
||||
destPubKey = &key
|
||||
destNodeID = getNodeID(destPubKey)
|
||||
// Do a quick check to ensure that the node ID refers to a vaild Yggdrasil
|
||||
// address or subnet - this might be superfluous
|
||||
addr := *address_addrForNodeID(destNodeID)
|
||||
copy(destAddr[:], addr[:])
|
||||
copy(destSnet[:], addr[:])
|
||||
if !destAddr.isValid() && !destSnet.isValid() {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// No public key was found in the CKR table so we've exhausted our options
|
||||
return
|
||||
}
|
||||
}
|
||||
doSearch := func(packet []byte) {
|
||||
var nodeID, mask *NodeID
|
||||
if dest.isValid() {
|
||||
nodeID, mask = dest.getNodeIDandMask()
|
||||
}
|
||||
if snet.isValid() {
|
||||
nodeID, mask = snet.getNodeIDandMask()
|
||||
switch {
|
||||
case destNodeID != nil:
|
||||
// We already know the full node ID, probably because it's from a CKR
|
||||
// route in which the public key is known ahead of time
|
||||
nodeID = destNodeID
|
||||
var m NodeID
|
||||
for i := range m {
|
||||
m[i] = 0xFF
|
||||
}
|
||||
mask = &m
|
||||
case destAddr.isValid():
|
||||
// We don't know the full node ID - try and use the address to generate
|
||||
// a truncated node ID
|
||||
nodeID, mask = destAddr.getNodeIDandMask()
|
||||
case destSnet.isValid():
|
||||
// We don't know the full node ID - try and use the subnet to generate
|
||||
// a truncated node ID
|
||||
nodeID, mask = destSnet.getNodeIDandMask()
|
||||
default:
|
||||
return
|
||||
}
|
||||
sinfo, isIn := r.core.searches.searches[*nodeID]
|
||||
if !isIn {
|
||||
@@ -151,11 +223,11 @@ func (r *router) sendPacket(bs []byte) {
|
||||
}
|
||||
var sinfo *sessionInfo
|
||||
var isIn bool
|
||||
if dest.isValid() {
|
||||
sinfo, isIn = r.core.sessions.getByTheirAddr(&dest)
|
||||
if destAddr.isValid() {
|
||||
sinfo, isIn = r.core.sessions.getByTheirAddr(&destAddr)
|
||||
}
|
||||
if snet.isValid() {
|
||||
sinfo, isIn = r.core.sessions.getByTheirSubnet(&snet)
|
||||
if destSnet.isValid() {
|
||||
sinfo, isIn = r.core.sessions.getByTheirSubnet(&destSnet)
|
||||
}
|
||||
switch {
|
||||
case !isIn || !sinfo.init:
|
||||
@@ -184,6 +256,14 @@ func (r *router) sendPacket(bs []byte) {
|
||||
}
|
||||
fallthrough // Also send the packet
|
||||
default:
|
||||
// If we know the public key ahead of time (i.e. a CKR route) then check
|
||||
// if the session perm pub key matches before we send the packet to it
|
||||
if destPubKey != nil {
|
||||
if !bytes.Equal((*destPubKey)[:], sinfo.theirPermPub[:]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Drop packets if the session MTU is 0 - this means that one or other
|
||||
// side probably has their TUN adapter disabled
|
||||
if sinfo.getMTU() == 0 {
|
||||
@@ -234,29 +314,55 @@ func (r *router) sendPacket(bs []byte) {
|
||||
// Don't continue - drop the packet
|
||||
return
|
||||
}
|
||||
|
||||
sinfo.send <- bs
|
||||
}
|
||||
}
|
||||
|
||||
// Called for incoming traffic by the session worker for that connection.
|
||||
// Checks that the IP address is correct (matches the session) and passes the packet to the tun/tap.
|
||||
func (r *router) recvPacket(bs []byte, theirAddr *address, theirSubnet *subnet) {
|
||||
func (r *router) recvPacket(bs []byte, sinfo *sessionInfo) {
|
||||
// Note: called directly by the session worker, not the router goroutine
|
||||
if len(bs) < 24 {
|
||||
util_putBytes(bs)
|
||||
return
|
||||
}
|
||||
var source address
|
||||
copy(source[:], bs[8:])
|
||||
var sourceAddr address
|
||||
var dest address
|
||||
var snet subnet
|
||||
copy(snet[:], bs[8:])
|
||||
switch {
|
||||
case source.isValid() && source == *theirAddr:
|
||||
case snet.isValid() && snet == *theirSubnet:
|
||||
default:
|
||||
var addrlen int
|
||||
if bs[0]&0xf0 == 0x60 {
|
||||
// IPv6 address
|
||||
addrlen = 16
|
||||
copy(sourceAddr[:addrlen], bs[8:])
|
||||
copy(dest[:addrlen], bs[24:])
|
||||
copy(snet[:addrlen/2], bs[8:])
|
||||
} else if bs[0]&0xf0 == 0x40 {
|
||||
// IPv4 address
|
||||
addrlen = 4
|
||||
copy(sourceAddr[:addrlen], bs[12:])
|
||||
copy(dest[:addrlen], bs[16:])
|
||||
} else {
|
||||
// Unknown address length
|
||||
return
|
||||
}
|
||||
// Check that the packet is destined for either our Yggdrasil address or
|
||||
// subnet, or that it matches one of the crypto-key routing source routes
|
||||
if !r.cryptokey.isValidSource(dest, addrlen) {
|
||||
util_putBytes(bs)
|
||||
return
|
||||
}
|
||||
// See whether the packet they sent should have originated from this session
|
||||
switch {
|
||||
case sourceAddr.isValid() && sourceAddr == sinfo.theirAddr:
|
||||
case snet.isValid() && snet == sinfo.theirSubnet:
|
||||
default:
|
||||
key, err := r.cryptokey.getPublicKeyForAddress(sourceAddr, addrlen)
|
||||
if err != nil || key != sinfo.theirPermPub {
|
||||
util_putBytes(bs)
|
||||
return
|
||||
}
|
||||
}
|
||||
//go func() { r.recv<-bs }()
|
||||
r.recv <- bs
|
||||
}
|
||||
|
@@ -11,6 +11,9 @@ package yggdrasil
|
||||
// A new search packet is sent immediately after receiving a response
|
||||
// A new search packet is sent periodically, once per second, in case a packet was dropped (this slowly causes the search to become parallel if the search doesn't timeout but also doesn't finish within 1 second for whatever reason)
|
||||
|
||||
// TODO?
|
||||
// Some kind of max search steps, in case the node is offline, so we don't crawl through too much of the network looking for a destination that isn't there?
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"time"
|
||||
@@ -88,11 +91,13 @@ func (s *searches) handleDHTRes(res *dhtRes) {
|
||||
func (s *searches) addToSearch(sinfo *searchInfo, res *dhtRes) {
|
||||
// Add responses to toVisit if closer to dest than the res node
|
||||
from := dhtInfo{key: res.Key, coords: res.Coords}
|
||||
sinfo.visited[*from.getNodeID()] = true
|
||||
for _, info := range res.Infos {
|
||||
if sinfo.visited[*info.getNodeID()] {
|
||||
if *info.getNodeID() == s.core.dht.nodeID || sinfo.visited[*info.getNodeID()] {
|
||||
continue
|
||||
}
|
||||
if dht_firstCloserThanThird(info.getNodeID(), &res.Dest, from.getNodeID()) {
|
||||
if dht_ordered(&sinfo.dest, info.getNodeID(), from.getNodeID()) {
|
||||
// Response is closer to the destination
|
||||
sinfo.toVisit = append(sinfo.toVisit, info)
|
||||
}
|
||||
}
|
||||
@@ -107,7 +112,8 @@ func (s *searches) addToSearch(sinfo *searchInfo, res *dhtRes) {
|
||||
}
|
||||
// Sort
|
||||
sort.SliceStable(sinfo.toVisit, func(i, j int) bool {
|
||||
return dht_firstCloserThanThird(sinfo.toVisit[i].getNodeID(), &res.Dest, sinfo.toVisit[j].getNodeID())
|
||||
// Should return true if i is closer to the destination than j
|
||||
return dht_ordered(&res.Dest, sinfo.toVisit[i].getNodeID(), sinfo.toVisit[j].getNodeID())
|
||||
})
|
||||
// Truncate to some maximum size
|
||||
if len(sinfo.toVisit) > search_MAX_SEARCH_SIZE {
|
||||
@@ -126,11 +132,9 @@ func (s *searches) doSearchStep(sinfo *searchInfo) {
|
||||
// Send to the next search target
|
||||
var next *dhtInfo
|
||||
next, sinfo.toVisit = sinfo.toVisit[0], sinfo.toVisit[1:]
|
||||
var oldPings int
|
||||
oldPings, next.pings = next.pings, 0
|
||||
rq := dhtReqKey{next.key, sinfo.dest}
|
||||
s.core.dht.addCallback(&rq, s.handleDHTRes)
|
||||
s.core.dht.ping(next, &sinfo.dest)
|
||||
next.pings = oldPings // Don't evict a node for searching with it too much
|
||||
sinfo.visited[*next.getNodeID()] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +188,10 @@ func (s *searches) checkDHTRes(info *searchInfo, res *dhtRes) bool {
|
||||
sinfo, isIn := s.core.sessions.getByTheirPerm(&res.Key)
|
||||
if !isIn {
|
||||
sinfo = s.core.sessions.createSession(&res.Key)
|
||||
if sinfo == nil {
|
||||
// nil if the DHT search finished but the session wasn't allowed
|
||||
return true
|
||||
}
|
||||
_, isIn := s.core.sessions.getByTheirPerm(&res.Key)
|
||||
if !isIn {
|
||||
panic("This should never happen")
|
||||
|
@@ -4,7 +4,11 @@ package yggdrasil
|
||||
// It's responsible for keeping track of open sessions to other nodes
|
||||
// The session information consists of crypto keys and coords
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
)
|
||||
|
||||
// All the information we know about an active session.
|
||||
// This includes coords, permanent and ephemeral keys, handles and nonces, various sorts of timing information for timeout and maintenance, and some metadata for the admin API.
|
||||
@@ -72,7 +76,10 @@ func (s *sessionInfo) update(p *sessionPing) bool {
|
||||
if p.MTU >= 1280 || p.MTU == 0 {
|
||||
s.theirMTU = p.MTU
|
||||
}
|
||||
s.coords = append([]byte{}, p.Coords...)
|
||||
if !bytes.Equal(s.coords, p.Coords) {
|
||||
// allocate enough space for additional coords
|
||||
s.coords = append(make([]byte, 0, len(p.Coords)+11), p.Coords...)
|
||||
}
|
||||
now := time.Now()
|
||||
s.time = now
|
||||
s.tstamp = p.Tstamp
|
||||
@@ -89,7 +96,8 @@ func (s *sessionInfo) timedout() bool {
|
||||
// Sessions are indexed by handle.
|
||||
// Additionally, stores maps of address/subnet onto keys, and keys onto handles.
|
||||
type sessions struct {
|
||||
core *Core
|
||||
core *Core
|
||||
lastCleanup time.Time
|
||||
// Maps known permanent keys to their shared key, used by DHT a lot
|
||||
permShared map[boxPubKey]*boxSharedKey
|
||||
// Maps (secret) handle onto session info
|
||||
@@ -100,6 +108,13 @@ type sessions struct {
|
||||
byTheirPerm map[boxPubKey]*handle
|
||||
addrToPerm map[address]*boxPubKey
|
||||
subnetToPerm map[subnet]*boxPubKey
|
||||
// Options from the session firewall
|
||||
sessionFirewallEnabled bool
|
||||
sessionFirewallAllowsDirect bool
|
||||
sessionFirewallAllowsRemote bool
|
||||
sessionFirewallAlwaysAllowsOutbound bool
|
||||
sessionFirewallWhitelist []string
|
||||
sessionFirewallBlacklist []string
|
||||
}
|
||||
|
||||
// Initializes the session struct.
|
||||
@@ -111,6 +126,85 @@ func (ss *sessions) init(core *Core) {
|
||||
ss.byTheirPerm = make(map[boxPubKey]*handle)
|
||||
ss.addrToPerm = make(map[address]*boxPubKey)
|
||||
ss.subnetToPerm = make(map[subnet]*boxPubKey)
|
||||
ss.lastCleanup = time.Now()
|
||||
}
|
||||
|
||||
// Enable or disable the session firewall
|
||||
func (ss *sessions) setSessionFirewallState(enabled bool) {
|
||||
ss.sessionFirewallEnabled = enabled
|
||||
}
|
||||
|
||||
// Set the session firewall defaults (first parameter is whether to allow
|
||||
// sessions from direct peers, second is whether to allow from remote nodes).
|
||||
func (ss *sessions) setSessionFirewallDefaults(allowsDirect bool, allowsRemote bool, alwaysAllowsOutbound bool) {
|
||||
ss.sessionFirewallAllowsDirect = allowsDirect
|
||||
ss.sessionFirewallAllowsRemote = allowsRemote
|
||||
ss.sessionFirewallAlwaysAllowsOutbound = alwaysAllowsOutbound
|
||||
}
|
||||
|
||||
// Set the session firewall whitelist - nodes always allowed to open sessions.
|
||||
func (ss *sessions) setSessionFirewallWhitelist(whitelist []string) {
|
||||
ss.sessionFirewallWhitelist = whitelist
|
||||
}
|
||||
|
||||
// Set the session firewall blacklist - nodes never allowed to open sessions.
|
||||
func (ss *sessions) setSessionFirewallBlacklist(blacklist []string) {
|
||||
ss.sessionFirewallBlacklist = blacklist
|
||||
}
|
||||
|
||||
// Determines whether the session with a given publickey is allowed based on
|
||||
// session firewall rules.
|
||||
func (ss *sessions) isSessionAllowed(pubkey *boxPubKey, initiator bool) bool {
|
||||
// Allow by default if the session firewall is disabled
|
||||
if !ss.sessionFirewallEnabled {
|
||||
return true
|
||||
}
|
||||
// Prepare for checking whitelist/blacklist
|
||||
var box boxPubKey
|
||||
// Reject blacklisted nodes
|
||||
for _, b := range ss.sessionFirewallBlacklist {
|
||||
key, err := hex.DecodeString(b)
|
||||
if err == nil {
|
||||
copy(box[:boxPubKeyLen], key)
|
||||
if box == *pubkey {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
// Allow whitelisted nodes
|
||||
for _, b := range ss.sessionFirewallWhitelist {
|
||||
key, err := hex.DecodeString(b)
|
||||
if err == nil {
|
||||
copy(box[:boxPubKeyLen], key)
|
||||
if box == *pubkey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Allow outbound sessions if appropriate
|
||||
if ss.sessionFirewallAlwaysAllowsOutbound {
|
||||
if initiator {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Look and see if the pubkey is that of a direct peer
|
||||
var isDirectPeer bool
|
||||
for _, peer := range ss.core.peers.ports.Load().(map[switchPort]*peer) {
|
||||
if peer.box == *pubkey {
|
||||
isDirectPeer = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Allow direct peers if appropriate
|
||||
if ss.sessionFirewallAllowsDirect && isDirectPeer {
|
||||
return true
|
||||
}
|
||||
// Allow remote nodes if appropriate
|
||||
if ss.sessionFirewallAllowsRemote && !isDirectPeer {
|
||||
return true
|
||||
}
|
||||
// Finally, default-deny if not matching any of the above rules
|
||||
return false
|
||||
}
|
||||
|
||||
// Gets the session corresponding to a given handle.
|
||||
@@ -166,6 +260,11 @@ func (ss *sessions) getByTheirSubnet(snet *subnet) (*sessionInfo, bool) {
|
||||
// Creates a new session and lazily cleans up old/timedout existing sessions.
|
||||
// This includse initializing session info to sane defaults (e.g. lowest supported MTU).
|
||||
func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo {
|
||||
if ss.sessionFirewallEnabled {
|
||||
if !ss.isSessionAllowed(theirPermKey, true) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
sinfo := sessionInfo{}
|
||||
sinfo.core = ss.core
|
||||
sinfo.theirPermPub = *theirPermKey
|
||||
@@ -202,13 +301,6 @@ func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo {
|
||||
sinfo.send = make(chan []byte, 32)
|
||||
sinfo.recv = make(chan *wire_trafficPacket, 32)
|
||||
go sinfo.doWorker()
|
||||
// Do some cleanup
|
||||
// Time thresholds almost certainly could use some adjusting
|
||||
for _, s := range ss.sinfos {
|
||||
if s.timedout() {
|
||||
s.close()
|
||||
}
|
||||
}
|
||||
ss.sinfos[sinfo.myHandle] = &sinfo
|
||||
ss.byMySes[sinfo.mySesPub] = &sinfo.myHandle
|
||||
ss.byTheirPerm[sinfo.theirPermPub] = &sinfo.myHandle
|
||||
@@ -217,6 +309,54 @@ func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo {
|
||||
return &sinfo
|
||||
}
|
||||
|
||||
func (ss *sessions) cleanup() {
|
||||
// Time thresholds almost certainly could use some adjusting
|
||||
for k := range ss.permShared {
|
||||
// Delete a key, to make sure this eventually shrinks to 0
|
||||
delete(ss.permShared, k)
|
||||
break
|
||||
}
|
||||
if time.Since(ss.lastCleanup) < time.Minute {
|
||||
return
|
||||
}
|
||||
for _, s := range ss.sinfos {
|
||||
if s.timedout() {
|
||||
s.close()
|
||||
}
|
||||
}
|
||||
permShared := make(map[boxPubKey]*boxSharedKey, len(ss.permShared))
|
||||
for k, v := range ss.permShared {
|
||||
permShared[k] = v
|
||||
}
|
||||
ss.permShared = permShared
|
||||
sinfos := make(map[handle]*sessionInfo, len(ss.sinfos))
|
||||
for k, v := range ss.sinfos {
|
||||
sinfos[k] = v
|
||||
}
|
||||
ss.sinfos = sinfos
|
||||
byMySes := make(map[boxPubKey]*handle, len(ss.byMySes))
|
||||
for k, v := range ss.byMySes {
|
||||
byMySes[k] = v
|
||||
}
|
||||
ss.byMySes = byMySes
|
||||
byTheirPerm := make(map[boxPubKey]*handle, len(ss.byTheirPerm))
|
||||
for k, v := range ss.byTheirPerm {
|
||||
byTheirPerm[k] = v
|
||||
}
|
||||
ss.byTheirPerm = byTheirPerm
|
||||
addrToPerm := make(map[address]*boxPubKey, len(ss.addrToPerm))
|
||||
for k, v := range ss.addrToPerm {
|
||||
addrToPerm[k] = v
|
||||
}
|
||||
ss.addrToPerm = addrToPerm
|
||||
subnetToPerm := make(map[subnet]*boxPubKey, len(ss.subnetToPerm))
|
||||
for k, v := range ss.subnetToPerm {
|
||||
subnetToPerm[k] = v
|
||||
}
|
||||
ss.subnetToPerm = subnetToPerm
|
||||
ss.lastCleanup = time.Now()
|
||||
}
|
||||
|
||||
// Closes a session, removing it from sessions maps and killing the worker goroutine.
|
||||
func (sinfo *sessionInfo) close() {
|
||||
delete(sinfo.core.sessions.sinfos, sinfo.myHandle)
|
||||
@@ -253,7 +393,7 @@ func (ss *sessions) getSharedKey(myPriv *boxPrivKey,
|
||||
return skey
|
||||
}
|
||||
// First do some cleanup
|
||||
const maxKeys = dht_bucket_number * dht_bucket_size
|
||||
const maxKeys = 1024
|
||||
for key := range ss.permShared {
|
||||
// Remove a random key until the store is small enough
|
||||
if len(ss.permShared) < maxKeys {
|
||||
@@ -297,6 +437,12 @@ func (ss *sessions) sendPingPong(sinfo *sessionInfo, isPong bool) {
|
||||
func (ss *sessions) handlePing(ping *sessionPing) {
|
||||
// Get the corresponding session (or create a new session)
|
||||
sinfo, isIn := ss.getByTheirPerm(&ping.SendPermPub)
|
||||
// Check the session firewall
|
||||
if !isIn && ss.sessionFirewallEnabled {
|
||||
if !ss.isSessionAllowed(&ping.SendPermPub, false) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !isIn || sinfo.timedout() {
|
||||
if isIn {
|
||||
sinfo.close()
|
||||
@@ -415,12 +561,42 @@ func (sinfo *sessionInfo) doWorker() {
|
||||
func (sinfo *sessionInfo) doSend(bs []byte) {
|
||||
defer util_putBytes(bs)
|
||||
if !sinfo.init {
|
||||
// To prevent using empty session keys
|
||||
return
|
||||
} // To prevent using empty session keys
|
||||
}
|
||||
// code isn't multithreaded so appending to this is safe
|
||||
coords := sinfo.coords
|
||||
// Read IPv6 flowlabel field (20 bits).
|
||||
// Assumes packet at least contains IPv6 header.
|
||||
flowkey := uint64(bs[1]&0x0f)<<16 | uint64(bs[2])<<8 | uint64(bs[3])
|
||||
// Check if the flowlabel was specified
|
||||
if flowkey == 0 {
|
||||
// Does the packet meet the minimum UDP packet size? (others are bigger)
|
||||
if len(bs) >= 48 {
|
||||
// Is the protocol TCP, UDP, SCTP?
|
||||
if bs[6] == 0x06 || bs[6] == 0x11 || bs[6] == 0x84 {
|
||||
// if flowlabel was unspecified (0), try to use known protocols' ports
|
||||
// protokey: proto | sport | dport
|
||||
flowkey = uint64(bs[6])<<32 /* proto */ |
|
||||
uint64(bs[40])<<24 | uint64(bs[41])<<16 /* sport */ |
|
||||
uint64(bs[42])<<8 | uint64(bs[43]) /* dport */
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we have a flowkey, either through the IPv6 flowlabel field or through
|
||||
// known TCP/UDP/SCTP proto-sport-dport triplet, then append it to the coords.
|
||||
// Appending extra coords after a 0 ensures that we still target the local router
|
||||
// but lets us send extra data (which is otherwise ignored) to help separate
|
||||
// traffic streams into independent queues
|
||||
if flowkey != 0 {
|
||||
coords = append(coords, 0) // First target the local switchport
|
||||
coords = wire_put_uint64(flowkey, coords) // Then variable-length encoded flowkey
|
||||
}
|
||||
// Prepare the payload
|
||||
payload, nonce := boxSeal(&sinfo.sharedSesKey, bs, &sinfo.myNonce)
|
||||
defer util_putBytes(payload)
|
||||
p := wire_trafficPacket{
|
||||
Coords: sinfo.coords,
|
||||
Coords: coords,
|
||||
Handle: sinfo.theirHandle,
|
||||
Nonce: *nonce,
|
||||
Payload: payload,
|
||||
@@ -437,43 +613,16 @@ func (sinfo *sessionInfo) doSend(bs []byte) {
|
||||
// TODO? remove the MTU updating part? That should never happen with TCP peers, and the old UDP code that caused it was removed (and if replaced, should be replaced with something that can reliably send messages with an arbitrary size).
|
||||
func (sinfo *sessionInfo) doRecv(p *wire_trafficPacket) {
|
||||
defer util_putBytes(p.Payload)
|
||||
payloadSize := uint16(len(p.Payload))
|
||||
if !sinfo.nonceIsOK(&p.Nonce) {
|
||||
return
|
||||
}
|
||||
bs, isOK := boxOpen(&sinfo.sharedSesKey, p.Payload, &p.Nonce)
|
||||
if !isOK {
|
||||
// We're going to guess that the session MTU is too large
|
||||
// Set myMTU to the largest value we think we can receive
|
||||
fixSessionMTU := func() {
|
||||
// This clamps down to 1280 almost immediately over ipv4
|
||||
// Over link-local ipv6, it seems to approach link MTU
|
||||
// So maybe it's doing the right thing?...
|
||||
//sinfo.core.log.Println("DEBUG got bad packet:", payloadSize)
|
||||
newMTU := payloadSize - boxOverhead
|
||||
if newMTU < 1280 {
|
||||
newMTU = 1280
|
||||
}
|
||||
if newMTU < sinfo.myMTU {
|
||||
sinfo.myMTU = newMTU
|
||||
sinfo.core.sessions.sendPingPong(sinfo, false)
|
||||
sinfo.mtuTime = time.Now()
|
||||
sinfo.wasMTUFixed = true
|
||||
}
|
||||
}
|
||||
go func() { sinfo.core.router.admin <- fixSessionMTU }()
|
||||
util_putBytes(bs)
|
||||
return
|
||||
}
|
||||
fixSessionMTU := func() {
|
||||
if time.Since(sinfo.mtuTime) > time.Minute {
|
||||
sinfo.myMTU = uint16(sinfo.core.tun.mtu)
|
||||
sinfo.mtuTime = time.Now()
|
||||
}
|
||||
}
|
||||
go func() { sinfo.core.router.admin <- fixSessionMTU }()
|
||||
sinfo.updateNonce(&p.Nonce)
|
||||
sinfo.time = time.Now()
|
||||
sinfo.bytesRecvd += uint64(len(bs))
|
||||
sinfo.core.router.recvPacket(bs, &sinfo.theirAddr, &sinfo.theirSubnet)
|
||||
sinfo.core.router.toRecv <- router_recvPacket{bs, sinfo}
|
||||
}
|
||||
|
@@ -1,86 +0,0 @@
|
||||
package yggdrasil
|
||||
|
||||
// This is where we record which signatures we've previously checked
|
||||
// It's so we can avoid needlessly checking them again
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This keeps track of what signatures have already been checked.
|
||||
// It's used to skip expensive crypto operations, given that many signatures are likely to be the same for the average node's peers.
|
||||
type sigManager struct {
|
||||
mutex sync.RWMutex
|
||||
checked map[sigBytes]knownSig
|
||||
lastCleaned time.Time
|
||||
}
|
||||
|
||||
// Represents a known signature.
|
||||
// Includes the key, the signature bytes, the bytes that were signed, and the time it was last used.
|
||||
type knownSig struct {
|
||||
key sigPubKey
|
||||
sig sigBytes
|
||||
bs []byte
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// Initializes the signature manager.
|
||||
func (m *sigManager) init() {
|
||||
m.checked = make(map[sigBytes]knownSig)
|
||||
}
|
||||
|
||||
// Checks if a key and signature match the supplied bytes.
|
||||
// If the same key/sig/bytes have been checked before, it returns true from the cached results.
|
||||
// If not, it checks the key, updates it in the cache if successful, and returns the checked results.
|
||||
func (m *sigManager) check(key *sigPubKey, sig *sigBytes, bs []byte) bool {
|
||||
if m.isChecked(key, sig, bs) {
|
||||
return true
|
||||
}
|
||||
verified := verify(key, bs, sig)
|
||||
if verified {
|
||||
m.putChecked(key, sig, bs)
|
||||
}
|
||||
return verified
|
||||
}
|
||||
|
||||
// Checks the cache to see if this key/sig/bytes combination has already been verified.
|
||||
// Returns true if it finds a match.
|
||||
func (m *sigManager) isChecked(key *sigPubKey, sig *sigBytes, bs []byte) bool {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
k, isIn := m.checked[*sig]
|
||||
if !isIn {
|
||||
return false
|
||||
}
|
||||
if k.key != *key || k.sig != *sig || len(bs) != len(k.bs) {
|
||||
return false
|
||||
}
|
||||
for idx := 0; idx < len(bs); idx++ {
|
||||
if bs[idx] != k.bs[idx] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
k.time = time.Now()
|
||||
return true
|
||||
}
|
||||
|
||||
// Puts a new result into the cache.
|
||||
// This result is then used by isChecked to skip the expensive crypto verification if it's needed again.
|
||||
// This is useful because, for nodes with multiple peers, there is often a lot of overlap between the signatures provided by each peer.
|
||||
func (m *sigManager) putChecked(key *sigPubKey, newsig *sigBytes, bs []byte) {
|
||||
m.mutex.Lock()
|
||||
defer m.mutex.Unlock()
|
||||
now := time.Now()
|
||||
if time.Since(m.lastCleaned) > 60*time.Second {
|
||||
// Since we have the write lock anyway, do some cleanup
|
||||
for s, k := range m.checked {
|
||||
if time.Since(k.time) > 60*time.Second {
|
||||
delete(m.checked, s)
|
||||
}
|
||||
}
|
||||
m.lastCleaned = now
|
||||
}
|
||||
k := knownSig{key: *key, sig: *newsig, bs: bs, time: now}
|
||||
m.checked[*newsig] = k
|
||||
}
|
@@ -12,15 +12,18 @@ package yggdrasil
|
||||
// A little annoying to do with constant changes from backpressure
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const switch_timeout = time.Minute
|
||||
const switch_updateInterval = switch_timeout / 2
|
||||
const switch_throttle = switch_updateInterval / 2
|
||||
const (
|
||||
switch_timeout = time.Minute
|
||||
switch_updateInterval = switch_timeout / 2
|
||||
switch_throttle = switch_updateInterval / 2
|
||||
switch_faster_threshold = 240 //Number of switch updates before switching to a faster parent
|
||||
)
|
||||
|
||||
// The switch locator represents the topology and network state dependent info about a node, minus the signatures that go with it.
|
||||
// Nodes will pick the best root they see, provided that the root continues to push out updates with new timestamps.
|
||||
@@ -118,13 +121,13 @@ func (x *switchLocator) isAncestorOf(y *switchLocator) bool {
|
||||
|
||||
// Information about a peer, used by the switch to build the tree and eventually make routing decisions.
|
||||
type peerInfo struct {
|
||||
key sigPubKey // ID of this peer
|
||||
locator switchLocator // Should be able to respond with signatures upon request
|
||||
degree uint64 // Self-reported degree
|
||||
time time.Time // Time this node was last seen
|
||||
firstSeen time.Time
|
||||
port switchPort // Interface number of this peer
|
||||
msg switchMsg // The wire switchMsg used
|
||||
key sigPubKey // ID of this peer
|
||||
locator switchLocator // Should be able to respond with signatures upon request
|
||||
degree uint64 // Self-reported degree
|
||||
time time.Time // Time this node was last seen
|
||||
faster map[switchPort]uint64 // Counter of how often a node is faster than the current parent, penalized extra if slower
|
||||
port switchPort // Interface number of this peer
|
||||
msg switchMsg // The wire switchMsg used
|
||||
}
|
||||
|
||||
// This is just a uint64 with a named type for clarity reasons.
|
||||
@@ -139,7 +142,7 @@ type tableElem struct {
|
||||
// This is the subset of the information about all peers needed to make routing decisions, and it stored separately in an atomically accessed table, which gets hammered in the "hot loop" of the routing logic (see: peer.handleTraffic in peers.go).
|
||||
type lookupTable struct {
|
||||
self switchLocator
|
||||
elems []tableElem
|
||||
elems map[switchPort]tableElem
|
||||
}
|
||||
|
||||
// This is switch information which is mutable and needs to be modified by other goroutines, but is not accessed atomically.
|
||||
@@ -155,17 +158,25 @@ type switchData struct {
|
||||
|
||||
// All the information stored by the switch.
|
||||
type switchTable struct {
|
||||
core *Core
|
||||
key sigPubKey // Our own key
|
||||
time time.Time // Time when locator.tstamp was last updated
|
||||
parent switchPort // Port of whatever peer is our parent, or self if we're root
|
||||
drop map[sigPubKey]int64 // Tstamp associated with a dropped root
|
||||
mutex sync.RWMutex // Lock for reads/writes of switchData
|
||||
data switchData
|
||||
updater atomic.Value //*sync.Once
|
||||
table atomic.Value //lookupTable
|
||||
core *Core
|
||||
key sigPubKey // Our own key
|
||||
time time.Time // Time when locator.tstamp was last updated
|
||||
drop map[sigPubKey]int64 // Tstamp associated with a dropped root
|
||||
mutex sync.RWMutex // Lock for reads/writes of switchData
|
||||
parent switchPort // Port of whatever peer is our parent, or self if we're root
|
||||
data switchData //
|
||||
updater atomic.Value // *sync.Once
|
||||
table atomic.Value // lookupTable
|
||||
packetIn chan []byte // Incoming packets for the worker to handle
|
||||
idleIn chan switchPort // Incoming idle notifications from peer links
|
||||
admin chan func() // Pass a lambda for the admin socket to query stuff
|
||||
queues switch_buffers // Queues - not atomic so ONLY use through admin chan
|
||||
queueTotalMaxSize uint64 // Maximum combined size of queues
|
||||
}
|
||||
|
||||
// Minimum allowed total size of switch queues.
|
||||
const SwitchQueueTotalMinSize = 4 * 1024 * 1024
|
||||
|
||||
// Initializes the switchTable struct.
|
||||
func (t *switchTable) init(core *Core, key sigPubKey) {
|
||||
now := time.Now()
|
||||
@@ -177,6 +188,10 @@ func (t *switchTable) init(core *Core, key sigPubKey) {
|
||||
t.updater.Store(&sync.Once{})
|
||||
t.table.Store(lookupTable{})
|
||||
t.drop = make(map[sigPubKey]int64)
|
||||
t.packetIn = make(chan []byte, 1024)
|
||||
t.idleIn = make(chan switchPort, 1024)
|
||||
t.admin = make(chan func())
|
||||
t.queueTotalMaxSize = SwitchQueueTotalMinSize
|
||||
}
|
||||
|
||||
// Safely gets a copy of this node's locator.
|
||||
@@ -193,6 +208,7 @@ func (t *switchTable) doMaintenance() {
|
||||
defer t.mutex.Unlock() // Release lock when we're done
|
||||
t.cleanRoot()
|
||||
t.cleanDropped()
|
||||
t.cleanPeers()
|
||||
}
|
||||
|
||||
// Updates the root periodically if it is ourself, or promotes ourself to root if we're better than the current root or if the current root has timed out.
|
||||
@@ -235,14 +251,38 @@ func (t *switchTable) cleanRoot() {
|
||||
// Removes a peer.
|
||||
// Must be called by the router mainLoop goroutine, e.g. call router.doAdmin with a lambda that calls this.
|
||||
// If the removed peer was this node's parent, it immediately tries to find a new parent.
|
||||
func (t *switchTable) unlockedRemovePeer(port switchPort) {
|
||||
func (t *switchTable) forgetPeer(port switchPort) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
delete(t.data.peers, port)
|
||||
t.updater.Store(&sync.Once{})
|
||||
if port != t.parent {
|
||||
return
|
||||
}
|
||||
t.parent = 0
|
||||
for _, info := range t.data.peers {
|
||||
t.unlockedHandleMsg(&info.msg, info.port)
|
||||
t.unlockedHandleMsg(&info.msg, info.port, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean all unresponsive peers from the table, needed in case a peer stops updating.
|
||||
// Needed in case a non-parent peer keeps the connection open but stops sending updates.
|
||||
// Also reclaims space from deleted peers by copying the map.
|
||||
func (t *switchTable) cleanPeers() {
|
||||
now := time.Now()
|
||||
for port, peer := range t.data.peers {
|
||||
if now.Sub(peer.time) > switch_timeout+switch_throttle {
|
||||
// Longer than switch_timeout to make sure we don't remove a working peer because the root stopped responding.
|
||||
delete(t.data.peers, port)
|
||||
}
|
||||
}
|
||||
if _, isIn := t.data.peers[t.parent]; !isIn {
|
||||
// The root timestamp would probably time out before this happens, but better safe than sorry.
|
||||
// We removed the current parent, so find a new one.
|
||||
t.parent = 0
|
||||
for _, peer := range t.data.peers {
|
||||
t.unlockedHandleMsg(&peer.msg, peer.port, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +356,7 @@ func (t *switchTable) checkRoot(msg *switchMsg) bool {
|
||||
func (t *switchTable) handleMsg(msg *switchMsg, fromPort switchPort) {
|
||||
t.mutex.Lock()
|
||||
defer t.mutex.Unlock()
|
||||
t.unlockedHandleMsg(msg, fromPort)
|
||||
t.unlockedHandleMsg(msg, fromPort, false)
|
||||
}
|
||||
|
||||
// This updates the switch with information about a peer.
|
||||
@@ -324,7 +364,8 @@ func (t *switchTable) handleMsg(msg *switchMsg, fromPort switchPort) {
|
||||
// That happens if this node is already our parent, or is advertising a better root, or is advertising a better path to the same root, etc...
|
||||
// There are a lot of very delicate order sensitive checks here, so its' best to just read the code if you need to understand what it's doing.
|
||||
// It's very important to not change the order of the statements in the case function unless you're absolutely sure that it's safe, including safe if used along side nodes that used the previous order.
|
||||
func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
|
||||
// Set the third arg to true if you're reprocessing an old message, e.g. to find a new parent after one disconnects, to avoid updating some timing related things.
|
||||
func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort, reprocessing bool) {
|
||||
// TODO directly use a switchMsg instead of switchMessage + sigs
|
||||
now := time.Now()
|
||||
// Set up the sender peerInfo
|
||||
@@ -339,11 +380,6 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
|
||||
prevKey = hop.Next
|
||||
}
|
||||
sender.msg = *msg
|
||||
oldSender, isIn := t.data.peers[fromPort]
|
||||
if !isIn {
|
||||
oldSender.firstSeen = now
|
||||
}
|
||||
sender.firstSeen = oldSender.firstSeen
|
||||
sender.port = fromPort
|
||||
sender.time = now
|
||||
// Decide what to do
|
||||
@@ -362,11 +398,40 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
|
||||
return true
|
||||
}
|
||||
doUpdate := false
|
||||
oldSender := t.data.peers[fromPort]
|
||||
if !equiv(&sender.locator, &oldSender.locator) {
|
||||
// Reset faster info, we'll start refilling it right after this
|
||||
sender.faster = nil
|
||||
doUpdate = true
|
||||
//sender.firstSeen = now // TODO? uncomment to prevent flapping?
|
||||
}
|
||||
// Update the matrix of peer "faster" thresholds
|
||||
if reprocessing {
|
||||
sender.faster = oldSender.faster
|
||||
} else {
|
||||
sender.faster = make(map[switchPort]uint64, len(oldSender.faster))
|
||||
for port, peer := range t.data.peers {
|
||||
if port == fromPort {
|
||||
continue
|
||||
} else if sender.locator.root != peer.locator.root || sender.locator.tstamp > peer.locator.tstamp {
|
||||
// We were faster than this node, so increment, as long as we don't overflow because of it
|
||||
if oldSender.faster[peer.port] < switch_faster_threshold {
|
||||
sender.faster[port] = oldSender.faster[peer.port] + 1
|
||||
} else {
|
||||
sender.faster[port] = switch_faster_threshold
|
||||
}
|
||||
} else {
|
||||
// Slower than this node, penalize (more than the reward amount)
|
||||
if oldSender.faster[port] > 1 {
|
||||
sender.faster[port] = oldSender.faster[peer.port] - 2
|
||||
} else {
|
||||
sender.faster[port] = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update sender
|
||||
t.data.peers[fromPort] = sender
|
||||
// Decide if we should also update our root info to make the sender our parent
|
||||
updateRoot := false
|
||||
oldParent, isIn := t.data.peers[t.parent]
|
||||
noParent := !isIn
|
||||
@@ -381,30 +446,49 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
|
||||
}
|
||||
return true
|
||||
}()
|
||||
sTime := now.Sub(sender.firstSeen)
|
||||
pTime := oldParent.time.Sub(oldParent.firstSeen) + switch_timeout
|
||||
// Really want to compare sLen/sTime and pLen/pTime
|
||||
// Cross multiplied to avoid divide-by-zero
|
||||
cost := len(sender.locator.coords) * int(pTime.Seconds())
|
||||
pCost := len(t.data.locator.coords) * int(sTime.Seconds())
|
||||
dropTstamp, isIn := t.drop[sender.locator.root]
|
||||
// Here be dragons
|
||||
// Decide if we need to update info about the root or change parents.
|
||||
switch {
|
||||
case !noLoop: // do nothing
|
||||
case isIn && dropTstamp >= sender.locator.tstamp: // do nothing
|
||||
case !noLoop:
|
||||
// This route loops, so we can't use the sender as our parent.
|
||||
case isIn && dropTstamp >= sender.locator.tstamp:
|
||||
// This is a known root with a timestamp older than a known timeout, so we can't trust it to be a new announcement.
|
||||
case firstIsBetter(&sender.locator.root, &t.data.locator.root):
|
||||
// This is a better root than what we're currently using, so we should update.
|
||||
updateRoot = true
|
||||
case t.data.locator.root != sender.locator.root: // do nothing
|
||||
case t.data.locator.tstamp > sender.locator.tstamp: // do nothing
|
||||
case t.data.locator.root != sender.locator.root:
|
||||
// This is not the same root, and it's apparently not better (from the above), so we should ignore it.
|
||||
case t.data.locator.tstamp > sender.locator.tstamp:
|
||||
// This timetsamp is older than the most recently seen one from this root, so we should ignore it.
|
||||
case noParent:
|
||||
// We currently have no working parent, and at this point in the switch statement, anything is better than nothing.
|
||||
updateRoot = true
|
||||
case cost < pCost:
|
||||
case sender.faster[t.parent] >= switch_faster_threshold:
|
||||
// The is reliably faster than the current parent.
|
||||
updateRoot = true
|
||||
case sender.port != t.parent: // do nothing
|
||||
case !equiv(&sender.locator, &t.data.locator):
|
||||
case reprocessing && sender.faster[t.parent] > oldParent.faster[sender.port]:
|
||||
// The sender seems to be reliably faster than the current parent, so switch to them instead.
|
||||
updateRoot = true
|
||||
case now.Sub(t.time) < switch_throttle: // do nothing
|
||||
case sender.port != t.parent:
|
||||
// Ignore further cases if the sender isn't our parent.
|
||||
case !reprocessing && !equiv(&sender.locator, &t.data.locator):
|
||||
// Special case:
|
||||
// If coords changed, then we need to penalize this node somehow, to prevent flapping.
|
||||
// First, reset all faster-related info to 0.
|
||||
// Then, de-parent the node and reprocess all messages to find a new parent.
|
||||
t.parent = 0
|
||||
for _, peer := range t.data.peers {
|
||||
if peer.port == sender.port {
|
||||
continue
|
||||
}
|
||||
t.unlockedHandleMsg(&peer.msg, peer.port, true)
|
||||
}
|
||||
// Process the sender last, to avoid keeping them as a parent if at all possible.
|
||||
t.unlockedHandleMsg(&sender.msg, sender.port, true)
|
||||
case now.Sub(t.time) < switch_throttle:
|
||||
// We've already gotten an update from this root recently, so ignore this one to avoid flooding.
|
||||
case sender.locator.tstamp > t.data.locator.tstamp:
|
||||
// The timestamp was updated, so we need to update locally and send to our peers.
|
||||
updateRoot = true
|
||||
}
|
||||
if updateRoot {
|
||||
@@ -429,6 +513,10 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
|
||||
return
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// The rest of these are related to the switch worker
|
||||
|
||||
// This is called via a sync.Once to update the atomically readable subset of switch information that gets used for routing decisions.
|
||||
func (t *switchTable) updateTable() {
|
||||
// WARNING this should only be called from within t.data.updater.Do()
|
||||
@@ -443,7 +531,7 @@ func (t *switchTable) updateTable() {
|
||||
defer t.mutex.RUnlock()
|
||||
newTable := lookupTable{
|
||||
self: t.data.locator.clone(),
|
||||
elems: make([]tableElem, 0, len(t.data.peers)),
|
||||
elems: make(map[switchPort]tableElem, len(t.data.peers)),
|
||||
}
|
||||
for _, pinfo := range t.data.peers {
|
||||
//if !pinfo.forward { continue }
|
||||
@@ -452,48 +540,285 @@ func (t *switchTable) updateTable() {
|
||||
}
|
||||
loc := pinfo.locator.clone()
|
||||
loc.coords = loc.coords[:len(loc.coords)-1] // Remove the them->self link
|
||||
newTable.elems = append(newTable.elems, tableElem{
|
||||
newTable.elems[pinfo.port] = tableElem{
|
||||
locator: loc,
|
||||
port: pinfo.port,
|
||||
})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(newTable.elems, func(i, j int) bool {
|
||||
return t.data.peers[newTable.elems[i].port].firstSeen.Before(t.data.peers[newTable.elems[j].port].firstSeen)
|
||||
})
|
||||
t.table.Store(newTable)
|
||||
}
|
||||
|
||||
// This does the switch layer lookups that decide how to route traffic.
|
||||
// Traffic uses greedy routing in a metric space, where the metric distance between nodes is equal to the distance between them on the tree.
|
||||
// Traffic must be routed to a node that is closer to the destination via the metric space distance.
|
||||
// In the event that two nodes are equally close, it gets routed to the one with the longest uptime (due to the order that things are iterated over).
|
||||
// The size of the outgoing packet queue is added to a node's tree distance when the cost of forwarding to a node, subject to the constraint that the real tree distance puts them closer to the destination than ourself.
|
||||
// Doing so adds a limited form of backpressure routing, based on local information, which allows us to forward traffic around *local* bottlenecks, provided that another greedy path exists.
|
||||
func (t *switchTable) lookup(dest []byte) switchPort {
|
||||
// Returns a copy of the atomically-updated table used for switch lookups
|
||||
func (t *switchTable) getTable() lookupTable {
|
||||
t.updater.Load().(*sync.Once).Do(t.updateTable)
|
||||
table := t.table.Load().(lookupTable)
|
||||
return t.table.Load().(lookupTable)
|
||||
}
|
||||
|
||||
// Starts the switch worker
|
||||
func (t *switchTable) start() error {
|
||||
t.core.log.Println("Starting switch")
|
||||
go t.doWorker()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if a packet should go to the self node
|
||||
// This means there's no node closer to the destination than us
|
||||
// This is mainly used to identify packets addressed to us, or that hit a blackhole
|
||||
func (t *switchTable) selfIsClosest(dest []byte) bool {
|
||||
table := t.getTable()
|
||||
myDist := table.self.dist(dest)
|
||||
if myDist == 0 {
|
||||
return 0
|
||||
// Skip the iteration step if it's impossible to be closer
|
||||
return true
|
||||
}
|
||||
// cost is in units of (expected distance) + (expected queue size), where expected distance is used as an approximation of the minimum backpressure gradient needed for packets to flow
|
||||
ports := t.core.peers.getPorts()
|
||||
var best switchPort
|
||||
bestCost := int64(^uint64(0) >> 1)
|
||||
for _, info := range table.elems {
|
||||
dist := info.locator.dist(dest)
|
||||
if !(dist < myDist) {
|
||||
if dist < myDist {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Returns true if the peer is closer to the destination than ourself
|
||||
func (t *switchTable) portIsCloser(dest []byte, port switchPort) bool {
|
||||
table := t.getTable()
|
||||
if info, isIn := table.elems[port]; isIn {
|
||||
theirDist := info.locator.dist(dest)
|
||||
myDist := table.self.dist(dest)
|
||||
return theirDist < myDist
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Get the coords of a packet without decoding
|
||||
func switch_getPacketCoords(packet []byte) []byte {
|
||||
_, pTypeLen := wire_decode_uint64(packet)
|
||||
coords, _ := wire_decode_coords(packet[pTypeLen:])
|
||||
return coords
|
||||
}
|
||||
|
||||
// Returns a unique string for each stream of traffic
|
||||
// Equal to coords
|
||||
// The sender may append arbitrary info to the end of coords (as long as it's begins with a 0x00) to designate separate traffic streams
|
||||
// Currently, it's the IPv6 next header type and the first 2 uint16 of the next header
|
||||
// This is equivalent to the TCP/UDP protocol numbers and the source / dest ports
|
||||
// TODO figure out if something else would make more sense (other transport protocols?)
|
||||
func switch_getPacketStreamID(packet []byte) string {
|
||||
return string(switch_getPacketCoords(packet))
|
||||
}
|
||||
|
||||
// Find the best port for a given set of coords
|
||||
func (t *switchTable) bestPortForCoords(coords []byte) switchPort {
|
||||
table := t.getTable()
|
||||
var best switchPort
|
||||
bestDist := table.self.dist(coords)
|
||||
for to, elem := range table.elems {
|
||||
dist := elem.locator.dist(coords)
|
||||
if !(dist < bestDist) {
|
||||
continue
|
||||
}
|
||||
p, isIn := ports[info.port]
|
||||
if !isIn {
|
||||
continue
|
||||
}
|
||||
cost := int64(dist) + p.getQueueSize()
|
||||
if cost < bestCost {
|
||||
best = info.port
|
||||
bestCost = cost
|
||||
}
|
||||
best = to
|
||||
bestDist = dist
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// Handle an incoming packet
|
||||
// Either send it to ourself, or to the first idle peer that's free
|
||||
// Returns true if the packet has been handled somehow, false if it should be queued
|
||||
func (t *switchTable) handleIn(packet []byte, idle map[switchPort]struct{}) bool {
|
||||
coords := switch_getPacketCoords(packet)
|
||||
ports := t.core.peers.getPorts()
|
||||
if t.selfIsClosest(coords) {
|
||||
// TODO? call the router directly, and remove the whole concept of a self peer?
|
||||
ports[0].sendPacket(packet)
|
||||
return true
|
||||
}
|
||||
table := t.getTable()
|
||||
myDist := table.self.dist(coords)
|
||||
var best *peer
|
||||
bestDist := myDist
|
||||
for port := range idle {
|
||||
if to := ports[port]; to != nil {
|
||||
if info, isIn := table.elems[to.port]; isIn {
|
||||
dist := info.locator.dist(coords)
|
||||
if !(dist < bestDist) {
|
||||
continue
|
||||
}
|
||||
best = to
|
||||
bestDist = dist
|
||||
}
|
||||
}
|
||||
}
|
||||
if best != nil {
|
||||
// Send to the best idle next hop
|
||||
delete(idle, best.port)
|
||||
best.sendPacket(packet)
|
||||
return true
|
||||
} else {
|
||||
// Didn't find anyone idle to send it to
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Info about a buffered packet
|
||||
type switch_packetInfo struct {
|
||||
bytes []byte
|
||||
time time.Time // Timestamp of when the packet arrived
|
||||
}
|
||||
|
||||
// Used to keep track of buffered packets
|
||||
type switch_buffer struct {
|
||||
packets []switch_packetInfo // Currently buffered packets, which may be dropped if it grows too large
|
||||
size uint64 // Total queue size in bytes
|
||||
}
|
||||
|
||||
type switch_buffers struct {
|
||||
switchTable *switchTable
|
||||
bufs map[string]switch_buffer // Buffers indexed by StreamID
|
||||
size uint64 // Total size of all buffers, in bytes
|
||||
maxbufs int
|
||||
maxsize uint64
|
||||
}
|
||||
|
||||
func (b *switch_buffers) cleanup(t *switchTable) {
|
||||
for streamID, buf := range b.bufs {
|
||||
// Remove queues for which we have no next hop
|
||||
packet := buf.packets[0]
|
||||
coords := switch_getPacketCoords(packet.bytes)
|
||||
if t.selfIsClosest(coords) {
|
||||
for _, packet := range buf.packets {
|
||||
util_putBytes(packet.bytes)
|
||||
}
|
||||
b.size -= buf.size
|
||||
delete(b.bufs, streamID)
|
||||
}
|
||||
}
|
||||
|
||||
for b.size > b.switchTable.queueTotalMaxSize {
|
||||
// Drop a random queue
|
||||
target := rand.Uint64() % b.size
|
||||
var size uint64 // running total
|
||||
for streamID, buf := range b.bufs {
|
||||
size += buf.size
|
||||
if size < target {
|
||||
continue
|
||||
}
|
||||
var packet switch_packetInfo
|
||||
packet, buf.packets = buf.packets[0], buf.packets[1:]
|
||||
buf.size -= uint64(len(packet.bytes))
|
||||
b.size -= uint64(len(packet.bytes))
|
||||
util_putBytes(packet.bytes)
|
||||
if len(buf.packets) == 0 {
|
||||
delete(b.bufs, streamID)
|
||||
} else {
|
||||
// Need to update the map, since buf was retrieved by value
|
||||
b.bufs[streamID] = buf
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handles incoming idle notifications
|
||||
// Loops over packets and sends the newest one that's OK for this peer to send
|
||||
// Returns true if the peer is no longer idle, false if it should be added to the idle list
|
||||
func (t *switchTable) handleIdle(port switchPort) bool {
|
||||
to := t.core.peers.getPorts()[port]
|
||||
if to == nil {
|
||||
return true
|
||||
}
|
||||
var best string
|
||||
var bestPriority float64
|
||||
t.queues.cleanup(t)
|
||||
now := time.Now()
|
||||
for streamID, buf := range t.queues.bufs {
|
||||
// Filter over the streams that this node is closer to
|
||||
// Keep the one with the smallest queue
|
||||
packet := buf.packets[0]
|
||||
coords := switch_getPacketCoords(packet.bytes)
|
||||
priority := float64(now.Sub(packet.time)) / float64(buf.size)
|
||||
if priority > bestPriority && t.portIsCloser(coords, port) {
|
||||
best = streamID
|
||||
bestPriority = priority
|
||||
}
|
||||
}
|
||||
if bestPriority != 0 {
|
||||
buf := t.queues.bufs[best]
|
||||
var packet switch_packetInfo
|
||||
// TODO decide if this should be LIFO or FIFO
|
||||
packet, buf.packets = buf.packets[0], buf.packets[1:]
|
||||
buf.size -= uint64(len(packet.bytes))
|
||||
t.queues.size -= uint64(len(packet.bytes))
|
||||
if len(buf.packets) == 0 {
|
||||
delete(t.queues.bufs, best)
|
||||
} else {
|
||||
// Need to update the map, since buf was retrieved by value
|
||||
t.queues.bufs[best] = buf
|
||||
}
|
||||
to.sendPacket(packet.bytes)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// The switch worker does routing lookups and sends packets to where they need to be
|
||||
func (t *switchTable) doWorker() {
|
||||
t.queues.switchTable = t
|
||||
t.queues.bufs = make(map[string]switch_buffer) // Packets per PacketStreamID (string)
|
||||
idle := make(map[switchPort]struct{}) // this is to deduplicate things
|
||||
for {
|
||||
select {
|
||||
case bytes := <-t.packetIn:
|
||||
// Try to send it somewhere (or drop it if it's corrupt or at a dead end)
|
||||
if !t.handleIn(bytes, idle) {
|
||||
// There's nobody free to take it right now, so queue it for later
|
||||
packet := switch_packetInfo{bytes, time.Now()}
|
||||
streamID := switch_getPacketStreamID(packet.bytes)
|
||||
buf, bufExists := t.queues.bufs[streamID]
|
||||
buf.packets = append(buf.packets, packet)
|
||||
buf.size += uint64(len(packet.bytes))
|
||||
t.queues.size += uint64(len(packet.bytes))
|
||||
// Keep a track of the max total queue size
|
||||
if t.queues.size > t.queues.maxsize {
|
||||
t.queues.maxsize = t.queues.size
|
||||
}
|
||||
t.queues.bufs[streamID] = buf
|
||||
if !bufExists {
|
||||
// Keep a track of the max total queue count. Only recalculate this
|
||||
// when the queue is new because otherwise repeating len(dict) might
|
||||
// cause unnecessary processing overhead
|
||||
if len(t.queues.bufs) > t.queues.maxbufs {
|
||||
t.queues.maxbufs = len(t.queues.bufs)
|
||||
}
|
||||
}
|
||||
t.queues.cleanup(t)
|
||||
}
|
||||
case port := <-t.idleIn:
|
||||
// Try to find something to send to this peer
|
||||
if !t.handleIdle(port) {
|
||||
// Didn't find anything ready to send yet, so stay idle
|
||||
idle[port] = struct{}{}
|
||||
}
|
||||
case f := <-t.admin:
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Passed a function to call.
|
||||
// This will send the function to t.admin and block until it finishes.
|
||||
func (t *switchTable) doAdmin(f func()) {
|
||||
// Pass this a function that needs to be run by the router's main goroutine
|
||||
// It will pass the function to the router and wait for the router to finish
|
||||
done := make(chan struct{})
|
||||
newF := func() {
|
||||
f()
|
||||
close(done)
|
||||
}
|
||||
t.admin <- newF
|
||||
<-done
|
||||
}
|
||||
|
@@ -17,6 +17,8 @@ package yggdrasil
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -26,6 +28,8 @@ import (
|
||||
)
|
||||
|
||||
const tcp_msgSize = 2048 + 65535 // TODO figure out what makes sense
|
||||
const default_tcp_timeout = 6 * time.Second
|
||||
const tcp_ping_interval = (default_tcp_timeout * 2 / 3)
|
||||
|
||||
// Wrapper function for non tcp/ip connections.
|
||||
func setNoDelay(c net.Conn, delay bool) {
|
||||
@@ -37,11 +41,12 @@ func setNoDelay(c net.Conn, delay bool) {
|
||||
|
||||
// The TCP listener and information about active TCP connections, to avoid duplication.
|
||||
type tcpInterface struct {
|
||||
core *Core
|
||||
serv net.Listener
|
||||
mutex sync.Mutex // Protecting the below
|
||||
calls map[string]struct{}
|
||||
conns map[tcpInfo](chan struct{})
|
||||
core *Core
|
||||
serv net.Listener
|
||||
tcp_timeout time.Duration
|
||||
mutex sync.Mutex // Protecting the below
|
||||
calls map[string]struct{}
|
||||
conns map[tcpInfo](chan struct{})
|
||||
}
|
||||
|
||||
// This is used as the key to a map that tracks existing connections, to prevent multiple connections to the same keys and local/remote address pair from occuring.
|
||||
@@ -59,36 +64,24 @@ func (iface *tcpInterface) getAddr() *net.TCPAddr {
|
||||
}
|
||||
|
||||
// Attempts to initiate a connection to the provided address.
|
||||
func (iface *tcpInterface) connect(addr string) {
|
||||
iface.call(addr)
|
||||
func (iface *tcpInterface) connect(addr string, intf string) {
|
||||
iface.call(addr, nil, intf)
|
||||
}
|
||||
|
||||
// Attempst to initiate a connection to the provided address, viathe provided socks proxy address.
|
||||
func (iface *tcpInterface) connectSOCKS(socksaddr, peeraddr string) {
|
||||
// TODO make sure this doesn't keep attempting/killing connections when one is already active.
|
||||
// I think some of the interaction between this and callWithConn needs work, so the dial isn't even attempted if there's already an outgoing call to peeraddr.
|
||||
// Or maybe only if there's already an outgoing call to peeraddr via this socksaddr?
|
||||
go func() {
|
||||
dialer, err := proxy.SOCKS5("tcp", socksaddr, nil, proxy.Direct)
|
||||
if err == nil {
|
||||
conn, err := dialer.Dial("tcp", peeraddr)
|
||||
if err == nil {
|
||||
iface.callWithConn(&wrappedConn{
|
||||
c: conn,
|
||||
raddr: &wrappedAddr{
|
||||
network: "tcp",
|
||||
addr: peeraddr,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}()
|
||||
iface.call(peeraddr, &socksaddr, "")
|
||||
}
|
||||
|
||||
// Initializes the struct.
|
||||
func (iface *tcpInterface) init(core *Core, addr string) (err error) {
|
||||
func (iface *tcpInterface) init(core *Core, addr string, readTimeout int32) (err error) {
|
||||
iface.core = core
|
||||
|
||||
iface.tcp_timeout = time.Duration(readTimeout) * time.Millisecond
|
||||
if iface.tcp_timeout >= 0 && iface.tcp_timeout < default_tcp_timeout {
|
||||
iface.tcp_timeout = default_tcp_timeout
|
||||
}
|
||||
|
||||
iface.serv, err = net.Listen("tcp", addr)
|
||||
if err == nil {
|
||||
iface.calls = make(map[string]struct{})
|
||||
@@ -112,54 +105,99 @@ func (iface *tcpInterface) listener() {
|
||||
}
|
||||
}
|
||||
|
||||
// Called by connectSOCKS, it's like call but with the connection already established.
|
||||
func (iface *tcpInterface) callWithConn(conn net.Conn) {
|
||||
go func() {
|
||||
raddr := conn.RemoteAddr().String()
|
||||
iface.mutex.Lock()
|
||||
_, isIn := iface.calls[raddr]
|
||||
iface.mutex.Unlock()
|
||||
if !isIn {
|
||||
iface.mutex.Lock()
|
||||
iface.calls[raddr] = struct{}{}
|
||||
iface.mutex.Unlock()
|
||||
defer func() {
|
||||
iface.mutex.Lock()
|
||||
delete(iface.calls, raddr)
|
||||
iface.mutex.Unlock()
|
||||
}()
|
||||
iface.handler(conn, false)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Checks if a connection already exists.
|
||||
// If not, it adds it to the list of active outgoing calls (to block future attempts) and dials the address.
|
||||
// If the dial is successful, it launches the handler.
|
||||
// When finished, it removes the outgoing call, so reconnection attempts can be made later.
|
||||
// This all happens in a separate goroutine that it spawns.
|
||||
func (iface *tcpInterface) call(saddr string) {
|
||||
func (iface *tcpInterface) call(saddr string, socksaddr *string, sintf string) {
|
||||
go func() {
|
||||
callname := saddr
|
||||
if sintf != "" {
|
||||
callname = fmt.Sprintf("%s/%s", saddr, sintf)
|
||||
}
|
||||
quit := false
|
||||
iface.mutex.Lock()
|
||||
if _, isIn := iface.calls[saddr]; isIn {
|
||||
if _, isIn := iface.calls[callname]; isIn {
|
||||
quit = true
|
||||
} else {
|
||||
iface.calls[saddr] = struct{}{}
|
||||
iface.calls[callname] = struct{}{}
|
||||
defer func() {
|
||||
// Block new calls for a little while, to mitigate livelock scenarios
|
||||
time.Sleep(default_tcp_timeout)
|
||||
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
|
||||
iface.mutex.Lock()
|
||||
delete(iface.calls, saddr)
|
||||
delete(iface.calls, callname)
|
||||
iface.mutex.Unlock()
|
||||
}()
|
||||
}
|
||||
iface.mutex.Unlock()
|
||||
if !quit {
|
||||
conn, err := net.Dial("tcp", saddr)
|
||||
if quit {
|
||||
return
|
||||
}
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if socksaddr != nil {
|
||||
if sintf != "" {
|
||||
return
|
||||
}
|
||||
var dialer proxy.Dialer
|
||||
dialer, err = proxy.SOCKS5("tcp", *socksaddr, nil, proxy.Direct)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn, err = dialer.Dial("tcp", saddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn = &wrappedConn{
|
||||
c: conn,
|
||||
raddr: &wrappedAddr{
|
||||
network: "tcp",
|
||||
addr: saddr,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
dialer := net.Dialer{}
|
||||
if sintf != "" {
|
||||
ief, err := net.InterfaceByName(sintf)
|
||||
if err != nil {
|
||||
return
|
||||
} else {
|
||||
if ief.Flags&net.FlagUp == 0 {
|
||||
return
|
||||
}
|
||||
addrs, err := ief.Addrs()
|
||||
if err == nil {
|
||||
dst, err := net.ResolveTCPAddr("tcp", saddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
src, _, err := net.ParseCIDR(addr.String())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if (src.To4() != nil) == (dst.IP.To4() != nil) && src.IsGlobalUnicast() {
|
||||
dialer.LocalAddr = &net.TCPAddr{
|
||||
IP: src,
|
||||
Port: 0,
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if dialer.LocalAddr == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
conn, err = dialer.Dial("tcp", saddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
iface.handler(conn, false)
|
||||
}
|
||||
iface.handler(conn, false)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -178,8 +216,9 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
timeout := time.Now().Add(6 * time.Second)
|
||||
sock.SetReadDeadline(timeout)
|
||||
if iface.tcp_timeout > 0 {
|
||||
sock.SetReadDeadline(time.Now().Add(iface.tcp_timeout))
|
||||
}
|
||||
_, err = sock.Read(metaBytes)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -248,24 +287,15 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
|
||||
}()
|
||||
// Note that multiple connections to the same node are allowed
|
||||
// E.g. over different interfaces
|
||||
p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link))
|
||||
p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link), sock.RemoteAddr().String())
|
||||
p.linkOut = make(chan []byte, 1)
|
||||
in := func(bs []byte) {
|
||||
p.handlePacket(bs)
|
||||
}
|
||||
out := make(chan []byte, 32) // TODO? what size makes sense
|
||||
out := make(chan []byte, 1)
|
||||
defer close(out)
|
||||
go func() {
|
||||
var shadow int64
|
||||
var stack [][]byte
|
||||
put := func(msg []byte) {
|
||||
stack = append(stack, msg)
|
||||
for len(stack) > 32 {
|
||||
util_putBytes(stack[0])
|
||||
stack = stack[1:]
|
||||
shadow++
|
||||
}
|
||||
}
|
||||
// This goroutine waits for outgoing packets, link protocol traffic, or sends idle keep-alive traffic
|
||||
send := func(msg []byte) {
|
||||
msgLen := wire_encode_uint64(uint64(len(msg)))
|
||||
buf := net.Buffers{tcp_msg[:], msgLen, msg}
|
||||
@@ -273,14 +303,18 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
|
||||
atomic.AddUint64(&p.bytesSent, uint64(len(tcp_msg)+len(msgLen)+len(msg)))
|
||||
util_putBytes(msg)
|
||||
}
|
||||
timerInterval := 4 * time.Second
|
||||
timerInterval := tcp_ping_interval
|
||||
timer := time.NewTimer(timerInterval)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
if shadow != 0 {
|
||||
p.updateQueueSize(-shadow)
|
||||
shadow = 0
|
||||
select {
|
||||
case msg := <-p.linkOut:
|
||||
// Always send outgoing link traffic first, if needed
|
||||
send(msg)
|
||||
continue
|
||||
default:
|
||||
}
|
||||
// Otherwise wait reset the timer and wait for something to do
|
||||
timer.Stop()
|
||||
select {
|
||||
case <-timer.C:
|
||||
@@ -296,34 +330,16 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
put(msg)
|
||||
}
|
||||
for len(stack) > 0 {
|
||||
select {
|
||||
case msg := <-p.linkOut:
|
||||
send(msg)
|
||||
case msg, ok := <-out:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
put(msg)
|
||||
default:
|
||||
msg := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
send(msg)
|
||||
p.updateQueueSize(-1)
|
||||
}
|
||||
send(msg) // Block until the socket write has finished
|
||||
// Now inform the switch that we're ready for more traffic
|
||||
p.core.switchTable.idleIn <- p.port
|
||||
}
|
||||
}
|
||||
}()
|
||||
p.core.switchTable.idleIn <- p.port // Start in the idle state
|
||||
p.out = func(msg []byte) {
|
||||
defer func() { recover() }()
|
||||
select {
|
||||
case out <- msg:
|
||||
p.updateQueueSize(1)
|
||||
default:
|
||||
util_putBytes(msg)
|
||||
}
|
||||
out <- msg
|
||||
}
|
||||
p.close = func() { sock.Close() }
|
||||
setNoDelay(sock, true)
|
||||
@@ -332,44 +348,56 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
|
||||
// Put all of our cleanup here...
|
||||
p.core.peers.removePeer(p.port)
|
||||
}()
|
||||
us, _, _ := net.SplitHostPort(sock.LocalAddr().String())
|
||||
them, _, _ := net.SplitHostPort(sock.RemoteAddr().String())
|
||||
themNodeID := getNodeID(&info.box)
|
||||
themAddr := address_addrForNodeID(themNodeID)
|
||||
themAddrString := net.IP(themAddr[:]).String()
|
||||
themString := fmt.Sprintf("%s@%s", themAddrString, them)
|
||||
iface.core.log.Println("Connected:", themString)
|
||||
iface.reader(sock, in) // In this goroutine, because of defers
|
||||
iface.core.log.Println("Disconnected:", themString)
|
||||
iface.core.log.Println("Connected:", themString, "source", us)
|
||||
err = iface.reader(sock, in) // In this goroutine, because of defers
|
||||
if err == nil {
|
||||
iface.core.log.Println("Disconnected:", themString, "source", us)
|
||||
} else {
|
||||
iface.core.log.Println("Disconnected:", themString, "source", us, "with error:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// This reads from the socket into a []byte buffer for incomping messages.
|
||||
// It copies completed messages out of the cache into a new slice, and passes them to the peer struct via the provided `in func([]byte)` argument.
|
||||
// Then it shifts the incomplete fragments of data forward so future reads won't overwrite it.
|
||||
func (iface *tcpInterface) reader(sock net.Conn, in func([]byte)) {
|
||||
func (iface *tcpInterface) reader(sock net.Conn, in func([]byte)) error {
|
||||
bs := make([]byte, 2*tcp_msgSize)
|
||||
frag := bs[:0]
|
||||
for {
|
||||
timeout := time.Now().Add(6 * time.Second)
|
||||
sock.SetReadDeadline(timeout)
|
||||
if iface.tcp_timeout > 0 {
|
||||
sock.SetReadDeadline(time.Now().Add(iface.tcp_timeout))
|
||||
}
|
||||
n, err := sock.Read(bs[len(frag):])
|
||||
if err != nil || n == 0 {
|
||||
break
|
||||
}
|
||||
frag = bs[:len(frag)+n]
|
||||
for {
|
||||
msg, ok, err := tcp_chop_msg(&frag)
|
||||
if err != nil {
|
||||
return
|
||||
if n > 0 {
|
||||
frag = bs[:len(frag)+n]
|
||||
for {
|
||||
msg, ok, err2 := tcp_chop_msg(&frag)
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("Message error: %v", err2)
|
||||
}
|
||||
if !ok {
|
||||
// We didn't get the whole message yet
|
||||
break
|
||||
}
|
||||
newMsg := append(util_getBytes(), msg...)
|
||||
in(newMsg)
|
||||
util_yield()
|
||||
}
|
||||
if !ok {
|
||||
break
|
||||
} // We didn't get the whole message yet
|
||||
newMsg := append(util_getBytes(), msg...)
|
||||
in(newMsg)
|
||||
util_yield()
|
||||
frag = append(bs[:0], frag...)
|
||||
}
|
||||
if err != nil || n == 0 {
|
||||
if err != io.EOF {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
frag = append(bs[:0], frag...)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,8 +3,14 @@ package yggdrasil
|
||||
// This manages the tun driver to send/recv packets to/from applications
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/songgao/packets/ethernet"
|
||||
"github.com/yggdrasil-network/water"
|
||||
|
||||
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
|
||||
)
|
||||
|
||||
const tun_IPv6_HEADER_LENGTH = 40
|
||||
@@ -20,21 +26,11 @@ type tunDevice struct {
|
||||
iface *water.Interface
|
||||
}
|
||||
|
||||
// Defines which parameters are expected by default for a TUN/TAP adapter on a
|
||||
// specific platform. These values are populated in the relevant tun_*.go for
|
||||
// the platform being targeted. They must be set.
|
||||
type tunDefaultParameters struct {
|
||||
maximumIfMTU int
|
||||
defaultIfMTU int
|
||||
defaultIfName string
|
||||
defaultIfTAPMode bool
|
||||
}
|
||||
|
||||
// Gets the maximum supported MTU for the platform based on the defaults in
|
||||
// getDefaults().
|
||||
// defaults.GetDefaults().
|
||||
func getSupportedMTU(mtu int) int {
|
||||
if mtu > getDefaults().maximumIfMTU {
|
||||
return getDefaults().maximumIfMTU
|
||||
if mtu > defaults.GetDefaults().MaximumIfMTU {
|
||||
return defaults.GetDefaults().MaximumIfMTU
|
||||
}
|
||||
return mtu
|
||||
}
|
||||
@@ -56,6 +52,23 @@ func (tun *tunDevice) start(ifname string, iftapmode bool, addr string, mtu int)
|
||||
}
|
||||
go func() { panic(tun.read()) }()
|
||||
go func() { panic(tun.write()) }()
|
||||
if iftapmode {
|
||||
go func() {
|
||||
for {
|
||||
if _, ok := tun.icmpv6.peermacs[tun.core.router.addr]; ok {
|
||||
break
|
||||
}
|
||||
request, err := tun.icmpv6.create_ndp_tap(tun.core.router.addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := tun.iface.Write(request); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,16 +82,74 @@ func (tun *tunDevice) write() error {
|
||||
continue
|
||||
}
|
||||
if tun.iface.IsTAP() {
|
||||
var frame ethernet.Frame
|
||||
frame.Prepare(
|
||||
tun.icmpv6.peermac[:6], // Destination MAC address
|
||||
tun.icmpv6.mymac[:6], // Source MAC address
|
||||
ethernet.NotTagged, // VLAN tagging
|
||||
ethernet.IPv6, // Ethertype
|
||||
len(data)) // Payload length
|
||||
copy(frame[tun_ETHER_HEADER_LENGTH:], data[:])
|
||||
if _, err := tun.iface.Write(frame); err != nil {
|
||||
panic(err)
|
||||
var destAddr address
|
||||
if data[0]&0xf0 == 0x60 {
|
||||
if len(data) < 40 {
|
||||
panic("Tried to send a packet shorter than an IPv6 header...")
|
||||
}
|
||||
copy(destAddr[:16], data[24:])
|
||||
} else if data[0]&0xf0 == 0x40 {
|
||||
if len(data) < 20 {
|
||||
panic("Tried to send a packet shorter than an IPv4 header...")
|
||||
}
|
||||
copy(destAddr[:4], data[16:])
|
||||
} else {
|
||||
return errors.New("Invalid address family")
|
||||
}
|
||||
sendndp := func(destAddr address) {
|
||||
neigh, known := tun.icmpv6.peermacs[destAddr]
|
||||
known = known && (time.Since(neigh.lastsolicitation).Seconds() < 30)
|
||||
if !known {
|
||||
request, err := tun.icmpv6.create_ndp_tap(destAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if _, err := tun.iface.Write(request); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
tun.icmpv6.peermacs[destAddr] = neighbor{
|
||||
lastsolicitation: time.Now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
var peermac macAddress
|
||||
var peerknown bool
|
||||
if data[0]&0xf0 == 0x40 {
|
||||
destAddr = tun.core.router.addr
|
||||
} else if data[0]&0xf0 == 0x60 {
|
||||
if !bytes.Equal(tun.core.router.addr[:16], destAddr[:16]) && !bytes.Equal(tun.core.router.subnet[:8], destAddr[:8]) {
|
||||
destAddr = tun.core.router.addr
|
||||
}
|
||||
}
|
||||
if neighbor, ok := tun.icmpv6.peermacs[destAddr]; ok && neighbor.learned {
|
||||
peermac = neighbor.mac
|
||||
peerknown = true
|
||||
} else if neighbor, ok := tun.icmpv6.peermacs[tun.core.router.addr]; ok && neighbor.learned {
|
||||
peermac = neighbor.mac
|
||||
peerknown = true
|
||||
sendndp(destAddr)
|
||||
} else {
|
||||
sendndp(tun.core.router.addr)
|
||||
}
|
||||
if peerknown {
|
||||
var proto ethernet.Ethertype
|
||||
switch {
|
||||
case data[0]&0xf0 == 0x60:
|
||||
proto = ethernet.IPv6
|
||||
case data[0]&0xf0 == 0x40:
|
||||
proto = ethernet.IPv4
|
||||
}
|
||||
var frame ethernet.Frame
|
||||
frame.Prepare(
|
||||
peermac[:6], // Destination MAC address
|
||||
tun.icmpv6.mymac[:6], // Source MAC address
|
||||
ethernet.NotTagged, // VLAN tagging
|
||||
proto, // Ethertype
|
||||
len(data)) // Payload length
|
||||
copy(frame[tun_ETHER_HEADER_LENGTH:], data[:])
|
||||
if _, err := tun.iface.Write(frame); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, err := tun.iface.Write(data); err != nil {
|
||||
@@ -109,10 +180,10 @@ func (tun *tunDevice) read() error {
|
||||
if tun.iface.IsTAP() {
|
||||
o = tun_ETHER_HEADER_LENGTH
|
||||
}
|
||||
if buf[o]&0xf0 != 0x60 ||
|
||||
n != 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o {
|
||||
// Either not an IPv6 packet or not the complete packet for some reason
|
||||
//panic("Should not happen in testing")
|
||||
switch {
|
||||
case buf[o]&0xf0 == 0x60 && n == 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o:
|
||||
case buf[o]&0xf0 == 0x40 && n == 256*int(buf[o+2])+int(buf[o+3])+o:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if buf[o+6] == 58 {
|
||||
|
@@ -13,17 +13,6 @@ import (
|
||||
water "github.com/yggdrasil-network/water"
|
||||
)
|
||||
|
||||
// Sane defaults for the Darwin/macOS platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func getDefaults() tunDefaultParameters {
|
||||
return tunDefaultParameters{
|
||||
maximumIfMTU: 65535,
|
||||
defaultIfMTU: 65535,
|
||||
defaultIfName: "auto",
|
||||
defaultIfTAPMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Configures the "utun" adapter with the correct IPv6 address and MTU.
|
||||
func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int) error {
|
||||
if iftapmode {
|
||||
@@ -87,7 +76,7 @@ func (tun *tunDevice) setupAddress(addr string) error {
|
||||
|
||||
ar.ifra_prefixmask.sin6_len = uint8(unsafe.Sizeof(ar.ifra_prefixmask))
|
||||
b := make([]byte, 16)
|
||||
binary.LittleEndian.PutUint16(b, uint16(0xFF00))
|
||||
binary.LittleEndian.PutUint16(b, uint16(0xFE00))
|
||||
ar.ifra_prefixmask.sin6_addr[0] = uint16(binary.BigEndian.Uint16(b))
|
||||
|
||||
ar.ifra_addr.sin6_len = uint8(unsafe.Sizeof(ar.ifra_addr))
|
||||
|
@@ -1,12 +0,0 @@
|
||||
package yggdrasil
|
||||
|
||||
// Sane defaults for the FreeBSD platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func getDefaults() tunDefaultParameters {
|
||||
return tunDefaultParameters{
|
||||
maximumIfMTU: 32767,
|
||||
defaultIfMTU: 32767,
|
||||
defaultIfName: "/dev/tap0",
|
||||
defaultIfTAPMode: true,
|
||||
}
|
||||
}
|
@@ -12,17 +12,6 @@ import (
|
||||
water "github.com/yggdrasil-network/water"
|
||||
)
|
||||
|
||||
// Sane defaults for the Linux platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func getDefaults() tunDefaultParameters {
|
||||
return tunDefaultParameters{
|
||||
maximumIfMTU: 65535,
|
||||
defaultIfMTU: 65535,
|
||||
defaultIfName: "auto",
|
||||
defaultIfTAPMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Configures the TAP adapter with the correct IPv6 address and MTU.
|
||||
func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int) error {
|
||||
var config water.Config
|
||||
@@ -40,6 +29,18 @@ func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int)
|
||||
}
|
||||
tun.iface = iface
|
||||
tun.mtu = getSupportedMTU(mtu)
|
||||
// The following check is specific to Linux, as the TAP driver only supports
|
||||
// an MTU of 65535-14 to make room for the ethernet headers. This makes sure
|
||||
// that the MTU gets rounded down to 65521 instead of causing a panic.
|
||||
if iftapmode {
|
||||
if tun.mtu > 65535-tun_ETHER_HEADER_LENGTH {
|
||||
tun.mtu = 65535 - tun_ETHER_HEADER_LENGTH
|
||||
}
|
||||
}
|
||||
// Friendly output
|
||||
tun.core.log.Printf("Interface name: %s", tun.iface.Name())
|
||||
tun.core.log.Printf("Interface IPv6: %s", addr)
|
||||
tun.core.log.Printf("Interface MTU: %d", tun.mtu)
|
||||
return tun.setupAddress(addr)
|
||||
}
|
||||
|
||||
|
@@ -1,12 +0,0 @@
|
||||
package yggdrasil
|
||||
|
||||
// Sane defaults for the NetBSD platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func getDefaults() tunDefaultParameters {
|
||||
return tunDefaultParameters{
|
||||
maximumIfMTU: 9000,
|
||||
defaultIfMTU: 9000,
|
||||
defaultIfName: "/dev/tap0",
|
||||
defaultIfTAPMode: true,
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
package yggdrasil
|
||||
|
||||
// Sane defaults for the OpenBSD platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func getDefaults() tunDefaultParameters {
|
||||
return tunDefaultParameters{
|
||||
maximumIfMTU: 16384,
|
||||
defaultIfMTU: 16384,
|
||||
defaultIfName: "/dev/tap0",
|
||||
defaultIfTAPMode: true,
|
||||
}
|
||||
}
|
@@ -7,17 +7,6 @@ import water "github.com/yggdrasil-network/water"
|
||||
// This is to catch unsupported platforms
|
||||
// If your platform supports tun devices, you could try configuring it manually
|
||||
|
||||
// These are sane defaults for any platform that has not been matched by one of
|
||||
// the other tun_*.go files.
|
||||
func getDefaults() tunDefaultParameters {
|
||||
return tunDefaultParameters{
|
||||
maximumIfMTU: 65535,
|
||||
defaultIfMTU: 65535,
|
||||
defaultIfName: "none",
|
||||
defaultIfTAPMode: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Creates the TUN/TAP adapter, if supported by the Water library. Note that
|
||||
// no guarantees are made at this point on an unsupported platform.
|
||||
func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int) error {
|
||||
|
@@ -10,17 +10,6 @@ import (
|
||||
|
||||
// This is to catch Windows platforms
|
||||
|
||||
// Sane defaults for the Windows platform. The "default" options may be
|
||||
// may be replaced by the running configuration.
|
||||
func getDefaults() tunDefaultParameters {
|
||||
return tunDefaultParameters{
|
||||
maximumIfMTU: 65535,
|
||||
defaultIfMTU: 65535,
|
||||
defaultIfName: "auto",
|
||||
defaultIfTAPMode: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Configures the TAP adapter with the correct IPv6 address and MTU. On Windows
|
||||
// we don't make use of a direct operating system API to do this - we instead
|
||||
// delegate the hard work to "netsh".
|
||||
@@ -68,6 +57,10 @@ func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Friendly output
|
||||
tun.core.log.Printf("Interface name: %s", tun.iface.Name())
|
||||
tun.core.log.Printf("Interface IPv6: %s", addr)
|
||||
tun.core.log.Printf("Interface MTU: %d", tun.mtu)
|
||||
return tun.setupAddress(addr)
|
||||
}
|
||||
|
||||
|
@@ -25,19 +25,15 @@ func wire_encode_uint64(elem uint64) []byte {
|
||||
|
||||
// Encode uint64 using a variable length scheme.
|
||||
// Similar to binary.Uvarint, but big-endian.
|
||||
func wire_put_uint64(elem uint64, out []byte) []byte {
|
||||
bs := make([]byte, 0, 10)
|
||||
bs = append(bs, byte(elem&0x7f))
|
||||
for e := elem >> 7; e > 0; e >>= 7 {
|
||||
bs = append(bs, byte(e|0x80))
|
||||
func wire_put_uint64(e uint64, out []byte) []byte {
|
||||
var b [10]byte
|
||||
i := len(b) - 1
|
||||
b[i] = byte(e & 0x7f)
|
||||
for e >>= 7; e != 0; e >>= 7 {
|
||||
i--
|
||||
b[i] = byte(e | 0x80)
|
||||
}
|
||||
// Now reverse bytes, because we set them in the wrong order
|
||||
// TODO just put them in the right place the first time...
|
||||
last := len(bs) - 1
|
||||
for idx := 0; idx < len(bs)/2; idx++ {
|
||||
bs[idx], bs[last-idx] = bs[last-idx], bs[idx]
|
||||
}
|
||||
return append(out, bs...)
|
||||
return append(out, b[i:]...)
|
||||
}
|
||||
|
||||
// Returns the length of a wire encoded uint64 of this value.
|
||||
|
222
yggdrasilctl.go
222
yggdrasilctl.go
@@ -1,222 +0,0 @@
|
||||
package main
|
||||
|
||||
import "flag"
|
||||
import "fmt"
|
||||
import "strings"
|
||||
import "net"
|
||||
import "sort"
|
||||
import "encoding/json"
|
||||
import "strconv"
|
||||
import "os"
|
||||
|
||||
type admin_info map[string]interface{}
|
||||
|
||||
func main() {
|
||||
server := flag.String("endpoint", "localhost:9001", "Admin socket endpoint")
|
||||
injson := flag.Bool("json", false, "Output in JSON format")
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) == 0 {
|
||||
fmt.Println("usage:", os.Args[0], "[-endpoint=localhost:9001] [-json] command [key=value] [...]")
|
||||
fmt.Println("example:", os.Args[0], "getPeers")
|
||||
fmt.Println("example:", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false")
|
||||
fmt.Println("example:", os.Args[0], "-endpoint=localhost:9001 getDHT")
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := net.Dial("tcp", *server)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
decoder := json.NewDecoder(conn)
|
||||
encoder := json.NewEncoder(conn)
|
||||
send := make(admin_info)
|
||||
recv := make(admin_info)
|
||||
|
||||
for c, a := range args {
|
||||
if c == 0 {
|
||||
send["request"] = a
|
||||
continue
|
||||
}
|
||||
tokens := strings.Split(a, "=")
|
||||
if i, err := strconv.Atoi(tokens[1]); err == nil {
|
||||
send[tokens[0]] = i
|
||||
} else {
|
||||
switch tokens[1] {
|
||||
case "true":
|
||||
send[tokens[0]] = true
|
||||
case "false":
|
||||
send[tokens[0]] = false
|
||||
default:
|
||||
send[tokens[0]] = tokens[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := encoder.Encode(&send); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := decoder.Decode(&recv); err == nil {
|
||||
if recv["status"] == "error" {
|
||||
if err, ok := recv["error"]; ok {
|
||||
fmt.Println("Error:", err)
|
||||
} else {
|
||||
fmt.Println("Unspecified error occured")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
if _, ok := recv["request"]; !ok {
|
||||
fmt.Println("Missing request in response (malformed response?)")
|
||||
return
|
||||
}
|
||||
if _, ok := recv["response"]; !ok {
|
||||
fmt.Println("Missing response body (malformed response?)")
|
||||
return
|
||||
}
|
||||
req := recv["request"].(map[string]interface{})
|
||||
res := recv["response"].(map[string]interface{})
|
||||
|
||||
if *injson {
|
||||
if json, err := json.MarshalIndent(res, "", " "); err == nil {
|
||||
fmt.Println(string(json))
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
switch req["request"] {
|
||||
case "dot":
|
||||
fmt.Println(res["dot"])
|
||||
case "help", "getPeers", "getSwitchPeers", "getDHT", "getSessions":
|
||||
maxWidths := make(map[string]int)
|
||||
var keyOrder []string
|
||||
keysOrdered := false
|
||||
|
||||
for _, tlv := range res {
|
||||
for slk, slv := range tlv.(map[string]interface{}) {
|
||||
if !keysOrdered {
|
||||
for k := range slv.(map[string]interface{}) {
|
||||
keyOrder = append(keyOrder, fmt.Sprint(k))
|
||||
}
|
||||
sort.Strings(keyOrder)
|
||||
keysOrdered = true
|
||||
}
|
||||
for k, v := range slv.(map[string]interface{}) {
|
||||
if len(fmt.Sprint(slk)) > maxWidths["key"] {
|
||||
maxWidths["key"] = len(fmt.Sprint(slk))
|
||||
}
|
||||
if len(fmt.Sprint(v)) > maxWidths[k] {
|
||||
maxWidths[k] = len(fmt.Sprint(v))
|
||||
if maxWidths[k] < len(k) {
|
||||
maxWidths[k] = len(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(keyOrder) > 0 {
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", "")
|
||||
for _, v := range keyOrder {
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths[v])+"s ", v)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
for slk, slv := range tlv.(map[string]interface{}) {
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", slk)
|
||||
for _, k := range keyOrder {
|
||||
preformatted := slv.(map[string]interface{})[k]
|
||||
var formatted string
|
||||
switch k {
|
||||
case "bytes_sent", "bytes_recvd":
|
||||
formatted = fmt.Sprintf("%d", uint(preformatted.(float64)))
|
||||
case "uptime", "last_seen":
|
||||
seconds := uint(preformatted.(float64)) % 60
|
||||
minutes := uint(preformatted.(float64)/60) % 60
|
||||
hours := uint(preformatted.(float64) / 60 / 60)
|
||||
formatted = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
|
||||
default:
|
||||
formatted = fmt.Sprint(preformatted)
|
||||
}
|
||||
fmt.Printf("%-"+fmt.Sprint(maxWidths[k])+"s ", formatted)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
case "getTunTap", "setTunTap":
|
||||
for k, v := range res {
|
||||
fmt.Println("Interface name:", k)
|
||||
if mtu, ok := v.(map[string]interface{})["mtu"].(float64); ok {
|
||||
fmt.Println("Interface MTU:", mtu)
|
||||
}
|
||||
if tap_mode, ok := v.(map[string]interface{})["tap_mode"].(bool); ok {
|
||||
fmt.Println("TAP mode:", tap_mode)
|
||||
}
|
||||
}
|
||||
case "getSelf":
|
||||
for k, v := range res["self"].(map[string]interface{}) {
|
||||
fmt.Println("IPv6 address:", k)
|
||||
if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok {
|
||||
fmt.Println("IPv6 subnet:", subnet)
|
||||
}
|
||||
if coords, ok := v.(map[string]interface{})["coords"].(string); ok {
|
||||
fmt.Println("Coords:", coords)
|
||||
}
|
||||
}
|
||||
case "addPeer", "removePeer", "addAllowedEncryptionPublicKey", "removeAllowedEncryptionPublicKey":
|
||||
if _, ok := res["added"]; ok {
|
||||
for _, v := range res["added"].([]interface{}) {
|
||||
fmt.Println("Added:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
if _, ok := res["not_added"]; ok {
|
||||
for _, v := range res["not_added"].([]interface{}) {
|
||||
fmt.Println("Not added:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
if _, ok := res["removed"]; ok {
|
||||
for _, v := range res["removed"].([]interface{}) {
|
||||
fmt.Println("Removed:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
if _, ok := res["not_removed"]; ok {
|
||||
for _, v := range res["not_removed"].([]interface{}) {
|
||||
fmt.Println("Not removed:", fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
case "getAllowedEncryptionPublicKeys":
|
||||
if _, ok := res["allowed_box_pubs"]; !ok {
|
||||
fmt.Println("All connections are allowed")
|
||||
} else if res["allowed_box_pubs"] == nil {
|
||||
fmt.Println("All connections are allowed")
|
||||
} else {
|
||||
fmt.Println("Connections are allowed only from the following public box keys:")
|
||||
for _, v := range res["allowed_box_pubs"].([]interface{}) {
|
||||
fmt.Println("-", v)
|
||||
}
|
||||
}
|
||||
case "getMulticastInterfaces":
|
||||
if _, ok := res["multicast_interfaces"]; !ok {
|
||||
fmt.Println("No multicast interfaces found")
|
||||
} else if res["multicast_interfaces"] == nil {
|
||||
fmt.Println("No multicast interfaces found")
|
||||
} else {
|
||||
fmt.Println("Multicast peer discovery is active on:")
|
||||
for _, v := range res["multicast_interfaces"].([]interface{}) {
|
||||
fmt.Println("-", v)
|
||||
}
|
||||
}
|
||||
default:
|
||||
if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil {
|
||||
fmt.Println(string(json))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v, ok := recv["status"]; ok && v == "error" {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
Reference in New Issue
Block a user