Compare commits

...

198 Commits

Author SHA1 Message Date
Arceliar
c1ae9ea0d4 Switch back to using an actor to manage link state, and slighty randomize the delay between multicast announcements. This seems to fix the issue with duplicate connections (and breaks a livelock in the multicast code where both nodes keep closing the listen side of their connection, but that's kind of a hack, we need a better solution) 2023-06-18 03:40:40 -05:00
Neil Alexander
2eda59d9e4 Improve link setup locking and guards 2023-05-23 22:39:10 +01:00
Neil Alexander
06ca8941c7 Fix race condition between incoming and outgoing connection setup 2023-05-22 23:10:44 +01:00
Arceliar
8562b6b86e Merge pull request #1040 from yggdrasil-network/Arceliar/allocs
Reduce allocations
2023-05-21 12:56:37 -05:00
Arceliar
e94985c583 try to cheer up the linter again 2023-05-21 12:49:49 -05:00
Arceliar
5a6f27e732 cheer up the linter 2023-05-21 12:43:03 -05:00
Arceliar
8b5add5301 reduce allocations (also pulls in updated ironwood to do the same) 2023-05-21 12:38:16 -05:00
Neil
52709696a5 Merge pull request #1036 from yggdrasil-network/neil/linktweaks
Tweak link state locking, add comments, listener priority, other fixes
2023-05-21 00:06:43 +01:00
Neil Alexander
cb8333f9ff Tweak lock behaviour 2023-05-21 00:02:04 +01:00
Neil Alexander
333561f4e1 Tweak link state locking, add comments, listener priority, other fixes 2023-05-20 23:44:31 +01:00
Neil
2565cbf11b Merge pull request #1034 from yggdrasil-network/neil/futurelink2
Link refactoring, admin socket changes, TLS changes
2023-05-20 23:02:44 +01:00
Arceliar
19ca25538f Merge pull request #1033 from yggdrasil-network/ironwood-experimental
Update to experimental ironwood
2023-05-20 17:00:23 -05:00
Neil Alexander
aff3201084 Fix incoming connection handlers 2023-05-20 22:22:15 +01:00
Neil Alexander
c0188f5600 Discriminate multicast peers more loosely 2023-05-20 21:18:49 +01:00
Neil Alexander
e0b39b303f Use regular mutex instead (less type assertions)
This reverts commit 5ba9dadc49.
2023-05-20 18:36:44 +01:00
Neil Alexander
5ba9dadc49 Use sync.Map instead of link actor 2023-05-20 18:31:01 +01:00
Neil Alexander
6e338b6f89 Fix con urrent map accesses 2023-05-20 18:21:02 +01:00
Neil Alexander
e290e744f4 Fix -autoconf 2023-05-20 10:54:49 +01:00
Neil Alexander
a233e775eb yggdrasilctl tweaks 2023-05-19 20:57:14 +01:00
Neil Alexander
6ac2fae845 Fix Windows build 2023-05-19 20:34:51 +01:00
Neil Alexander
7b1635245f Add missing path notify and bloom transform 2023-05-19 19:33:40 +01:00
Neil Alexander
a9ec3877b5 Fix unit test 2023-05-19 19:09:06 +01:00
Neil Alexander
7afa23be4c Link refactoring, admin socket changes 2023-05-19 19:09:05 +01:00
Arceliar
c7ee7d9681 update ironwood dependency (it should build now...) 2023-05-14 21:24:08 -05:00
Arceliar
ffb2f06992 Merge branch 'ironwood-experimental' of https://github.com/yggdrasil-network/yggdrasil-go into ironwood-experimental 2023-05-14 21:14:32 -05:00
Arceliar
101189a9dc update ironwood dependency 2023-05-14 21:13:53 -05:00
Neil Alexander
c7ea223a9a Update mobile bindings 2023-05-14 10:16:33 +01:00
Arceliar
669e61af9a update to bugfixed ironwood, fix broken core test, add getPaths handler to admin socket 2023-05-13 16:15:04 -05:00
Arceliar
5e95246c26 update to ironwood v0.0.0-20230513191034-495699d87ae4 with API changes 2023-05-13 14:44:38 -05:00
Neil Alexander
1345960d5f Update to Arceliar/ironwood@14d951a 2023-05-07 17:29:46 +01:00
Arceliar
8696650958 Update go.mod 2023-03-26 17:06:18 -05:00
Arceliar
ebd3596c2c Update ci.yml 2023-03-26 17:05:55 -05:00
Arceliar
e99c870d51 update admin functions and fix core tests 2023-03-26 16:49:40 -05:00
Arceliar
abbe94fa80 fix core tests and run gofmt on src 2023-03-26 16:34:49 -05:00
Arceliar
fc632c5caa comment out some unused ipv6rwc code 2023-03-26 16:17:31 -05:00
Arceliar
5b6d9d52f3 update ironwood replace, update ipv6rwc to work (may need updates later if interface changes) 2023-03-26 16:12:45 -05:00
Neil Alexander
5a243d5b95 Update ironwood replace 2023-03-19 21:44:34 +00:00
Neil Alexander
a148f4cfec More updates for Ygg v0.5 2023-03-19 10:33:07 +00:00
Neil Alexander
83c1a810b5 New handshake, use softcrdt upstream 2023-03-18 12:14:32 +00:00
Neil
1420ea5662 Merge pull request #1004 from Dry-Leaf/bsd_build_fix
Added member to Logger struct expected by tun_bsd.go
2023-02-26 22:01:32 +00:00
Neil
a8f0ada7ee Merge branch 'develop' into bsd_build_fix 2023-02-26 21:54:50 +00:00
Neil
1685b87a04 Merge pull request #1021 from yggdrasil-network/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.0.0-20221014081412-f15817d10f9b to 0.7.0
2023-02-26 21:43:51 +00:00
Neil
9ee6c46b1d Merge branch 'develop' into bsd_build_fix 2023-02-26 21:40:29 +00:00
Neil
3b0a819e68 Merge branch 'develop' into dependabot/go_modules/golang.org/x/net-0.7.0 2023-02-26 21:36:09 +00:00
Neil Alexander
38736358dd Fix lint error properly this time 2023-02-26 21:35:56 +00:00
Neil Alexander
1dd1d0ab8c Build packages with Go 1.20 2023-02-26 21:32:26 +00:00
Neil Alexander
6d6c408957 Test against Go 1.20, maybe fix lint issue 2023-02-26 21:31:20 +00:00
Neil
783b4d3de6 Merge branch 'develop' into bsd_build_fix 2023-02-26 21:28:29 +00:00
Neil
a6f742ee93 Merge branch 'develop' into dependabot/go_modules/golang.org/x/net-0.7.0 2023-02-26 21:27:56 +00:00
Neil
4189053cfc Merge pull request #981 from yggdrasil-network/neilalexander/tryall
Try all addresses when connecting to a DNS name
2023-02-26 21:24:01 +00:00
dependabot[bot]
886281af7c Bump golang.org/x/net from 0.0.0-20221014081412-f15817d10f9b to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20221014081412-f15817d10f9b to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-25 02:28:24 +00:00
anon
9cbc71bc8a Added member to Logger struct expected by tun_bsd.go 2022-12-18 00:37:34 -05:00
Neil Alexander
723097fbf6 Deduplicate some logic 2022-11-26 16:18:15 +00:00
Neil Alexander
1adc88ec77 Merge branch 'develop' into neilalexander/tryall 2022-11-26 16:00:46 +00:00
Neil Alexander
14f1cd4696 Version 0.4.7
Merge pull request #986 from yggdrasil-network/develop
2022-11-20 21:20:11 +00:00
Neil Alexander
b0f6544b07 Update changelog date 2022-11-20 21:14:33 +00:00
Neil Alexander
48d278bd2a Version 0.4.7 changelog (#985) 2022-11-15 19:06:14 +00:00
Neil Alexander
596f16aa6c Reduce allocations in encrypted package (update to Arceliar/ironwood@ec61cea) 2022-11-15 12:46:08 +00:00
Neil Alexander
ae24f5de38 Less aggressive key ratcheting (update to Arceliar/ironwood@bf5f12a) 2022-11-12 16:55:23 +00:00
Neil Alexander
cba667f28d Fix race conditions (update to Arceliar/ironwood@2c0740b) 2022-11-12 16:47:20 +00:00
Neil Alexander
9df3bc0066 Update to Arceliar/ironwood@846a97f5e5 2022-11-12 15:26:43 +00:00
Neil Alexander
e824c73e21 Fix crash 2022-11-12 11:56:50 +00:00
Neil Alexander
7efd66932f Redial failed connections if possible (#983) 2022-11-12 11:30:03 +00:00
solanav
0da871f528 Fix #884 (#916)
* Fixed #884

* Remove yggdrasil and yggdrasilctl

* Fixed #884

Co-authored-by: asolana <asolana@deloitte.es>
Co-authored-by: solanav <solanav1337@gmail.com>
Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
2022-11-08 22:19:43 +00:00
majestrate
6fed2a75d7 Make TLS certs never expire (#977)
According to RFC5280 we can make TLS certs never expire by setting their `NotAfter` date to a value that is basically the end of time.

Fixes #976.
2022-11-08 22:11:22 +00:00
Neil Alexander
110613b234 Try all addresses when connecting to a DNS name
Fixes #980
2022-11-08 21:59:13 +00:00
Neil Alexander
6112c9cf18 Fix build 2022-11-01 18:34:49 +00:00
Neil Alexander
590d83aa9c Fix #975 by not exporting uint8 2022-11-01 17:42:52 +00:00
Revertron
ee33bd248f Added two new methods to mobile package (#974)
* Added two new methods

In order to implement https://github.com/yggdrasil-network/yggdrasil-android/issues/25 we need these new methods.

* Renamed methods, changed comments
2022-11-01 12:10:50 +00:00
Neil Alexander
cfa293d189 Fix bug in admin socket where requests fail unless "arguments":{} is specified in the JSON 2022-10-26 22:29:19 +01:00
Neil Alexander
98b0aaf747 Merge branch 'master' into develop 2022-10-26 18:26:37 +01:00
Neil Alexander
4c66a13b93 Version 0.4.6 2022-10-26 18:25:48 +01:00
Neil Alexander
f08dec822a Priority support (#964)
* Allow setting link priorities

* Fix a bug

* Allow setting priority on listeners and multicast interfaces

* Update `yggdrasilctl`

* Update to Arceliar/ironwood#5
2022-10-26 09:24:24 +01:00
Neil Alexander
9a9452dcc8 Fix panic in GetPeers that may happen mid-link setup 2022-10-25 18:58:52 +01:00
Neil Alexander
65e350153e Don't start multicast module if all Beacon and Listen are disabled 2022-10-22 18:05:14 +01:00
Neil Alexander
35ea66d651 Varying connection check strictness based on scope 2022-10-22 17:45:09 +01:00
Neil Alexander
8fe1c41295 Don't reject multiple genuine links from the same host 2022-10-22 16:59:25 +01:00
Neil Alexander
d66b3ffb7a Always allow link-local peerings again 2022-10-22 16:23:25 +01:00
Neil Alexander
63c4cb5c21 Fix reporting name for TCP 2022-10-22 15:47:09 +01:00
Neil Alexander
0a1a155e66 Use SO_REUSEADDR instead of SO_REUSEPORT on Linux 2022-10-22 14:56:29 +01:00
Neil Alexander
c55611a478 Tweak logging for connections 2022-10-22 14:56:11 +01:00
Neil Alexander
22caddef63 Don't log duplicate connection attempt 2022-10-21 19:49:49 +01:00
Neil Alexander
81839ad50d Fix InterfacePeers 2022-10-21 19:49:15 +01:00
Neil Alexander
b8a2d9f125 Version 0.4.5 (#957)
* Version 0.4.5 changelog

* Update changelog
2022-10-18 23:04:06 +01:00
Neil Alexander
8ce7c86383 Update some dependencies 2022-10-15 17:45:41 +01:00
Neil Alexander
69782ad87b Improve shutdown behaviour (fixes #891) 2022-10-15 16:07:32 +01:00
Neil Alexander
ee21c56e43 Fix setting nodeinfo (closes #954) 2022-10-15 15:42:52 +01:00
Neil Alexander
69632bacb5 Tidy up 2022-10-02 13:20:39 +01:00
Neil Alexander
962665189c Tweaks to yggdrasilctl 2022-10-02 13:15:11 +01:00
Neil Alexander
428d2375da Don't allow configuring the same peer more than once 2022-10-02 12:39:18 +01:00
Neil Alexander
8cf76f841d Silence already connected to this node 2022-10-02 12:36:51 +01:00
ehmry
7db934488e Reimplement AddPeer and RemovePeer for admin socket (#951)
* Reimplement AddPeer and RemovePeer for admin socket

Fix #950

* Disconnect the peer on `removePeer`

Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
2022-10-02 12:35:43 +01:00
Neil Alexander
c922eba2d8 Fix sending arguments to the admin socket in yggdrasilctl 2022-09-24 21:28:09 +01:00
Neil Alexander
1de587a971 Update to Arceliar/ironwood@ed4b6d4 2022-09-24 17:06:24 +01:00
Neil Alexander
d9fe6f72ac Lint tweaks 2022-09-24 17:05:44 +01:00
Neil Alexander
d24d3fa047 Use deadline for link handshake (#949)
This uses a 6 second deadline for timeouts instead of using `util.FuncTimeout` at 30 seconds for the read and then again for the write.

If the handshake doesn't complete within 6 seconds then it's going to probably collapse when we give the connection to Ironwood and it tries to do a keepalive anyway.
2022-09-24 16:51:31 +01:00
Neil Alexander
e165b1fa0c Add quote marks to InterfacePeers comment
Fixes #945.
2022-09-24 14:44:50 +01:00
Neil Alexander
01c44a087b Rename tuntap package to tun
We haven't had TAP support in ages.
2022-09-24 14:41:47 +01:00
Neil Alexander
217ac39e77 Allow setting default config path and AdminListen at compile time
By providing the following items to `LDFLAGS`:

* `-X github.com/yggdrasil-network/yggdrasil-go/src/defaults.defaultConfig=/path/to/config`
* '-X github.com/yggdrasil-network/yggdrasil-go/src/defaults.defaultAdminListen=unix://path/to/sock'

Closes #818.
2022-09-24 14:09:08 +01:00
Neil Alexander
0abfe78858 Silence error when reconnecting to already connected peer 2022-09-24 13:46:22 +01:00
Neil Alexander
5ad8c33d26 Remove packaging from main CI run 2022-09-24 13:38:14 +01:00
Neil Alexander
b67c313f44 Admin socket and yggdrasilctl improvements
This refactors the request parsing, as well as improving the output for some request types. It also tweaks `yggdrasilctl` output, which should help with #947.
2022-09-24 12:22:38 +01:00
Neil Alexander
5ef61faeff Link refactor (#941)
* Link refactoring

* More refactoring

* More tweaking

* Cleaner shutdowns, UNIX socket support, more tweaks

* Actorise links, remove mutex

* SOCKS support
2022-09-17 20:07:00 +01:00
Alexander Ivanov
414aaf6eb9 Update mobile.go (#942) 2022-09-05 12:55:35 +01:00
Neil Alexander
88a393a7b3 Load listen addresses 2022-09-03 17:26:12 +01:00
Neil Alexander
dc9720e580 Extend getSessions admin call to include uptime/TX/RX 2022-09-03 16:55:57 +01:00
Neil Alexander
5477566fa9 Length not capacity 2022-09-03 12:38:42 +01:00
Neil Alexander
9cdfd59476 Tidy up a bit, make sure to copy the private key at startup 2022-09-03 12:34:29 +01:00
Neil Alexander
a7d06e048a Refactor TUN setup (isolated config) 2022-09-03 12:20:57 +01:00
Neil Alexander
b1f61fb0a8 Refactor admin socket setup (isolated config) 2022-09-03 11:54:46 +01:00
Neil Alexander
493208fb37 Refactor multicast setup (isolated config, etc) 2022-09-03 11:42:05 +01:00
Neil Alexander
dad0b10dfe Move Core._applyOption 2022-09-03 10:51:44 +01:00
Neil Alexander
c6fe81b5d2 Admin socket and yggdrasilctl refactoring (#939) 2022-09-03 10:50:43 +01:00
Neil Alexander
4f2abece81 Fix panic in tcp.init for incorrectly formatted listen addresses 2022-09-01 16:56:42 +01:00
Karandashov Daniil
486ffebedd Delete unused param (#935) 2022-08-29 20:40:19 +01:00
Arceliar
af99fa4f6b Merge pull request #929 from yggdrasil-network/neilalexander/refactor
Node setup refactoring
2022-08-28 13:46:42 -05:00
Arceliar
a182fad8d6 Merge branch 'develop' into neilalexander/refactor 2022-08-28 13:39:26 -05:00
Alexander Ivanov
f8e626dbe1 Fix Android multicast crash (#930)
* Do not exit on multicast errors (mobile)

* Consistency with cmd/yggdrasil/main.go

Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
2022-08-10 22:54:02 +01:00
Neil Alexander
dd66e8a9c9 Merge branch 'develop' into neilalexander/refactor 2022-08-06 15:23:44 +01:00
Neil Alexander
16b8149052 No longer use ioutil which is deprecated 2022-08-06 15:21:21 +01:00
Neil Alexander
d5c0dc9bee Go 1.19 in CI 2022-08-06 15:19:01 +01:00
Neil Alexander
4c889703b1 Continue refactoring 2022-08-06 15:05:12 +01:00
Neil Alexander
5616b9fc84 Don't lose my work 2022-07-24 10:23:25 +01:00
Neil Alexander
41b4bf69cf Version 0.4.4 2022-07-07 18:36:11 +01:00
Neil Alexander
36c754cd0d Merge branch 'develop' into v044 2022-07-07 18:19:24 +01:00
Neil Alexander
8c454a146c Silence incorrect linter warning 2022-07-07 18:19:15 +01:00
Neil Alexander
df7ca3a5b8 Update changelog 2022-07-07 18:17:39 +01:00
Neil Alexander
234addc81f Update changelog 2022-07-07 18:17:27 +01:00
Neil Alexander
96ba6f0fd9 Merge branch 'develop' into v044 2022-07-07 18:16:05 +01:00
Neil Alexander
e4ec277683 Merge pull request #902 from Rubikoid/getself-fix-coords
Fix printing self coordinates in getself command of yggdrasilctl
2022-07-07 18:15:27 +01:00
Neil Alexander
88a0a3e8fb Fix data races in handleProto (observed by @majestrate) 2022-07-07 17:03:29 +01:00
Rubikoid
c19319df5e Fix coords print 2022-05-03 11:40:19 +03:00
Neil Alexander
4ddebb338d Update changelog 2022-04-18 15:29:43 +01:00
Neil Alexander
e13657d2ca Version 0.4.4 changelog 2022-04-18 15:27:47 +01:00
Neil Alexander
42d4298e19 Update ironwood to latest commit on archive-ygg0.4 branch 2022-04-18 15:23:52 +01:00
Neil Alexander
5e89ab706f Update README.md 2022-04-18 15:20:45 +01:00
Neil Alexander
b77b018c4d Modify workflow strategy 2022-04-18 10:35:05 +01:00
Neil Alexander
c3de1542b0 Move CodeQL into main CI workflow 2022-04-18 10:33:33 +01:00
Neil Alexander
55f7874b35 Limit concurrency of CI runs 2022-04-18 10:30:40 +01:00
Neil Alexander
e9caf989b8 Enable CodeQL 2022-04-18 10:27:43 +01:00
Neil Alexander
d2308f8d3a Remove Appveyor and CircleCI configs 2022-04-18 10:25:05 +01:00
Neil Alexander
bc78530fcb Build packages in GitHub Actions 2022-04-17 23:38:16 +01:00
Neil Alexander
073799d3de Require Go 1.17 2022-04-17 18:22:26 +01:00
Neil Alexander
41d890bb64 Run goimports 2022-04-17 18:02:25 +01:00
Neil Alexander
90f9be38c5 Fix lint errors 2022-04-17 17:56:54 +01:00
Neil Alexander
c7ffbc05a5 Update GitHub Actions 2022-04-17 17:53:55 +01:00
Neil Alexander
93c94e38f9 GitHub Actions 2022-04-17 17:24:34 +01:00
Neil Alexander
6c4778bb67 Merge pull request #907 from yggdrasil-network/neilalexander/pmtud 2022-04-03 17:45:33 +01:00
Neil Alexander
0c4c385885 Fix regression in Path MTU discovery
In the past we used to send back anything up to 900 bytes of the packet in the ICMPv6 Packet Too Big response, whereas now we seemingly only send back 40 bytes.

It turns out that sending back only the 40 bytes of IPv6 headers isn't enough for most operating systems to positively ID the flow to reduce the MTU. This PR updates it so that we can send up to 512 bytes instead (900 is probably excessive) — that should leave plenty of room for any number of IPv6 extension headers and the next protocol headers and some of the payload.

This seems to fix the problem in my testing.
2022-04-03 12:48:06 +01:00
Neil Alexander
559e31c502 Merge pull request #896 from yggdrasil-network/develop
Version 0.4.3
2022-02-06 15:24:01 +00:00
Neil Alexander
31717a8578 Version 0.4.3 changelog (#895)
* Version 0.4.3 changelog

* Update CHANGELOG.md
2022-02-06 15:16:54 +00:00
Neil Alexander
315e222173 Update to Arceliar/ironwood@8951369625 2022-02-01 21:53:55 +00:00
Neil Alexander
2d2ad4692b Restore uptime, bytes_sent and bytes_recvd to getPeers (#888)
* Restore `uptime`, `bytes_sent` and `bytes_recvd` to the admin API for peers

* Wrap conn in Yggdrasil instead, so not necessary to do so in Ironwood

* Shuffle struct for alignment
2022-02-01 13:37:45 +00:00
Tom
9f5cc0eecb Make message clearer and downgrade (#812)
* Make message clearer and downgrade

* Differentiate between incoming and outgoing conn
2022-01-30 21:58:57 +00:00
R4SAS
620b901473 Revert downgrading of wireguard and update wintun in windows installer (#865)
* Revert "Revert Wireguard update"

This reverts commit 03a5cce5bb.

Signed-off-by: R4SAS <r4sas@i2pmail.org>

* [win] update installer build script

Signed-off-by: R4SAS <r4sas@i2pmail.org>

* [appveyor] use golang 1.17.3 for building

Signed-off-by: R4SAS <r4sas@i2pmail.org>

* [appveyor] use golang 1.17.5 for building

Signed-off-by: R4SAS <r4sas@i2pmail.org>

* test script

Signed-off-by: R4SAS <r4sas@i2pmail.org>

* test msi and semver scripts

Signed-off-by: R4SAS <r4sas@i2pmail.org>

Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
2022-01-30 21:57:10 +00:00
Neil Alexander
09ea351682 Update build 2022-01-30 19:59:17 +00:00
Neil Alexander
6d92edd405 Move src/mobile into main repository (#864)
* Move `src/mobile` into main repository

* Update go.mod/go.sum

* Move to `contrib`, separate mobile build script
2022-01-30 19:48:32 +00:00
Neil Alexander
a4bdf3de32 Remove CAP_NET_RAW from systemd service unit, as it's not clear why it is there in the first place 2022-01-15 22:17:49 +00:00
Neil Alexander
408d381591 Set hostArchitectures in macOS .pkg installer 2021-12-06 11:19:58 +00:00
Alex Kotov
87e936195e Add some tests (#828)
* Add tests

* Add tests

* Add tests

* Add tests

* Fix code style

* Remove unnecessary tests
2021-11-04 08:05:53 +00:00
Neil Alexander
e4e58831bf Version 0.4.2 2021-11-03 22:16:53 +00:00
Neil Alexander
03a5cce5bb Revert Wireguard update
This reverts commit 5c19f3f88c.
2021-11-03 20:03:27 +00:00
Neil Alexander
1f64319712 Version 0.4.1 2021-11-03 17:53:35 +00:00
Neil Alexander
4f3117d81d Use network-online.target instead of network.target for systemd service unit 2021-11-03 17:40:06 +00:00
Neil Alexander
5c19f3f88c Update dependencies 2021-11-03 10:33:00 +00:00
Arceliar
feb02c485a Merge pull request #861 from yggdrasil-network/fix860
Fix panic in `address.GetKey()`
2021-11-02 17:30:50 -05:00
Neil Alexander
4859accbb0 Fix panic in address.GetKey() (fixes #860) 2021-11-02 18:03:16 +00:00
Neil Alexander
99227b60ce Update CI to use Go 1.17, produce Apple Silicon builds (closes #844) 2021-09-28 11:02:15 +01:00
Arceliar
f92d812f3c Merge pull request #822 from yggdrasil-network/sni
TLS Server Name Indication
2021-09-24 05:14:28 -05:00
Arceliar
6af9b61b15 Merge pull request #842 from Arceliar/mutex
Fix incorrect mutex use in ipv6rwc
2021-09-24 04:43:44 -05:00
Arceliar
f2d1eff8f6 Merge pull request #835 from kotovalexarian/test-and-refactor-proto-handler
Really tiny refactoring of "src/core"
2021-09-24 04:43:06 -05:00
Neil Alexander
9a1d1df85e Use newer Xcode image for macOS builds in CircleCI 2021-09-23 12:11:03 +01:00
Arceliar
e5d638ff4b better way to empty ipv6rwc buffer 2021-09-23 04:39:12 -05:00
Arceliar
86e5306eec fix race from mutex that wasn't held long enough 2021-09-23 04:35:31 -05:00
Arceliar
529a33034b gofmt to add new build comments 2021-09-23 04:34:58 -05:00
Paul Dee
1c7deb72db Align struct elements to byte boundaries: reduce memory footprint. (#834) 2021-09-21 21:19:40 +01:00
Fyodor Ustinov
52345a2de4 Check tun.config is not equal to nil before usage (#830)
We have to check tun.config is not nil before first use, not after.
2021-09-21 21:19:25 +01:00
Alex Kotov
571186ca77 Rename protohandler attributes 2021-09-03 01:45:30 +05:00
Alex Kotov
3c89781057 Align and reorder code for lesser diff 2021-09-01 07:58:11 +05:00
Alex Kotov
a5f2ba80a2 Organize code in "src/core/proto.go" 2021-09-01 07:50:03 +05:00
Alex Kotov
538ee13669 Add type core.AddHandlerFunc 2021-09-01 06:16:57 +05:00
Arceliar
3613614b41 Revert "Add IPReadWriteCloser interface"
This reverts commit ebe366ef3b.
2021-08-07 12:56:36 -05:00
Neil Alexander
ebe366ef3b Add IPReadWriteCloser interface 2021-08-07 10:17:21 +01:00
Alex Kotov
cbb6dc1b7d Split yggdrasilctl code into separate functions (refactoring) (#815)
* Move yggdrasilctl responses to separate functions

* Move yggdrasilctl request switch to separate function

* Add empty lines

* Create struct CmdLine for yggdrasilctl

* Move yggdrasilctl command line parsing to separate func

* Turn struct CmdLine into CmdLineEnv

* Rename func parseCmdLine to parseFlagsAndArgs

* Move yggdrasilctl endpoint setting logic into separate func

* Function to create yggdrasilctl CmdLineEnv

* Reorder code

* Move struct fields into lines

* Turn yggdrasilctl CmdLineEnv funcs to methods

* Move yggdrasilctl connection code to separate func

* Rename functions

* Move yggdrasilctl command line env to separate mod

* Move yggdrasilctl command line env to main mod

* Run goimports

Co-authored-by: Neil Alexander <neilalexander@users.noreply.github.com>
2021-08-02 22:47:38 +01:00
Neil Alexander
d1cd671bec Fix bug 2021-08-01 21:39:49 +01:00
Neil Alexander
bbdff033ce Update SNI code 2021-08-01 21:36:51 +01:00
Neil Alexander
f094cf34bf Set SNI by default if the peering URI contains a DNS name 2021-07-28 22:23:33 +01:00
Neil Alexander
d8df9755f2 Allow specifying TLS SNI with ?sni= in peering URI 2021-07-28 22:11:20 +01:00
Neil Alexander
b333c7d7f3 Merge pull request #813 from cofob/patch-1
Allow yggdrasil bind to ports <1024
2021-07-22 12:18:11 +01:00
cofob
6a0ddc20ef Allow yggdrasil bind to ports <1024 2021-07-21 17:57:59 +07:00
Neil Alexander
52309d094c Merge pull request #800 from yggdrasil-network/iprwc
Refactor PacketConn/ReadWriteCloser interfaces
2021-07-15 09:39:03 +01:00
Arceliar
747a2538d7 Merge pull request #801 from tdemin/develop
Preallocate memory when deriving address from key
2021-07-08 17:47:43 -05:00
Timur Demin
04ecdf6045 Preallocate memory when deriving address from key
This makes src/address.AddrForKey preallocate 32 bytes before starting
the address derivation. As benches in syg_go show, reallocating temp
takes 20% of the function runtime.
2021-07-08 16:04:43 +05:00
Arceliar
cd5383f7b7 fix core tests 2021-07-07 18:36:51 -05:00
Arceliar
3704ebf4cb fix debug rpcs and cleanup core.Close/core.Stop 2021-07-06 19:45:12 -05:00
Neil Alexander
e224c02d6d Revert "Add LocalAddr to complete net.PacketConn interface"
This reverts commit e4ce2c79a9.
2021-07-05 22:35:46 +01:00
Neil Alexander
e4ce2c79a9 Add LocalAddr to complete net.PacketConn interface 2021-07-05 22:26:09 +01:00
Arceliar
f990a56046 have the core wrap and export the underlying PacketConn, move IPv6 ReadWriteCloser wrapper logic to a separate package 2021-07-05 13:14:12 -05:00
90 changed files with 4593 additions and 3004 deletions

View File

@@ -1,230 +0,0 @@
# Golang CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-go/ for more details
version: 2.1
jobs:
lint:
docker:
- image: circleci/golang:1.16
steps:
- checkout
- run:
name: Run golangci-lint
command: |
go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.31.0
golangci-lint run
- run:
name: Run Go tests
command: |
go test ./...
build-linux:
docker:
- image: circleci/golang:1.16
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 --bare)' >> $BASH_ENV
echo 'export CIVERSIONRPM=$(sh contrib/semver/version.sh --bare | tr "-" ".")' >> $BASH_ENV
echo 'export CIBRANCH=$(echo $CIRCLE_BRANCH | tr -d "/")' >> $BASH_ENV
case "$CINAME" in \
"yggdrasil") (echo 'export CICONFLICTS=yggdrasil-develop' >> $BASH_ENV) ;; \
"yggdrasil-develop") (echo 'export CICONFLICTS=yggdrasil' >> $BASH_ENV) ;; \
*) (echo 'export CICONFLICTS="yggdrasil yggdrasil-develop"' >> $BASH_ENV) ;; \
esac
git config --global user.email "$(git log --format='%ae' HEAD -1)";
git config --global user.name "$(git log --format='%an' HEAD -1)";
- run:
name: Install RPM utilities
command: |
sudo apt-get update
sudo apt-get install -y rpm file
mkdir -p ~/rpmbuild/BUILD ~/rpmbuild/RPMS ~/rpmbuild/SOURCES ~/rpmbuild/SPECS ~/rpmbuild/SRPMS
- run:
name: Test debug builds
command: |
./build -d
test -f yggdrasil && test -f yggdrasilctl
- run:
name: Build for Linux (including Debian packages)
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=armel sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-armel && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-armel;
PKGARCH=arm64 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-arm64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-arm64;
mv *.deb /tmp/upload/
- run:
name: Build for Linux (RPM packages)
command: |
git clone https://github.com/yggdrasil-network/yggdrasil-package-rpm ~/rpmbuild/SPECS
cd ../ && tar -czvf ~/rpmbuild/SOURCES/v$CIVERSIONRPM --transform "s/project/yggdrasil-go-$CIBRANCH-$CIVERSIONRPM/" project
sed -i "s/yggdrasil-go/yggdrasil-go-$CIBRANCH/" ~/rpmbuild/SPECS/yggdrasil.spec
sed -i "s/^PKGNAME=yggdrasil/PKGNAME=yggdrasil-$CIBRANCH/" ~/rpmbuild/SPECS/yggdrasil.spec
sed -i "s/^Name\:.*/Name\: $CINAME/" ~/rpmbuild/SPECS/yggdrasil.spec
sed -i "s/^Version\:.*/Version\: $CIVERSIONRPM/" ~/rpmbuild/SPECS/yggdrasil.spec
sed -i "s/^Conflicts\:.*/Conflicts\: $CICONFLICTS/" ~/rpmbuild/SPECS/yggdrasil.spec
cat ~/rpmbuild/SPECS/yggdrasil.spec
GOARCH=amd64 rpmbuild -v --nodeps --target=x86_64 -ba ~/rpmbuild/SPECS/yggdrasil.spec
#GOARCH=386 rpmbuild -v --nodeps --target=i386 -bb ~/rpmbuild/SPECS/yggdrasil.spec
find ~/rpmbuild/RPMS/ -name '*.rpm' -exec mv {} /tmp/upload \;
find ~/rpmbuild/SRPMS/ -name '*.rpm' -exec mv {} /tmp/upload \;
- run:
name: Build for EdgeRouter and VyOS
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;
BUILDDIR_YGG=$CIRCLE_WORKING_DIRECTORY ./build-edgerouter-lite $CIRCLE_BRANCH;
BUILDDIR_YGG=$CIRCLE_WORKING_DIRECTORY ./build-vyos-i386 $CIRCLE_BRANCH
BUILDDIR_YGG=$CIRCLE_WORKING_DIRECTORY ./build-vyos-amd64 $CIRCLE_BRANCH
mv *.deb /tmp/upload;
- persist_to_workspace:
root: /tmp
paths:
- upload
build-macos:
macos:
xcode: "10.0.0"
working_directory: ~/go/src/github.com/yggdrasil-network/yggdrasil-go
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 --bare)' >> $BASH_ENV
echo 'export PATH=$PATH:/usr/local/go/bin:~/go/bin' >> $BASH_ENV
git config --global user.email "$(git log --format='%ae' HEAD -1)";
git config --global user.name "$(git log --format='%an' HEAD -1)";
echo -e "Host *\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config
- run:
name: Install Go 1.16
command: |
cd /tmp
curl -LO https://dl.google.com/go/go1.16.darwin-amd64.pkg
sudo installer -pkg /tmp/go1.16.darwin-amd64.pkg -target /
#- run:
# name: Install Gomobile
# command: |
# GO111MODULE=off go get golang.org/x/mobile/cmd/gomobile
# gomobile init
- run:
name: Build for macOS
command: |
GO111MODULE=on GOOS=darwin GOARCH=amd64 ./build
cp yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-amd64
cp yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-darwin-amd64;
- run:
name: Build for macOS (.pkg format)
command: |
PKGARCH=amd64 sh contrib/macos/create-pkg.sh
mv *.pkg /tmp/upload/
#- run:
# name: Build framework for iOS (.framework format)
# command: |
# sudo GO111MODULE=off go get -v github.com/yggdrasil-network/yggdrasil-go/cmd/...
# sudo GO111MODULE=off go get -v github.com/yggdrasil-network/yggdrasil-go/src/...
# GO111MODULE=off ./build -i
# mv *.framework /tmp/upload
- persist_to_workspace:
root: /tmp
paths:
- upload
build-other:
docker:
- image: circleci/golang:1.16
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 --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 OpenBSD
command: |
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: |
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 Windows
command: |
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;
- persist_to_workspace:
root: /tmp
paths:
- upload
upload:
machine: true
steps:
- attach_workspace:
at: /tmp
- store_artifacts:
path: /tmp/upload
destination: /
workflows:
version: 2.1
build:
jobs:
- lint
- build-linux
- build-macos
- build-other
- upload:
requires:
- build-linux
- build-macos
- build-other

131
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,131 @@
name: Yggdrasil
on:
push:
pull_request:
release:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v3
with:
go-version: 1.19
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
args: --issues-exit-code=1
codeql:
name: Analyse
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: go
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
build-linux:
strategy:
fail-fast: false
matrix:
goversion: ["1.19", "1.20"]
name: Build & Test (Linux, Go ${{ matrix.goversion }})
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goversion }}
- name: Build Yggdrasil
run: go build -v ./...
- name: Unit tests
run: go test -v ./...
build-windows:
strategy:
fail-fast: false
matrix:
goversion: ["1.19", "1.20"]
name: Build & Test (Windows, Go ${{ matrix.goversion }})
needs: [lint]
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goversion }}
- name: Build Yggdrasil
run: go build -v ./...
- name: Unit tests
run: go test -v ./...
build-macos:
strategy:
fail-fast: false
matrix:
goversion: ["1.19", "1.20"]
name: Build & Test (macOS, Go ${{ matrix.goversion }})
needs: [lint]
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.goversion }}
- name: Build Yggdrasil
run: go build -v ./...
- name: Unit tests
run: go test -v ./...
tests-ok:
name: All tests passed
needs: [lint, codeql, build-linux, build-windows, build-macos]
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
steps:
- name: Check all tests passed
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

137
.github/workflows/pkg.yml vendored Normal file
View File

@@ -0,0 +1,137 @@
name: Packages
on:
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-packages-debian:
strategy:
fail-fast: false
matrix:
pkgarch: ["amd64", "i386", "mips", "mipsel", "armhf", "armel", "arm64"]
name: Package (Debian, ${{ matrix.pkgarch }})
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: "1.20"
- name: Build package
env:
PKGARCH: ${{ matrix.pkgarch }}
run: sh contrib/deb/generate.sh
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: Debian package (${{ matrix.pkgarch }})
path: "*.deb"
if-no-files-found: error
build-packages-macos:
strategy:
fail-fast: false
matrix:
pkgarch: ["amd64", "arm64"]
name: Package (macOS, ${{ matrix.pkgarch }})
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: "1.20"
- name: Build package
env:
PKGARCH: ${{ matrix.pkgarch }}
run: sh contrib/macos/create-pkg.sh
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: macOS package (${{ matrix.pkgarch }})
path: "*.pkg"
if-no-files-found: error
build-packages-windows:
strategy:
fail-fast: false
matrix:
pkgarch: ["x64", "x86", "arm", "arm64"]
name: Package (Windows, ${{ matrix.pkgarch }})
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: "1.20"
- name: Build package
run: sh contrib/msi/build-msi.sh ${{ matrix.pkgarch }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: Windows package (${{ matrix.pkgarch }})
path: "*.msi"
if-no-files-found: error
build-packages-router:
strategy:
fail-fast: false
matrix:
pkgarch: ["edgerouter-x", "edgerouter-lite", "vyos-amd64", "vyos-i386"]
name: Package (Router, ${{ matrix.pkgarch }})
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
path: yggdrasil
- uses: actions/checkout@v3
with:
repository: neilalexander/vyatta-yggdrasil
path: vyatta-yggdrasil
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: "1.20"
- name: Build package
env:
BUILDDIR_YGG: /home/runner/work/yggdrasil-go/yggdrasil-go/yggdrasil
run: cd /home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil && ./build-${{ matrix.pkgarch }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: Router package (${{ matrix.pkgarch }})
path: "/home/runner/work/yggdrasil-go/yggdrasil-go/vyatta-yggdrasil/*.deb"
if-no-files-found: error

View File

@@ -1,4 +1,5 @@
# 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/)
@@ -25,63 +26,173 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- in case of vulnerabilities.
-->
## [0.4.0] - 2021-07-04
## [0.4.7] - 2022-11-20
### Added
- New routing scheme, which is backwards incompatible with previous versions of Yggdrasil
- The wire protocol version number, exchanged as part of the peer setup handshake, has been increased to 0.4
- Nodes running this new version **will not** be able to peer with earlier versions of Yggdrasil
- Please note that **the network may be temporarily unstable** while infrastructure is being upgraded to the new release
- TLS connections now use public key pinning
- If no public key was already pinned, then the public key received as part of the TLS handshake is pinned to the connection
- The public key received as part of the handshake is checked against the pinned keys, and if no match is found, the connection is rejected
- Dropped outbound peerings will now try to reconnect after a single second, rather than waiting up to 60 seconds for the normal peer timer
### Changed
- Session encryption keys are now rotated at most once per minute, which reduces CPU usage and improves throughput on fast low latency links
- Buffers are now reused in the session encryption handler, which improves session throughput and reduces memory allocations
- Buffers are now reused in the router for DHT and path traffic, which improves overall routing throughput and reduces memory allocations
### Fixed
- A bug in the admin socket where requests fail unless `arguments` is specified has been fixed
- Certificates on TLS listeners will no longer expire after a year
- The `-address` and `-subnet` command line options now return a useful warning when no configuration is specified
## [0.4.6] - 2022-10-25
### Added
- Support for prioritising multiple peerings to the same node has been added, useful for nodes with multiple network interfaces
- The priority can be configured by specifying `?priority=X` in a `Peers` or `Listen` URI, or by specifying `Priority` within a `MulticastInterfaces` configuration entry
- Priorities are values between 0 and 254 (default is 0), lower numbers are prioritised and nodes will automatically negotiate the higher of the two values
### Changed
- On Linux, `SO_REUSEADDR` is now used on the multicast port instead of `SO_REUSEPORT`, which should allow processes running under different users to run simultaneously
### Fixed
- Adding peers using the `InterfacePeers` configuration option should now work correctly again
- Multiple connections from the same remote IP address will no longer be incorrectly dropped
- The admin socket will no longer incorrectly claim TCP connections as TLS
- A panic that could occur when calling `GetPeers` while a peering link is being set up has been fixed
## [0.4.5] - 2022-10-15
### Added
- Support for peering over UNIX sockets is now available, by configuring `Listen` and peering URIs in the `unix:///path/to/socket.sock` format
### Changed
- `yggdrasilctl` has been refactored and now has cleaner output
- It is now possible to `addPeer` and `removePeer` using the admin socket again
- The `getSessions` admin socket call reports number of bytes received and transmitted again
- The link setup code has been refactored, making it easier to support new peering types in the future
- Yggdrasil now maintains configuration internally, rather than relying on a shared and potentially mutable structure
### Fixed
- Tracking information about expired root nodes has been fixed, which should hopefully resolve issues with reparenting and connection failures when the root node disappears
- A bug in the mobile framework code which caused a crash on Android when multicast failed to set up has been fixed
- Yggdrasil should now shut down gracefully and clean up correctly when running as a Windows service
## [0.4.4] - 2022-07-07
### Fixed
- ICMPv6 "Packet Too Big" payload size has been increased, which should fix Path MTU Discovery (PMTUD) when two nodes have different `IfMTU` values configured
- A crash has been fixed when handling debug packet responses
- `yggdrasilctl getSelf` should now report coordinates correctly again
### Changed
- Go 1.17 is now required to build Yggdrasil
## [0.4.3] - 2022-02-06
### Added
- `bytes_sent`, `bytes_recvd` and `uptime` have been added to `getPeers`
- Clearer logging when connections are rejected due to incompatible peer versions
### Fixed
- Latency-based parent selection tiebreak is now reliable on platforms even with low timer resolution
- Tree distance calculation offsets have been corrected
## [0.4.2] - 2021-11-03
### Fixed
- Reverted a dependency update which resulted in problems building with Go 1.16 and running on Windows
## [0.4.1] - 2021-11-03
### Added
- TLS peerings now support Server Name Indication (SNI)
- The SNI is sent automatically if the peering URI contains a DNS name
- A custom SNI can be specified by adding the `?sni=domain.com` parameter to the peering URI
- A new `ipv6rwc` API package now implements the IPv6-specific logic separate from the `tun` package
### Fixed
- A crash when calculating the partial public key for very high IPv6 addresses has been fixed
- A crash due to a concurrent map write has been fixed
- A crash due to missing TUN configuration has been fixed
- A race condition in the keystore code has been fixed
## [0.4.0] - 2021-07-04
### Added
- New routing scheme, which is backwards incompatible with previous versions of Yggdrasil
- The wire protocol version number, exchanged as part of the peer setup handshake, has been increased to 0.4
- Nodes running this new version **will not** be able to peer with earlier versions of Yggdrasil
- Please note that **the network may be temporarily unstable** while infrastructure is being upgraded to the new release
- TLS connections now use public key pinning
- If no public key was already pinned, then the public key received as part of the TLS handshake is pinned to the connection
- The public key received as part of the handshake is checked against the pinned keys, and if no match is found, the connection is rejected
### Changed
- IP addresses are now derived from ed25519 public (signing) keys
- Previously, addresses were derived from a hash of X25519 (Diffie-Hellman) keys
- Importantly, this means that **all internal IPv6 addresses will change with this release** — this will affect anyone running public services or relying on Yggdrasil for remote access
- Previously, addresses were derived from a hash of X25519 (Diffie-Hellman) keys
- Importantly, this means that **all internal IPv6 addresses will change with this release** — this will affect anyone running public services or relying on Yggdrasil for remote access
- It is now recommended to peer over TLS
- Link-local peers from multicast peer discovery will now connect over TLS, with the key from the multicast beacon pinned to the connection
- `socks://` peers now expect the destination endpoint to be a `tls://` listener, instead of a `tcp://` listener
- Link-local peers from multicast peer discovery will now connect over TLS, with the key from the multicast beacon pinned to the connection
- `socks://` peers now expect the destination endpoint to be a `tls://` listener, instead of a `tcp://` listener
- Multicast peer discovery is now more configurable
- There are separate configuration options to control if beacons are sent, what port to listen on for incoming connections (if sending beacons), and whether or not to listen for beacons from other nodes (and open connections when receiving a beacon)
- Each configuration entry in the list specifies a regular expression to match against interface names
- If an interface matches multiple regex in the list, it will use the settings for the first entry in the list that it matches with
- There are separate configuration options to control if beacons are sent, what port to listen on for incoming connections (if sending beacons), and whether or not to listen for beacons from other nodes (and open connections when receiving a beacon)
- Each configuration entry in the list specifies a regular expression to match against interface names
- If an interface matches multiple regex in the list, it will use the settings for the first entry in the list that it matches with
- The session and routing code has been entirely redesigned and rewritten
- This is still an early work-in-progress, so the code hasn't been as well tested or optimized as the old code base — please bear with us for these next few releases as we work through any bugs or issues
- Generally speaking, we expect to see reduced bandwidth use and improved reliability with the new design, especially in cases where nodes move around or change peerings frequently
- Cryptographic sessions no longer use a single shared (ephemeral) secret for the entire life of the session. Keys are now rotated regularly for ongoing sessions (currently rotated at least once per round trip exchange of traffic, subject to change in future releases)
- Source routing has been added. Under normal circumstances, this is what is used to forward session traffic (e.g. the user's IPv6 traffic)
- DHT-based routing has been added. This is used when the sender does not know a source route to the destination. Forwarding through the DHT is less efficient, but the only information that it requires the sender to know is the destination node's (static) key. This is primarily used during the key exchange at session setup, or as a temporary fallback when a source route fails due to changes in the network
- The new DHT design is no longer RPC-based, does not support crawling and does not inherently allow nodes to look up the owner of an arbitrary key. Responding to lookups is now implemented at the application level and a response is only sent if the destination key matches the node's `/128` IP or `/64` prefix
- The greedy routing scheme, used to forward all traffic in previous releases, is now only used for protocol traffic (i.e. DHT setup and source route discovery)
- The routing logic now lives in a [standalone library](https://github.com/Arceliar/ironwood). You are encouraged **not** to use it, as it's still considered pre-alpha, but it's available for those who want to experiment with the new routing algorithm in other contexts
- Session MTUs may be slightly lower now, in order to accommodate large packet headers if required
- This is still an early work-in-progress, so the code hasn't been as well tested or optimized as the old code base — please bear with us for these next few releases as we work through any bugs or issues
- Generally speaking, we expect to see reduced bandwidth use and improved reliability with the new design, especially in cases where nodes move around or change peerings frequently
- Cryptographic sessions no longer use a single shared (ephemeral) secret for the entire life of the session. Keys are now rotated regularly for ongoing sessions (currently rotated at least once per round trip exchange of traffic, subject to change in future releases)
- Source routing has been added. Under normal circumstances, this is what is used to forward session traffic (e.g. the user's IPv6 traffic)
- DHT-based routing has been added. This is used when the sender does not know a source route to the destination. Forwarding through the DHT is less efficient, but the only information that it requires the sender to know is the destination node's (static) key. This is primarily used during the key exchange at session setup, or as a temporary fallback when a source route fails due to changes in the network
- The new DHT design is no longer RPC-based, does not support crawling and does not inherently allow nodes to look up the owner of an arbitrary key. Responding to lookups is now implemented at the application level and a response is only sent if the destination key matches the node's `/128` IP or `/64` prefix
- The greedy routing scheme, used to forward all traffic in previous releases, is now only used for protocol traffic (i.e. DHT setup and source route discovery)
- The routing logic now lives in a [standalone library](https://github.com/Arceliar/ironwood). You are encouraged **not** to use it, as it's still considered pre-alpha, but it's available for those who want to experiment with the new routing algorithm in other contexts
- Session MTUs may be slightly lower now, in order to accommodate large packet headers if required
- Many of the admin functions available over `yggdrasilctl` have been changed or removed as part of rewrites to the code
- Several remote `debug` functions have been added temporarily, to allow for crawling and census gathering during the transition to the new version, but we intend to remove this at some point in the (possibly distant) future
- The list of available functions will likely be expanded in future releases
- Several remote `debug` functions have been added temporarily, to allow for crawling and census gathering during the transition to the new version, but we intend to remove this at some point in the (possibly distant) future
- The list of available functions will likely be expanded in future releases
- The configuration file format has been updated in response to the changed/removed features
### Removed
- Tunnel routing (a.k.a. crypto-key routing or "CKR") has been removed
- It was far too easy to accidentally break routing altogether by capturing the route to peers with the TUN adapter
- We recommend tunnelling an existing standard over Yggdrasil instead (e.g. `ip6gre`, `ip6gretap` or other similar encapsulations, using Yggdrasil IPv6 addresses as the tunnel endpoints)
- All `TunnelRouting` configuration options will no longer take effect
- It was far too easy to accidentally break routing altogether by capturing the route to peers with the TUN adapter
- We recommend tunnelling an existing standard over Yggdrasil instead (e.g. `ip6gre`, `ip6gretap` or other similar encapsulations, using Yggdrasil IPv6 addresses as the tunnel endpoints)
- All `TunnelRouting` configuration options will no longer take effect
- Session firewall has been removed
- This was never a true firewall — it didn't behave like a stateful IP firewall, often allowed return traffic unexpectedly and was simply a way to prevent a node from being flooded with unwanted sessions, so the name could be misleading and usually lead to a false sense of security
- Due to design changes, the new code needs to address the possible memory exhaustion attacks in other ways and a single configurable list no longer makes sense
- Users who want a firewall or other packet filter mechansim should configure something supported by their OS instead (e.g. `ip6tables`)
- All `SessionFirewall` configuration options will no longer take effect
- This was never a true firewall — it didn't behave like a stateful IP firewall, often allowed return traffic unexpectedly and was simply a way to prevent a node from being flooded with unwanted sessions, so the name could be misleading and usually lead to a false sense of security
- Due to design changes, the new code needs to address the possible memory exhaustion attacks in other ways and a single configurable list no longer makes sense
- Users who want a firewall or other packet filter mechansim should configure something supported by their OS instead (e.g. `ip6tables`)
- All `SessionFirewall` configuration options will no longer take effect
- `SIGHUP` handling to reload the configuration at runtime has been removed
- It was not obvious which parts of the configuration could be reloaded at runtime, and which required the application to be killed and restarted to take effect
- Reloading the config without restarting was also a delicate and bug-prone process, and was distracting from more important developments
- `SIGHUP` will be handled normally (i.e. by exiting)
- It was not obvious which parts of the configuration could be reloaded at runtime, and which required the application to be killed and restarted to take effect
- Reloading the config without restarting was also a delicate and bug-prone process, and was distracting from more important developments
- `SIGHUP` will be handled normally (i.e. by exiting)
- `cmd/yggrasilsim` has been removed, and is unlikely to return to this repository
## [0.3.16] - 2021-03-18
### Added
- New simulation code under `cmd/yggdrasilsim` (work-in-progress)
### Changed
- Multi-threading in the switch
- Swich lookups happen independently for each (incoming) peer connection, instead of being funneled to a single dedicated switch worker
- Packets are queued for each (outgoing) peer connection, instead of being handled by a single dedicated switch worker
@@ -97,13 +208,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Upgrade build to Go 1.16
### Fixed
- Fixed a bug where the connection listener could exit prematurely due to resoruce exhaustion (if e.g. too many connections were opened)
- Fixed DefaultIfName for OpenBSD (`/dev/tun0` -> `tun0`)
- Fixed an issue where a peer could sometimes never be added to the switch
- Fixed a goroutine leak that could occur if a peer with an open connection continued to spam additional connection attempts
## [0.3.15] - 2020-09-27
### Added
- Support for pinning remote public keys in peering strings has been added, e.g.
- By signing public key: `tcp://host:port?ed25519=key`
- By encryption public key: `tcp://host:port?curve25519=key`
@@ -113,25 +227,32 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added support for SOCKS proxy authentication, e.g. `socks://user@password:host/...`
### Fixed
- Some bugs in the multicast code that could cause unnecessary CPU usage have been fixed
- A possible multicast deadlock on macOS when enumerating interfaces has been fixed
- A deadlock in the connection code has been fixed
- Updated HJSON dependency that caused some build problems
### Changed
- `DisconnectPeer` and `RemovePeer` have been separated and implemented properly now
- Less nodes are stored in the DHT now, reducing ambient network traffic and possible instability
- Default config file for FreeBSD is now at `/usr/local/etc/yggdrasil.conf` instead of `/etc/yggdrasil.conf`
## [0.3.14] - 2020-03-28
### Fixed
- Fixes a memory leak that may occur if packets are incorrectly never removed from a switch queue
### Changed
- Make DHT searches a bit more reliable by tracking the 16 most recently visited nodes
## [0.3.13] - 2020-02-21
### Added
- Support for the Wireguard TUN driver, which now replaces Water and provides far better support and performance on Windows
- Windows `.msi` installer files are now supported (bundling the Wireguard TUN driver)
- NodeInfo code is now actorised, should be more reliable
@@ -139,6 +260,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- The Yggdrasil API now supports dialing a remote node using the public key instead of the Node ID
### Changed
- The `-loglevel` command line parameter is now cumulative and automatically includes all levels below the one specified
- DHT search code has been significantly simplified and processes rumoured nodes in parallel, speeding up search time
- DHT search results are now sorted
@@ -146,26 +268,32 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- The Yggdrasil API now returns public keys instead of node IDs when querying for local and remote addresses
### Fixed
- The multicast code no longer panics when shutting down the node
- A potential OOB error when calculating IPv4 flow labels (when tunnel routing is enabled) has been fixed
- A bug resulting in incorrect idle notifications in the switch should now be fixed
- MTUs are now using a common datatype throughout the codebase
### Removed
- TAP mode has been removed entirely, since it is no longer supported with the Wireguard TUN package. Please note that if you are using TAP mode, you may need to revise your config!
- NetBSD support has been removed until the Wireguard TUN package supports NetBSD
## [0.3.12] - 2019-11-24
### Added
- New API functions `SetMaximumSessionMTU` and `GetMaximumSessionMTU`
- New command line parameters `-address` and `-subnet` for getting the address/subnet from the config file, for use with `-useconffile` or `-useconf`
- A warning is now produced in the Yggdrasil output at startup when the MTU in the config is invalid or has been adjusted for some reason
### Changed
- On Linux, outgoing `InterfacePeers` connections now use `SO_BINDTODEVICE` to prefer an outgoing interface
- The `genkeys` utility is now in `cmd` rather than `misc`
### Fixed
- A data race condition has been fixed when updating session coordinates
- A crash when shutting down when no multicast interfaces are configured has been fixed
- A deadlock when calling `AddPeer` multiple times has been fixed
@@ -174,10 +302,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- The MTU calculation now correctly accounts for ethernet headers when running in TAP mode
## [0.3.11] - 2019-10-25
### Added
- Support for TLS listeners and peers has been added, allowing the use of `tls://host:port` in `Peers`, `InterfacePeers` and `Listen` configuration settings - this allows hiding Yggdrasil peerings inside regular TLS connections
### Changed
- Go 1.13 or later is now required for building Yggdrasil
- Some exported API functions have been updated to work with standard Go interfaces:
- `net.Conn` instead of `yggdrasil.Conn`
@@ -187,27 +318,35 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Multicast module reloading behaviour has been improved
### Fixed
- An incorrectly held mutex in the crypto-key routing code has been fixed
- Multicast module no longer opens a listener socket if no multicast interfaces are configured
## [0.3.10] - 2019-10-10
### Added
- The core library now includes several unit tests for peering and `yggdrasil.Conn` connections
### Changed
- On recent Linux kernels, Yggdrasil will now set the `tcp_congestion_control` algorithm used for its own TCP sockets to [BBR](https://github.com/google/bbr), which reduces latency under load
- The systemd service configuration in `contrib` (and, by extension, some of our packages) now attempts to load the `tun` module, in case TUN/TAP support is available but not loaded, and it restricts Yggdrasil to the `CAP_NET_ADMIN` capability for managing the TUN/TAP adapter, rather than letting it do whatever the (typically `root`) user can do
### Fixed
- The `yggdrasil.Conn.RemoteAddr()` function no longer blocks, fixing a deadlock when CKR is used while under heavy load
## [0.3.9] - 2019-09-27
### Added
- Yggdrasil will now complain more verbosely when a peer URI is incorrectly formatted
- Soft-shutdown methods have been added, allowing a node to shut down gracefully when terminated
- New multicast interval logic which sends multicast beacons more often when Yggdrasil is first started to increase the chance of finding nearby nodes quickly after startup
### Changed
- The switch now buffers packets more eagerly in an attempt to give the best link a chance to send, which appears to reduce packet reordering when crossing aggregate sets of peerings
- Substantial amounts of the codebase have been refactored to use the actor model, which should substantially reduce the chance of deadlocks
- Nonce tracking in sessions has been modified so that memory usage is reduced whilst still only allowing duplicate packets within a small window
@@ -216,6 +355,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- The maximum queue size is now managed exclusively by the switch rather than by the core
### Fixed
- The broken `hjson-go` dependency which affected builds of the previous version has now been resolved in the module manifest
- Some minor memory leaks in the switch have been fixed, which improves memory usage on mobile builds
- A memory leak in the add-peer loop has been fixed
@@ -227,10 +367,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- A panic which could occur when the TUN/TAP interface reads an undersized/corrupted packet has been fixed
### Removed
- A number of legacy debug functions have now been removed and a number of exported API functions are now better documented
## [0.3.8] - 2019-08-21
### Changed
- Yggdrasil can now send multiple packets from the switch at once, which results in improved throughput with smaller packets or lower MTUs
- Performance has been slightly improved by not allocating cancellations where not necessary
- Crypto-key routing options have been renamed for clarity
@@ -243,20 +386,25 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- New nonce tracking should help to reduce the number of packets dropped as a result of multiple/aggregate paths or congestion control in the switch
### Fixed
- A deadlock was fixed in the session code which could result in Yggdrasil failing to pass traffic after some time
### Security
- Address verification was not strict enough, which could result in a malicious session sending traffic with unexpected or spoofed source or destination addresses which Yggdrasil could fail to reject
- Versions `0.3.6` and `0.3.7` are vulnerable - users of these versions should upgrade as soon as possible
- Versions `0.3.5` and earlier are not affected
## [0.3.7] - 2019-08-14
### Changed
- The switch should now forward packets along a single path more consistently in cases where congestion is low and multiple equal-length paths exist, which should improve stability and result in fewer out-of-order packets
- Sessions should now be more tolerant of out-of-order packets, by replacing a bitmask with a variable sized heap+map structure to track recently received nonces, which should reduce the number of packets dropped due to reordering when multiple paths are used or multiple independent flows are transmitted through the same session
- The admin socket can no longer return a dotfile representation of the known parts of the network, this could be rebuilt by clients using information from `getSwitchPeers`,`getDHT` and `getSessions`
### Fixed
- A number of significant performance regressions introduced in version 0.3.6 have been fixed, resulting in better performance
- Flow labels are now used to prioritise traffic flows again correctly
- In low-traffic scenarios where there are multiple peerings between a pair of nodes, Yggdrasil now prefers the most active peering instead of the least active, helping to reduce packet reordering
@@ -270,13 +418,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- A number of minor allocation and pointer fixes
## [0.3.6] - 2019-08-03
### Added
- Yggdrasil now has a public API with interfaces such as `yggdrasil.ConnDialer`, `yggdrasil.ConnListener` and `yggdrasil.Conn` for using Yggdrasil as a transport directly within applications
- Session gatekeeper functions, part of the API, which can be used to control whether to allow or reject incoming or outgoing sessions dynamically (compared to the previous fixed whitelist/blacklist approach)
- Support for logging to files or syslog (where supported)
- Platform defaults now include the ability to set sane defaults for multicast interfaces
### Changed
- Following a massive refactoring exercise, Yggdrasil's codebase has now been broken out into modules
- Core node functionality in the `yggdrasil` package with a public API
- This allows Yggdrasil to be integrated directly into other applications and used as a transport
@@ -289,6 +440,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Upstream dependency references have been updated, which includes a number of fixes in the Water library
### Fixed
- Multicast discovery is no longer disabled if the nominated interfaces aren't available on the system yet, e.g. during boot
- Multicast interfaces are now re-evaluated more frequently so that Yggdrasil doesn't need to be restarted to use interfaces that have become available since startup
- Admin socket error cases are now handled better
@@ -303,12 +455,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Lots of small bug tweaks and clean-ups throughout the codebase
## [0.3.5] - 2019-03-13
### Fixed
- The `AllowedEncryptionPublicKeys` option has now been fixed to handle incoming connections properly and no longer blocks outgoing connections (this was broken in v0.3.4)
- Multicast TCP listeners will now be stopped correctly when the link-local address on the interface changes or disappears altogether
## [0.3.4] - 2019-03-12
### Added
- Support for multiple listeners (although currently only TCP listeners are supported)
- New multicast behaviour where each multicast interface is given its own link-local listener and does not depend on the `Listen` configuration
- Blocking detection in the switch to avoid parenting a blocked peer
@@ -319,6 +475,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Added `LinkLocalTCPPort` option for controlling the port number that link-local TCP listeners will listen on by default when setting up `MulticastInterfaces` (a node restart is currently required for changes to `LinkLocalTCPPort` to take effect - it cannot be updated by reloading config during runtime)
### Changed
- The `Listen` configuration statement is now an array instead of a string
- The `Listen` configuration statement should now conform to the same formatting as peers with the protocol prefix, e.g. `tcp://[::]:0`
- Session workers are now non-blocking
@@ -327,6 +484,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Peer forwarding is now prioritised instead of randomised
### Fixed
- Admin socket `getTunTap` call now returns properly instead of claiming no interface is enabled in all cases
- Handling of `getRoutes` etc in `yggdrasilctl` is now working
- Local interface names are no longer leaked in multicast packets
@@ -334,7 +492,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Yggdrasil now correctly responds to multicast interfaces going up and down during runtime
## [0.3.3] - 2019-02-18
### Added
- Dynamic reconfiguration, which allows reloading the configuration file to make changes during runtime by sending a `SIGHUP` signal (note: this only works with `-useconffile` and not `-useconf` and currently reconfiguring TUN/TAP is not supported)
- Support for building Yggdrasil as an iOS or Android framework if the appropriate tools (e.g. `gomobile`/`gobind` + SDKs) are available
- Connection contexts used for TCP connections which allow more exotic socket options to be set, e.g.
@@ -343,26 +503,33 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Flexible logging support, which allows for logging at different levels of verbosity
### Changed
- Switch changes to improve parent selection
- Node configuration is now stored centrally, rather than having fragments/copies distributed at startup time
- Significant refactoring in various areas, including for link types (TCP, AWDL etc), generic streams and adapters
- macOS builds through CircleCI are now 64-bit only
### Fixed
- Simplified `systemd` service now in `contrib`
### Removed
- `ReadTimeout` option is now deprecated
## [0.3.2] - 2018-12-26
### Added
- The admin socket is now multithreaded, greatly improving performance of the crawler and allowing concurrent lookups to take place
- The ability to hide NodeInfo defaults through either setting the `NodeInfoPrivacy` option or through setting individual `NodeInfo` attributes to `null`
### Changed
- The `armhf` build now targets ARMv6 instead of ARMv7, adding support for Raspberry Pi Zero and other older models, amongst others
### Fixed
- DHT entries are now populated using a copy in memory to fix various potential DHT bugs
- DHT traffic should now throttle back exponentially to reduce idle traffic
- Adjust how nodes are inserted into the DHT which should help to reduce some incorrect DHT traffic
@@ -370,7 +537,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- In TUN mode, ICMPv6 packets are now ignored whereas they were incorrectly processed before
## [0.3.1] - 2018-12-17
### Added
- Build name and version is now imprinted onto the binaries if available/specified during build
- Ability to disable admin socket with `AdminListen: none`
- `AF_UNIX` domain sockets for the admin socket
@@ -380,18 +549,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Adds flags `-c`, `-l` and `-t` to `build` script for specifying `GCFLAGS`, `LDFLAGS` or whether to keep symbol/DWARF tables
### Changed
- Default `AdminListen` in newly generated config is now `unix:///var/run/yggdrasil.sock`
- Formatting of `getRoutes` in the admin socket has been improved
- Debian package now adds `yggdrasil` group to assist with `AF_UNIX` admin socket permissions
- Crypto, address and other utility code refactored into separate Go packages
### Fixed
- Switch peer convergence is now much faster again (previously it was taking up to a minute once the peering was established)
- `yggdrasilctl` is now less prone to crashing when parameters are specified incorrectly
- Panic fixed when `Peers` or `InterfacePeers` was commented out
## [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
@@ -404,6 +577,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `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
@@ -413,12 +587,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `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
@@ -426,81 +603,106 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- 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).
@@ -508,18 +710,24 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- 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.

View File

@@ -1,7 +1,6 @@
# Yggdrasil
[![CircleCI](https://circleci.com/gh/yggdrasil-network/yggdrasil-go.svg?style=shield&circle-token=:circle-token
)](https://circleci.com/gh/yggdrasil-network/yggdrasil-go)
[![Build status](https://github.com/yggdrasil-network/yggdrasil-go/actions/workflows/ci.yml/badge.svg)](https://github.com/yggdrasil-network/yggdrasil-go/actions/workflows/ci.yml)
## Introduction
@@ -16,7 +15,7 @@ connectivity - it also works over IPv4.
Yggdrasil works on a number of platforms, including Linux, macOS, Ubiquiti
EdgeRouter, VyOS, Windows, FreeBSD, OpenBSD and OpenWrt.
Please see our [Installation](https://yggdrasil-network.github.io/installation.html)
Please see our [Installation](https://yggdrasil-network.github.io/installation.html)
page for more information. You may also find other platform-specific wrappers, scripts
or tools in the `contrib` folder.
@@ -25,7 +24,7 @@ or tools in the `contrib` folder.
If you want to build from source, as opposed to installing one of the pre-built
packages:
1. Install [Go](https://golang.org) (requires Go 1.16 or later)
1. Install [Go](https://golang.org) (requires Go 1.17 or later)
2. Clone this repository
2. Run `./build`
@@ -57,6 +56,7 @@ other configuration such as listen addresses or multicast addresses, etc.
### Run Yggdrasil
To run with the generated static configuration:
```
./yggdrasil -useconffile /path/to/yggdrasil.conf
```

View File

@@ -1,20 +0,0 @@
version: '{build}'
pull_requests:
do_not_increment_build_number: true
os: Visual Studio 2019
shallow_clone: false
environment:
MSYS2_PATH_TYPE: inherit
CHERE_INVOKING: enabled_from_arguments
build_script:
- cmd: >-
cd %APPVEYOR_BUILD_FOLDER%
- c:\msys64\usr\bin\bash -lc "./contrib/msi/build-msi.sh x64"
- c:\msys64\usr\bin\bash -lc "./contrib/msi/build-msi.sh x86"
test: off
artifacts:
- path: '*.msi'

32
build
View File

@@ -9,13 +9,11 @@ PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)}
LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER"
ARGS="-v"
while getopts "uaitc:l:dro:p" option
while getopts "utc:l:dro:p" option
do
case "$option"
in
u) UPX=true;;
i) IOS=true;;
a) ANDROID=true;;
t) TABLES=true;;
c) GCFLAGS="$GCFLAGS $OPTARG";;
l) LDFLAGS="$LDFLAGS $OPTARG";;
@@ -30,25 +28,11 @@ if [ -z $TABLES ] && [ -z $DEBUG ]; then
LDFLAGS="$LDFLAGS -s -w"
fi
if [ $IOS ]; then
echo "Building framework for iOS"
go get golang.org/x/mobile/bind
gomobile bind -target ios -tags mobile -o Yggdrasil.framework -ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \
github.com/yggdrasil-network/yggdrasil-extras/src/mobile \
github.com/yggdrasil-network/yggdrasil-go/src/config
elif [ $ANDROID ]; then
echo "Building aar for Android"
go get golang.org/x/mobile/bind
gomobile bind -target android -tags mobile -o yggdrasil.aar -ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \
github.com/yggdrasil-network/yggdrasil-extras/src/mobile \
github.com/yggdrasil-network/yggdrasil-go/src/config
else
for CMD in yggdrasil yggdrasilctl ; do
echo "Building: $CMD"
go build $ARGS -ldflags="$LDFLAGS" -gcflags="$GCFLAGS" ./cmd/$CMD
for CMD in yggdrasil yggdrasilctl ; do
echo "Building: $CMD"
go build $ARGS -ldflags="$LDFLAGS" -gcflags="$GCFLAGS" ./cmd/$CMD
if [ $UPX ]; then
upx --brute $CMD
fi
done
fi
if [ $UPX ]; then
upx --brute $CMD
fi
done

View File

@@ -1,5 +1,4 @@
/*
This file generates crypto keys.
It prints out a new set of keys each time if finds a "better" one.
By default, "better" means a higher NodeID (-> higher IP address).
@@ -8,7 +7,6 @@ This is because the IP address format can compress leading 1s in the address, to
If run with the "-sig" flag, it generates signing keys instead.
A "better" signing key means one with a higher TreeID.
This only matters if it's high enough to make you the root of the tree.
*/
package main

View File

@@ -1,158 +1,287 @@
package main
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"golang.org/x/text/encoding/unicode"
"github.com/gologme/log"
gsyslog "github.com/hashicorp/go-syslog"
"github.com/hjson/hjson-go"
"github.com/hjson/hjson-go/v4"
"github.com/kardianos/minwinsvc"
"github.com/mitchellh/mapstructure"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/tuntap"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
type node struct {
core core.Core
config *config.NodeConfig
tuntap *tuntap.TunAdapter
core *core.Core
tun *tun.TunAdapter
multicast *multicast.Multicast
admin *admin.AdminSocket
}
func readConfig(log *log.Logger, useconf bool, useconffile string, normaliseconf bool) *config.NodeConfig {
// Use a configuration file. If -useconf, the configuration will be read
// from stdin. If -useconffile, the configuration will be read from the
// filesystem.
var conf []byte
var err error
if useconffile != "" {
// Read the file from the filesystem
conf, err = ioutil.ReadFile(useconffile)
// The main function is responsible for configuring and starting Yggdrasil.
func main() {
genconf := flag.Bool("genconf", false, "print a new config to stdout")
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")
exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format")
exportcsr := flag.Bool("exportcsr", false, "use in combination with either -useconf or -useconffile, outputs your self-signed certificate request in PEM format")
exportcert := flag.Bool("exportcert", false, "use in combination with either -useconf or -useconffile, outputs your self-signed certificate in PEM format")
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)")
ver := flag.Bool("version", false, "prints the version of this build")
logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
getaddr := flag.Bool("address", false, "returns the IPv6 address as derived from the supplied configuration")
getsnet := flag.Bool("subnet", false, "returns the IPv6 subnet as derived from the supplied configuration")
loglevel := flag.String("loglevel", "info", "loglevel to enable")
flag.Parse()
// Catch interrupts from the operating system to exit gracefully.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
// Capture the service being stopped on Windows.
minwinsvc.SetOnExit(cancel)
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch *logto {
case "stdout":
logger = log.New(os.Stdout, "", log.Flags())
case "syslog":
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
logger = log.New(syslogger, "", log.Flags())
}
default:
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
logger = log.New(logfd, "", log.Flags())
}
}
if logger == nil {
logger = log.New(os.Stdout, "", log.Flags())
logger.Warnln("Logging defaulting to stdout")
}
if *normaliseconf {
setLogLevel("error", logger)
} else {
// Read the file from stdin.
conf, err = ioutil.ReadAll(os.Stdin)
setLogLevel(*loglevel, logger)
}
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.Equal(conf[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
conf, err = decoder.Bytes(conf)
cfg := config.GenerateConfig()
var err error
switch {
case *ver:
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
return
case *autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN interface.
case *useconf:
if _, err := cfg.ReadFrom(os.Stdin); err != nil {
panic(err)
}
case *useconffile != "":
f, err := os.Open(*useconffile)
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
// configuration will use a sane default.
cfg := defaults.GenerateConfig()
var dat map[string]interface{}
if err := hjson.Unmarshal(conf, &dat); err != nil {
panic(err)
}
// Check if we have old field names
if _, ok := dat["TunnelRouting"]; ok {
log.Warnln("WARNING: Tunnel routing is no longer supported")
}
if old, ok := dat["SigningPrivateKey"]; ok {
log.Warnln("WARNING: The \"SigningPrivateKey\" configuration option has been renamed to \"PrivateKey\"")
if _, ok := dat["PrivateKey"]; !ok {
if privstr, err := hex.DecodeString(old.(string)); err == nil {
priv := ed25519.PrivateKey(privstr)
pub := priv.Public().(ed25519.PublicKey)
dat["PrivateKey"] = hex.EncodeToString(priv[:])
dat["PublicKey"] = hex.EncodeToString(pub[:])
} else {
log.Warnln("WARNING: The \"SigningPrivateKey\" configuration option contains an invalid value and will be ignored")
}
if _, err := cfg.ReadFrom(f); err != nil {
panic(err)
}
}
if oldmc, ok := dat["MulticastInterfaces"]; ok {
if oldmcvals, ok := oldmc.([]interface{}); ok {
var newmc []config.MulticastInterfaceConfig
for _, oldmcval := range oldmcvals {
if str, ok := oldmcval.(string); ok {
newmc = append(newmc, config.MulticastInterfaceConfig{
Regex: str,
Beacon: true,
Listen: true,
})
}
}
if newmc != nil {
if oldport, ok := dat["LinkLocalTCPPort"]; ok {
// numbers parse to float64 by default
if port, ok := oldport.(float64); ok {
for idx := range newmc {
newmc[idx].Port = uint16(port)
}
}
}
dat["MulticastInterfaces"] = newmc
}
}
}
// Sanitise the config
confJson, err := json.Marshal(dat)
if err != nil {
panic(err)
}
if err := json.Unmarshal(confJson, &cfg); err != nil {
panic(err)
}
// Overlay our newly mapped configuration onto the autoconf node config that
// we generated above.
if err = mapstructure.Decode(dat, &cfg); err != nil {
panic(err)
}
return cfg
}
_ = f.Close()
// Generates a new configuration and returns it in HJSON format. This is used
// with -genconf.
func doGenconf(isjson bool) string {
cfg := defaults.GenerateConfig()
var bs []byte
var err error
if isjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
case *genconf:
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
default:
fmt.Println("Usage:")
flag.PrintDefaults()
if *getaddr || *getsnet {
fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.")
}
}
if err != nil {
panic(err)
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
publicKey := privateKey.Public().(ed25519.PublicKey)
switch {
case *getaddr:
addr := address.AddrForKey(publicKey)
ip := net.IP(addr[:])
fmt.Println(ip.String())
return
case *getsnet:
snet := address.SubnetForKey(publicKey)
ipnet := net.IPNet{
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
Mask: net.CIDRMask(len(snet)*8, 128),
}
fmt.Println(ipnet.String())
return
case *normaliseconf:
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
case *exportkey:
pem, err := cfg.MarshalPEMPrivateKey()
if err != nil {
panic(err)
}
fmt.Println(string(pem))
return
case *exportcsr:
pem, err := cfg.GenerateCertificateSigningRequest()
if err != nil {
panic(err)
}
fmt.Println(string(pem))
return
case *exportcert:
pem, err := cfg.MarshalPEMCertificate()
if err != nil {
panic(err)
}
fmt.Println(string(pem))
return
}
return string(bs)
n := &node{}
// Setup the Yggdrasil node itself.
{
options := []core.SetupOption{
core.NodeInfo(cfg.NodeInfo),
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
}
for _, addr := range cfg.Listen {
options = append(options, core.ListenAddress(addr))
}
for _, peer := range cfg.Peers {
options = append(options, core.Peer{URI: peer})
}
for intf, peers := range cfg.InterfacePeers {
for _, peer := range peers {
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
}
}
for _, root := range cfg.RootCertificates {
options = append(options, core.RootCertificate(*root))
}
for _, allowed := range cfg.AllowedPublicKeys {
k, err := hex.DecodeString(allowed)
if err != nil {
panic(err)
}
options = append(options, core.AllowedPublicKey(k[:]))
}
if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil {
panic(err)
}
}
// Setup the admin socket.
{
options := []admin.SetupOption{
admin.ListenAddress(cfg.AdminListen),
}
if n.admin, err = admin.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil {
n.admin.SetupAdminHandlers()
}
}
// Setup the multicast module.
{
options := []multicast.SetupOption{}
for _, intf := range cfg.MulticastInterfaces {
options = append(options, multicast.MulticastInterface{
Regex: regexp.MustCompile(intf.Regex),
Beacon: intf.Beacon,
Listen: intf.Listen,
Port: intf.Port,
Priority: uint8(intf.Priority),
})
}
if n.multicast, err = multicast.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.multicast != nil {
n.multicast.SetupAdminHandlers(n.admin)
}
}
// Setup the TUN module.
{
options := []tun.SetupOption{
tun.InterfaceName(cfg.IfName),
tun.InterfaceMTU(cfg.IfMTU),
}
if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.tun != nil {
n.tun.SetupAdminHandlers(n.admin)
}
}
// Block until we are told to shut down.
<-ctx.Done()
// Shut down the node.
_ = n.admin.Stop()
_ = n.multicast.Stop()
_ = n.tun.Stop()
n.core.Stop()
}
func setLogLevel(loglevel string, logger *log.Logger) {
@@ -180,227 +309,3 @@ func setLogLevel(loglevel string, logger *log.Logger) {
}
}
}
type yggArgs struct {
genconf bool
useconf bool
useconffile string
normaliseconf bool
confjson bool
autoconf bool
ver bool
logto string
getaddr bool
getsnet bool
loglevel string
}
func getArgs() yggArgs {
genconf := flag.Bool("genconf", false, "print a new config to stdout")
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)")
ver := flag.Bool("version", false, "prints the version of this build")
logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
getaddr := flag.Bool("address", false, "returns the IPv6 address as derived from the supplied configuration")
getsnet := flag.Bool("subnet", false, "returns the IPv6 subnet as derived from the supplied configuration")
loglevel := flag.String("loglevel", "info", "loglevel to enable")
flag.Parse()
return yggArgs{
genconf: *genconf,
useconf: *useconf,
useconffile: *useconffile,
normaliseconf: *normaliseconf,
confjson: *confjson,
autoconf: *autoconf,
ver: *ver,
logto: *logto,
getaddr: *getaddr,
getsnet: *getsnet,
loglevel: *loglevel,
}
}
// The main function is responsible for configuring and starting Yggdrasil.
func run(args yggArgs, ctx context.Context, done chan struct{}) {
defer close(done)
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch args.logto {
case "stdout":
logger = log.New(os.Stdout, "", log.Flags())
case "syslog":
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
logger = log.New(syslogger, "", log.Flags())
}
default:
if logfd, err := os.OpenFile(args.logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
logger = log.New(logfd, "", log.Flags())
}
}
if logger == nil {
logger = log.New(os.Stdout, "", log.Flags())
logger.Warnln("Logging defaulting to stdout")
}
if args.normaliseconf {
setLogLevel("error", logger)
} else {
setLogLevel(args.loglevel, logger)
}
var cfg *config.NodeConfig
var err error
switch {
case args.ver:
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
return
case args.autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN/TAP interface.
cfg = defaults.GenerateConfig()
case args.useconffile != "" || args.useconf:
// Read the configuration from either stdin or from the filesystem
cfg = readConfig(logger, args.useconf, args.useconffile, args.normaliseconf)
// If the -normaliseconf option was specified then remarshal the above
// configuration and print it back to stdout. This lets the user update
// their configuration file with newly mapped names (like above) or to
// convert from plain JSON to commented HJSON.
if args.normaliseconf {
var bs []byte
if args.confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
}
case args.genconf:
// Generate a new configuration and print it to stdout.
fmt.Println(doGenconf(args.confjson))
return
default:
// No flags were provided, therefore print the list of flags to stdout.
flag.PrintDefaults()
}
// Have we got a working configuration? If we don't then it probably means
// that neither -autoconf, -useconf or -useconffile were set above. Stop
// if we don't.
if cfg == nil {
return
}
// Have we been asked for the node address yet? If so, print it and then stop.
getNodeKey := func() ed25519.PublicKey {
if pubkey, err := hex.DecodeString(cfg.PrivateKey); err == nil {
return ed25519.PrivateKey(pubkey).Public().(ed25519.PublicKey)
}
return nil
}
switch {
case args.getaddr:
if key := getNodeKey(); key != nil {
addr := address.AddrForKey(key)
ip := net.IP(addr[:])
fmt.Println(ip.String())
}
return
case args.getsnet:
if key := getNodeKey(); key != nil {
snet := address.SubnetForKey(key)
ipnet := net.IPNet{
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
Mask: net.CIDRMask(len(snet)*8, 128),
}
fmt.Println(ipnet.String())
}
return
default:
}
// Setup the Yggdrasil node itself. The node{} type includes a Core, so we
// don't need to create this manually.
n := node{config: cfg}
// Now start Yggdrasil - this starts the DHT, router, switch and other core
// components needed for Yggdrasil to operate
if err = n.core.Start(cfg, logger); err != nil {
logger.Errorln("An error occurred during startup")
panic(err)
}
// Register the session firewall gatekeeper function
// Allocate our modules
n.admin = &admin.AdminSocket{}
n.multicast = &multicast.Multicast{}
n.tuntap = &tuntap.TunAdapter{}
// Start the admin socket
if err := n.admin.Init(&n.core, cfg, logger, nil); err != nil {
logger.Errorln("An error occurred initialising admin socket:", err)
} else if err := n.admin.Start(); err != nil {
logger.Errorln("An error occurred starting admin socket:", err)
}
n.admin.SetupAdminHandlers(n.admin)
// Start the multicast interface
if err := n.multicast.Init(&n.core, cfg, logger, nil); err != nil {
logger.Errorln("An error occurred initialising multicast:", err)
} else if err := n.multicast.Start(); err != nil {
logger.Errorln("An error occurred starting multicast:", err)
}
n.multicast.SetupAdminHandlers(n.admin)
// Start the TUN/TAP interface
if err := n.tuntap.Init(&n.core, cfg, logger, nil); err != nil {
logger.Errorln("An error occurred initialising TUN/TAP:", err)
} else if err := n.tuntap.Start(); err != nil {
logger.Errorln("An error occurred starting TUN/TAP:", err)
}
n.tuntap.SetupAdminHandlers(n.admin)
// Make some nice output that tells us what our IPv6 address and subnet are.
// This is just logged to stdout for the user.
address := n.core.Address()
subnet := n.core.Subnet()
public := n.core.GetSelf().Key
logger.Infof("Your public key is %s", hex.EncodeToString(public[:]))
logger.Infof("Your IPv6 address is %s", address.String())
logger.Infof("Your IPv6 subnet is %s", subnet.String())
// Catch interrupts from the operating system to exit gracefully.
<-ctx.Done()
// Capture the service being stopped on Windows.
minwinsvc.SetOnExit(n.shutdown)
n.shutdown()
}
func (n *node) shutdown() {
_ = n.admin.Stop()
_ = n.multicast.Stop()
_ = n.tuntap.Stop()
n.core.Stop()
}
func main() {
args := getArgs()
hup := make(chan os.Signal, 1)
//signal.Notify(hup, os.Interrupt, syscall.SIGHUP)
term := make(chan os.Signal, 1)
signal.Notify(term, os.Interrupt, syscall.SIGTERM)
for {
done := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
go run(args, ctx, done)
select {
case <-hup:
cancel()
<-done
case <-term:
cancel()
<-done
return
case <-done:
return
}
}
}

View File

@@ -0,0 +1,90 @@
package main
import (
"bytes"
"flag"
"fmt"
"log"
"os"
"github.com/hjson/hjson-go/v4"
"golang.org/x/text/encoding/unicode"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
)
type CmdLineEnv struct {
args []string
endpoint, server string
injson, ver bool
}
func newCmdLineEnv() CmdLineEnv {
var cmdLineEnv CmdLineEnv
cmdLineEnv.endpoint = config.GetDefaults().DefaultAdminListen
return cmdLineEnv
}
func (cmdLineEnv *CmdLineEnv) parseFlagsAndArgs() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] command [key=value] [key=value] ...\n\n", os.Args[0])
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println()
fmt.Println("Please note that options must always specified BEFORE the command\non the command line or they will be ignored.")
fmt.Println()
fmt.Println("Commands:\n - Use \"list\" for a list of available commands")
fmt.Println()
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", cmdLineEnv.endpoint, "Admin socket endpoint")
injson := flag.Bool("json", false, "Output in JSON format (as opposed to pretty-print)")
ver := flag.Bool("version", false, "Prints the version of this build")
flag.Parse()
cmdLineEnv.args = flag.Args()
cmdLineEnv.server = *server
cmdLineEnv.injson = *injson
cmdLineEnv.ver = *ver
}
func (cmdLineEnv *CmdLineEnv) setEndpoint(logger *log.Logger) {
if cmdLineEnv.server == cmdLineEnv.endpoint {
if cfg, err := os.ReadFile(config.GetDefaults().DefaultConfigFile); err == nil {
if bytes.Equal(cfg[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(cfg[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
cfg, err = decoder.Bytes(cfg)
if err != nil {
panic(err)
}
}
var dat map[string]interface{}
if err := hjson.Unmarshal(cfg, &dat); err != nil {
panic(err)
}
if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") {
cmdLineEnv.endpoint = ep
logger.Println("Found platform default config file", config.GetDefaults().DefaultConfigFile)
logger.Println("Using endpoint", cmdLineEnv.endpoint, "from AdminListen")
} else {
logger.Println("Configuration file doesn't contain appropriate AdminListen option")
logger.Println("Falling back to platform default", config.GetDefaults().DefaultAdminListen)
}
} else {
logger.Println("Can't open config file from default location", config.GetDefaults().DefaultConfigFile)
logger.Println("Falling back to platform default", config.GetDefaults().DefaultAdminListen)
}
} else {
cmdLineEnv.endpoint = cmdLineEnv.server
logger.Println("Using endpoint", cmdLineEnv.endpoint, "from command line")
}
}

View File

@@ -6,24 +6,21 @@ import (
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
"golang.org/x/text/encoding/unicode"
"github.com/hjson/hjson-go"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/olekukonko/tablewriter"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
type admin_info map[string]interface{}
func main() {
// makes sure we can use defer and still return an error code to the OS
os.Exit(run())
@@ -32,6 +29,7 @@ func main() {
func run() int {
logbuffer := &bytes.Buffer{}
logger := log.New(logbuffer, "", log.Flags())
defer func() int {
if r := recover(); r != nil {
logger.Println("Fatal error:", r)
@@ -41,83 +39,30 @@ func run() int {
return 0
}()
endpoint := defaults.GetDefaults().DefaultAdminListen
cmdLineEnv := newCmdLineEnv()
cmdLineEnv.parseFlagsAndArgs()
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] command [key=value] [key=value] ...\n\n", os.Args[0])
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println()
fmt.Println("Please note that options must always specified BEFORE the command\non the command line or they will be ignored.")
fmt.Println()
fmt.Println("Commands:\n - Use \"list\" for a list of available commands")
fmt.Println()
fmt.Println("Examples:")
fmt.Println(" - ", os.Args[0], "list")
fmt.Println(" - ", os.Args[0], "getPeers")
fmt.Println(" - ", os.Args[0], "-v getSelf")
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)")
ver := flag.Bool("version", false, "Prints the version of this build")
flag.Parse()
args := flag.Args()
if *ver {
if cmdLineEnv.ver {
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
fmt.Println("To get the version number of the running Yggdrasil node, run", os.Args[0], "getSelf")
return 0
}
if len(args) == 0 {
if len(cmdLineEnv.args) == 0 {
flag.Usage()
return 0
}
if *server == endpoint {
if config, err := ioutil.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil {
if bytes.Equal(config[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(config[0:2], []byte{0xFE, 0xFF}) {
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 {
endpoint = *server
logger.Println("Using endpoint", endpoint, "from command line")
}
cmdLineEnv.setEndpoint(logger)
var conn net.Conn
u, err := url.Parse(endpoint)
u, err := url.Parse(cmdLineEnv.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:])
logger.Println("Connecting to UNIX socket", cmdLineEnv.endpoint[7:])
conn, err = net.Dial("unix", cmdLineEnv.endpoint[7:])
case "tcp":
logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", u.Host)
@@ -127,326 +72,226 @@ func run() int {
}
} else {
logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", endpoint)
conn, err = net.Dial("tcp", cmdLineEnv.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 {
send := &admin.AdminSocketRequest{}
recv := &admin.AdminSocketResponse{}
args := map[string]string{}
for c, a := range cmdLineEnv.args {
if c == 0 {
if strings.HasPrefix(a, "-") {
logger.Printf("Ignoring flag %s as it should be specified before other parameters\n", a)
continue
}
logger.Printf("Sending request: %v\n", a)
send["request"] = a
send.Name = a
continue
}
tokens := strings.Split(a, "=")
if len(tokens) == 1 {
send[tokens[0]] = true
} else if len(tokens) > 2 {
send[tokens[0]] = strings.Join(tokens[1:], "=")
} else if len(tokens) == 2 {
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]])
}
tokens := strings.SplitN(a, "=", 2)
switch {
case len(tokens) == 1:
logger.Println("Ignoring invalid argument:", a)
default:
args[tokens[0]] = tokens[1]
}
}
if send.Arguments, err = json.Marshal(args); err != nil {
panic(err)
}
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")
}
return 1
}
if _, ok := recv["request"]; !ok {
fmt.Println("Missing request in response (malformed response?)")
return 1
}
if _, ok := recv["response"]; !ok {
fmt.Println("Missing response body (malformed response?)")
return 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))
}
return 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" || k == "nodeinfo" || k == "was_mtu_fixed" {
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 boxSigKey, ok := v.(map[string]interface{})["key"].(string); ok {
fmt.Println("Public key:", boxSigKey)
}
if coords, ok := v.(map[string]interface{})["coords"].(string); ok {
fmt.Println("Coords:", coords)
}
if *verbose {
if nodeID, ok := v.(map[string]interface{})["node_id"].(string); ok {
fmt.Println("Node ID:", nodeID)
}
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 routes, ok := res["routes"].(map[string]interface{}); !ok {
fmt.Println("No routes found")
} else {
if res["routes"] == nil || len(routes) == 0 {
fmt.Println("No routes found")
} else {
fmt.Println("Routes:")
for k, v := range routes {
if pv, ok := v.(string); ok {
fmt.Println("-", k, " via ", pv)
}
}
}
}
case "settunnelrouting":
fallthrough
case "gettunnelrouting":
if enabled, ok := res["enabled"].(bool); !ok {
fmt.Println("Tunnel routing is disabled")
} else if !enabled {
fmt.Println("Tunnel routing is disabled")
} else {
fmt.Println("Tunnel routing is enabled")
}
default:
if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil {
fmt.Println(string(json))
}
}
} else {
logger.Println("Error receiving response:", err)
if err := decoder.Decode(&recv); err != nil {
panic(err)
}
if v, ok := recv["status"]; ok && v != "success" {
if recv.Status == "error" {
if err := recv.Error; err != "" {
fmt.Println("Admin socket returned an error:", err)
} else {
fmt.Println("Admin socket returned an error but didn't specify any error text")
}
return 1
}
if cmdLineEnv.injson {
if json, err := json.MarshalIndent(recv.Response, "", " "); err == nil {
fmt.Println(string(json))
}
return 0
}
table := tablewriter.NewWriter(os.Stdout)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetAutoFormatHeaders(false)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)
table.SetAutoWrapText(false)
switch strings.ToLower(send.Name) {
case "list":
var resp admin.ListResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Command", "Arguments", "Description"})
for _, entry := range resp.List {
for i := range entry.Fields {
entry.Fields[i] = entry.Fields[i] + "=..."
}
table.Append([]string{entry.Command, strings.Join(entry.Fields, ", "), entry.Description})
}
table.Render()
case "getself":
var resp admin.GetSelfResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Append([]string{"Build name:", resp.BuildName})
table.Append([]string{"Build version:", resp.BuildVersion})
table.Append([]string{"IPv6 address:", resp.IPAddress})
table.Append([]string{"IPv6 subnet:", resp.Subnet})
table.Append([]string{"Routing table size:", fmt.Sprintf("%d", resp.RoutingEntries)})
table.Append([]string{"Public key:", resp.PublicKey})
table.Render()
case "getpeers":
var resp admin.GetPeersResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"URI", "State", "Dir", "IP Address", "Uptime", "RX", "TX", "Pr", "Last Error"})
for _, peer := range resp.Peers {
state, lasterr, dir := "Up", "-", "Out"
if !peer.Up {
state, lasterr = "Down", fmt.Sprintf("%s ago: %s", peer.LastErrorTime.Round(time.Second), peer.LastError)
}
if peer.Inbound {
dir = "In"
}
uri, err := url.Parse(peer.URI)
if err != nil {
panic(err)
}
uri.RawQuery = ""
table.Append([]string{
uri.String(),
state,
dir,
peer.IPAddress,
(time.Duration(peer.Uptime) * time.Second).String(),
peer.RXBytes.String(),
peer.TXBytes.String(),
fmt.Sprintf("%d", peer.Priority),
lasterr,
})
}
table.Render()
case "gettree":
var resp admin.GetTreeResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
//table.SetHeader([]string{"Public Key", "IP Address", "Port", "Rest"})
table.SetHeader([]string{"Public Key", "IP Address", "Parent", "Sequence"})
for _, tree := range resp.Tree {
table.Append([]string{
tree.PublicKey,
tree.IPAddress,
tree.Parent,
fmt.Sprintf("%d", tree.Sequence),
//fmt.Sprintf("%d", dht.Port),
//fmt.Sprintf("%d", dht.Rest),
})
}
table.Render()
case "getpaths":
var resp admin.GetPathsResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Public Key", "IP Address", "Path", "Seq"})
for _, p := range resp.Paths {
table.Append([]string{
p.PublicKey,
p.IPAddress,
fmt.Sprintf("%v", p.Path),
fmt.Sprintf("%d", p.Sequence),
})
}
table.Render()
case "getsessions":
var resp admin.GetSessionsResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Public Key", "IP Address", "Uptime", "RX", "TX"})
for _, p := range resp.Sessions {
table.Append([]string{
p.PublicKey,
p.IPAddress,
(time.Duration(p.Uptime) * time.Second).String(),
p.RXBytes.String(),
p.TXBytes.String(),
})
}
table.Render()
case "getnodeinfo":
var resp core.GetNodeInfoResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
for _, v := range resp {
fmt.Println(string(v))
break
}
case "getmulticastinterfaces":
var resp multicast.GetMulticastInterfacesResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Interface"})
for _, p := range resp.Interfaces {
table.Append([]string{p})
}
table.Render()
case "gettun":
var resp tun.GetTUNResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Append([]string{"TUN enabled:", fmt.Sprintf("%#v", resp.Enabled)})
if resp.Enabled {
table.Append([]string{"Interface name:", resp.Name})
table.Append([]string{"Interface MTU:", fmt.Sprintf("%d", resp.MTU)})
}
table.Render()
case "addpeer", "removepeer":
default:
fmt.Println(string(recv.Response))
}
return 0
}

View File

@@ -1,7 +1,5 @@
/*
This file generates crypto keys for [ansible-yggdrasil](https://github.com/jcgruenhage/ansible-yggdrasil/)
*/
package main

View File

@@ -15,6 +15,10 @@ command -v mkbom >/dev/null 2>&1 || (
sudo make install || (echo "Failed to build mkbom"; exit 1)
)
# Build Yggdrasil
echo "running GO111MODULE=on GOOS=darwin GOARCH=${PKGARCH-amd64} ./build"
GO111MODULE=on GOOS=darwin GOARCH=${PKGARCH-amd64} ./build
# 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
@@ -75,6 +79,7 @@ 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 ))
[ "$PKGARCH" = "amd64" ] && PKGHOSTARCH="x86_64" || PKGHOSTARCH=${PKGARCH}
# Create the PackageInfo file
cat > pkgbuild/flat/base.pkg/PackageInfo << EOF
@@ -94,7 +99,7 @@ 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"/>
<options customize="never" allow-external-scripts="no" hostArchitectures="${PKGHOSTARCH}" />
<domains enable_anywhere="true"/>
<installation-check script="pm_install_check();"/>
<script>

52
contrib/mobile/build Executable file
View File

@@ -0,0 +1,52 @@
#!/bin/sh
set -ef
[ ! -d contrib/mobile ] && (echo "Must run ./contrib/mobile/build [-i] [-a] from the repository top level folder"; exit 1)
PKGSRC=${PKGSRC:-github.com/yggdrasil-network/yggdrasil-go/src/version}
PKGNAME=${PKGNAME:-$(sh contrib/semver/name.sh)}
PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)}
LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER"
ARGS="-v"
while getopts "aitc:l:d" option
do
case "$option"
in
i) IOS=true;;
a) ANDROID=true;;
t) TABLES=true;;
c) GCFLAGS="$GCFLAGS $OPTARG";;
l) LDFLAGS="$LDFLAGS $OPTARG";;
d) ARGS="$ARGS -tags debug" DEBUG=true;;
esac
done
if [ -z $TABLES ] && [ -z $DEBUG ]; then
LDFLAGS="$LDFLAGS -s -w"
fi
if [ ! $IOS ] && [ ! $ANDROID ]; then
echo "Must specify -a (Android), -i (iOS) or both"
exit 1
fi
if [ $IOS ]; then
echo "Building framework for iOS"
go get golang.org/x/mobile/bind
gomobile bind \
-target ios -tags mobile -o Yggdrasil.xcframework \
-ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \
./contrib/mobile ./src/config;
fi
if [ $ANDROID ]; then
echo "Building aar for Android"
go get golang.org/x/mobile/bind
gomobile bind \
-target android -tags mobile -o yggdrasil.aar \
-ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" \
./contrib/mobile ./src/config;
fi

243
contrib/mobile/mobile.go Normal file
View File

@@ -0,0 +1,243 @@
package mobile
import (
"encoding/hex"
"encoding/json"
"net"
"regexp"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
_ "golang.org/x/mobile/bind"
)
// Yggdrasil mobile package is meant to "plug the gap" for mobile support, as
// Gomobile will not create headers for Swift/Obj-C etc if they have complex
// (non-native) types. Therefore for iOS we will expose some nice simple
// functions. Note that in the case of iOS we handle reading/writing to/from TUN
// in Swift therefore we use the "dummy" TUN interface instead.
type Yggdrasil struct {
core *core.Core
iprwc *ipv6rwc.ReadWriteCloser
config *config.NodeConfig
multicast *multicast.Multicast
log MobileLogger
}
// StartAutoconfigure starts a node with a randomly generated config
func (m *Yggdrasil) StartAutoconfigure() error {
return m.StartJSON([]byte("{}"))
}
// StartJSON starts a node with the given JSON config. You can get JSON config
// (rather than HJSON) by using the GenerateConfigJSON() function
func (m *Yggdrasil) StartJSON(configjson []byte) error {
logger := log.New(m.log, "", 0)
logger.EnableLevel("error")
logger.EnableLevel("warn")
logger.EnableLevel("info")
m.config = config.GenerateConfig()
if err := m.config.UnmarshalHJSON(configjson); err != nil {
return err
}
// Setup the Yggdrasil node itself.
{
options := []core.SetupOption{}
for _, peer := range m.config.Peers {
options = append(options, core.Peer{URI: peer})
}
for intf, peers := range m.config.InterfacePeers {
for _, peer := range peers {
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
}
}
for _, allowed := range m.config.AllowedPublicKeys {
k, err := hex.DecodeString(allowed)
if err != nil {
panic(err)
}
options = append(options, core.AllowedPublicKey(k[:]))
}
for _, root := range m.config.RootCertificates {
options = append(options, core.RootCertificate(*root))
}
var err error
m.core, err = core.New(m.config.Certificate, logger, options...)
if err != nil {
panic(err)
}
}
// Setup the multicast module.
if len(m.config.MulticastInterfaces) > 0 {
var err error
options := []multicast.SetupOption{}
for _, intf := range m.config.MulticastInterfaces {
options = append(options, multicast.MulticastInterface{
Regex: regexp.MustCompile(intf.Regex),
Beacon: intf.Beacon,
Listen: intf.Listen,
Port: intf.Port,
Priority: uint8(intf.Priority),
})
}
m.multicast, err = multicast.New(m.core, logger, options...)
if err != nil {
logger.Errorln("An error occurred starting multicast:", err)
}
}
mtu := m.config.IfMTU
m.iprwc = ipv6rwc.NewReadWriteCloser(m.core)
if m.iprwc.MaxMTU() < mtu {
mtu = m.iprwc.MaxMTU()
}
m.iprwc.SetMTU(mtu)
return nil
}
// Send sends a packet to Yggdrasil. It should be a fully formed
// IPv6 packet
func (m *Yggdrasil) Send(p []byte) error {
if m.iprwc == nil {
return nil
}
_, _ = m.iprwc.Write(p)
return nil
}
// Send sends a packet from given buffer to Yggdrasil. From first byte up to length.
func (m *Yggdrasil) SendBuffer(p []byte, length int) error {
if m.iprwc == nil {
return nil
}
if len(p) < length {
return nil
}
_, _ = m.iprwc.Write(p[:length])
return nil
}
// Recv waits for and reads a packet coming from Yggdrasil. It
// will be a fully formed IPv6 packet
func (m *Yggdrasil) Recv() ([]byte, error) {
if m.iprwc == nil {
return nil, nil
}
var buf [65535]byte
n, _ := m.iprwc.Read(buf[:])
return buf[:n], nil
}
// Recv waits for and reads a packet coming from Yggdrasil to given buffer, returning size of packet
func (m *Yggdrasil) RecvBuffer(buf []byte) (int, error) {
if m.iprwc == nil {
return 0, nil
}
n, _ := m.iprwc.Read(buf)
return n, nil
}
// Stop the mobile Yggdrasil instance
func (m *Yggdrasil) Stop() error {
logger := log.New(m.log, "", 0)
logger.EnableLevel("info")
logger.Infof("Stop the mobile Yggdrasil instance %s", "")
if err := m.multicast.Stop(); err != nil {
return err
}
m.core.Stop()
return nil
}
// Retry resets the peer connection timer and tries to dial them immediately.
func (m *Yggdrasil) RetryPeersNow() {
m.core.RetryPeersNow()
}
// GenerateConfigJSON generates mobile-friendly configuration in JSON format
func GenerateConfigJSON() []byte {
nc := config.GenerateConfig()
nc.IfName = "none"
if json, err := json.Marshal(nc); err == nil {
return json
}
return nil
}
// GetAddressString gets the node's IPv6 address
func (m *Yggdrasil) GetAddressString() string {
ip := m.core.Address()
return ip.String()
}
// GetSubnetString gets the node's IPv6 subnet in CIDR notation
func (m *Yggdrasil) GetSubnetString() string {
subnet := m.core.Subnet()
return subnet.String()
}
// GetPublicKeyString gets the node's public key in hex form
func (m *Yggdrasil) GetPublicKeyString() string {
return hex.EncodeToString(m.core.GetSelf().Key)
}
// GetRoutingEntries gets the number of entries in the routing table
func (m *Yggdrasil) GetRoutingEntries() int {
return int(m.core.GetSelf().RoutingEntries)
}
func (m *Yggdrasil) GetPeersJSON() (result string) {
peers := []struct {
core.PeerInfo
IP string
}{}
for _, v := range m.core.GetPeers() {
a := address.AddrForKey(v.Key)
ip := net.IP(a[:]).String()
peers = append(peers, struct {
core.PeerInfo
IP string
}{
PeerInfo: v,
IP: ip,
})
}
if res, err := json.Marshal(peers); err == nil {
return string(res)
} else {
return "{}"
}
}
func (m *Yggdrasil) GetPathsJSON() (result string) {
if res, err := json.Marshal(m.core.GetPaths()); err == nil {
return string(res)
} else {
return "{}"
}
}
func (m *Yggdrasil) GetTreeJSON() (result string) {
if res, err := json.Marshal(m.core.GetTree()); err == nil {
return string(res)
} else {
return "{}"
}
}
// GetMTU returns the configured node MTU. This must be called AFTER Start.
func (m *Yggdrasil) GetMTU() int {
return int(m.core.MTU())
}
func GetVersion() string {
return version.BuildVersion()
}

View File

@@ -0,0 +1,13 @@
//go:build android
// +build android
package mobile
import "log"
type MobileLogger struct{}
func (nsl MobileLogger) Write(p []byte) (n int, err error) {
log.Println(string(p))
return len(p), nil
}

View File

@@ -0,0 +1,28 @@
//go:build ios
// +build ios
package mobile
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
void Log(const char *text) {
NSString *nss = [NSString stringWithUTF8String:text];
NSLog(@"%@", nss);
}
*/
import "C"
import (
"unsafe"
)
type MobileLogger struct {
}
func (nsl MobileLogger) Write(p []byte) (n int, err error) {
p = append(p, 0)
cstr := (*C.char)(unsafe.Pointer(&p[0]))
C.Log(cstr)
return len(p), nil
}

View File

@@ -0,0 +1,14 @@
//go:build !android && !ios
// +build !android,!ios
package mobile
import "fmt"
type MobileLogger struct {
}
func (nsl MobileLogger) Write(p []byte) (n int, err error) {
fmt.Print(string(p))
return len(p), nil
}

View File

@@ -0,0 +1,16 @@
package mobile
import "testing"
func TestStartYggdrasil(t *testing.T) {
ygg := &Yggdrasil{}
if err := ygg.StartAutoconfigure(); err != nil {
t.Fatalf("Failed to start Yggdrasil: %s", err)
}
t.Log("Address:", ygg.GetAddressString())
t.Log("Subnet:", ygg.GetSubnetString())
t.Log("Routing entries:", ygg.GetRoutingEntries())
if err := ygg.Stop(); err != nil {
t.Fatalf("Failed to stop Yggdrasil: %s", err)
}
}

View File

@@ -1,9 +1,9 @@
#!/bin/sh
# This script generates an MSI file for Yggdrasil for a given architecture. It
# needs to run on Windows within MSYS2 and Go 1.13 or later must be installed on
# the system and within the PATH. This is ran currently by Appveyor (see
# appveyor.yml in the repository root) for both x86 and x64.
# needs to run on Windows within MSYS2 and Go 1.17 or later must be installed on
# the system and within the PATH. This is ran currently by GitHub Actions (see
# the workflows in the repository).
#
# Author: Neil Alexander <neilalexander@users.noreply.github.com>
@@ -11,37 +11,21 @@
PKGARCH=$1
if [ "${PKGARCH}" == "" ];
then
echo "tell me the architecture: x86, x64 or arm"
echo "tell me the architecture: x86, x64, arm or arm64"
exit 1
fi
# Get the rest of the repository history. This is needed within Appveyor because
# otherwise we don't get all of the branch histories and therefore the semver
# scripts don't work properly.
if [ "${APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH}" != "" ];
then
git fetch --all
git checkout ${APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH}
elif [ "${APPVEYOR_REPO_BRANCH}" != "" ];
then
git fetch --all
git checkout ${APPVEYOR_REPO_BRANCH}
fi
# Install prerequisites within MSYS2
pacman -S --needed --noconfirm unzip git curl
# Download the wix tools!
if [ ! -d wixbin ];
then
curl -LO https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip
if [ `md5sum wix311-binaries.zip | cut -f 1 -d " "` != "47a506f8ab6666ee3cc502fb07d0ee2a" ];
curl -LO https://wixtoolset.org/downloads/v3.14.0.3910/wix314-binaries.zip
if [ `md5sum wix314-binaries.zip | cut -f 1 -d " "` != "34f655cf108086838dd5a76d4318063b" ];
then
echo "wix package didn't match expected checksum"
exit 1
fi
mkdir -p wixbin
unzip -o wix311-binaries.zip -d wixbin || (
unzip -o wix314-binaries.zip -d wixbin || (
echo "failed to unzip WiX"
exit 1
)
@@ -51,7 +35,7 @@ fi
[ "${PKGARCH}" == "x64" ] && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "x86" ] && GOOS=windows GOARCH=386 CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "arm" ] && GOOS=windows GOARCH=arm CGO_ENABLED=0 ./build
#[ "${PKGARCH}" == "arm64" ] && GOOS=windows GOARCH=arm64 CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "arm64" ] && GOOS=windows GOARCH=arm64 CGO_ENABLED=0 ./build
# Create the postinstall script
cat > updateconfig.bat << EOF
@@ -69,14 +53,14 @@ EOF
PKGNAME=$(sh contrib/semver/name.sh)
PKGVERSION=$(sh contrib/msi/msversion.sh --bare)
PKGVERSIONMS=$(echo $PKGVERSION | tr - .)
[ "${PKGARCH}" == "x64" ] && \
([ "${PKGARCH}" == "x64" ] || [ "${PKGARCH}" == "arm64" ]) && \
PKGGUID="77757838-1a23-40a5-a720-c3b43e0260cc" PKGINSTFOLDER="ProgramFiles64Folder" || \
PKGGUID="54a3294e-a441-4322-aefb-3bb40dd022bb" PKGINSTFOLDER="ProgramFilesFolder"
# Download the Wintun driver
if [ ! -d wintun ];
then
curl -o wintun.zip https://www.wintun.net/builds/wintun-0.11.zip
curl -o wintun.zip https://www.wintun.net/builds/wintun-0.14.1.zip
unzip wintun.zip
fi
if [ $PKGARCH = "x64" ]; then
@@ -85,8 +69,8 @@ elif [ $PKGARCH = "x86" ]; then
PKGWINTUNDLL=wintun/bin/x86/wintun.dll
elif [ $PKGARCH = "arm" ]; then
PKGWINTUNDLL=wintun/bin/arm/wintun.dll
#elif [ $PKGARCH = "arm64" ]; then
# PKGWINTUNDLL=wintun/bin/arm64/wintun.dll
elif [ $PKGARCH = "arm64" ]; then
PKGWINTUNDLL=wintun/bin/arm64/wintun.dll
else
echo "wasn't sure which architecture to get wintun for"
exit 1

View File

@@ -1,9 +1,11 @@
#!/bin/sh
# Get the current branch name
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
BRANCH="$GITHUB_REF_NAME"
if [ -z "$BRANCH" ]; then
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
fi
# Complain if the git history is not available
if [ $? != 0 ] || [ -z "$BRANCH" ]; then
printf "yggdrasil"
exit 0

View File

@@ -1,8 +1,8 @@
[Unit]
Description=yggdrasil
Wants=network.target
Wants=network-online.target
Wants=yggdrasil-default-config.service
After=network.target
After=network-online.target
After=yggdrasil-default-config.service
[Service]
@@ -10,7 +10,7 @@ Group=yggdrasil
ProtectHome=true
ProtectSystem=true
SyslogIdentifier=yggdrasil
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
ExecStartPre=+-/sbin/modprobe tun
ExecStart=/usr/bin/yggdrasil -useconffile /etc/yggdrasil.conf
ExecReload=/bin/kill -HUP $MAINPID

43
go.mod
View File

@@ -1,26 +1,39 @@
module github.com/yggdrasil-network/yggdrasil-go
go 1.16
go 1.19
require (
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/Arceliar/ironwood v0.0.0-20230521174855-fdfa6326d125
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d
github.com/cheggaaa/pb/v3 v3.0.8
github.com/fatih/color v1.12.0 // indirect
github.com/gologme/log v1.2.0
github.com/hashicorp/go-syslog v1.0.0
github.com/hjson/hjson-go v3.1.0+incompatible
github.com/kardianos/minwinsvc v1.0.0
github.com/hjson/hjson-go/v4 v4.3.0
github.com/kardianos/minwinsvc v1.0.2
github.com/vishvananda/netlink v1.1.0
golang.org/x/mobile v0.0.0-20221110043201-43a038452099
golang.org/x/net v0.9.0
golang.org/x/sys v0.7.0
golang.org/x/text v0.9.0
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a
golang.zx2c4.com/wireguard/windows v0.4.12
)
require (
github.com/bits-and-blooms/bitset v1.5.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.3.1 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/tools v0.6.0 // indirect
)
require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/fatih/color v1.12.0 // indirect
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.4.1
github.com/vishvananda/netlink v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273
golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2
golang.zx2c4.com/wireguard/windows v0.3.14
)

96
go.sum
View File

@@ -1,10 +1,15 @@
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031 h1:DZVDfYhVdu+0wAiRHoY1olyNkKxIot9UjBnbQFzuUlM=
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031/go.mod h1:RP72rucOFm5udrnEzTmIWLRVGQiV/fSUAQXJ0RST/nk=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3ugUxx2EnnWmgba1kCqPkd4Gk1yQ=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI=
github.com/Arceliar/ironwood v0.0.0-20230521174855-fdfa6326d125 h1:l2elyrosw63mTqZzwR0Nv8vPZWZC/0Hvwl8Iuva5htM=
github.com/Arceliar/ironwood v0.0.0-20230521174855-fdfa6326d125/go.mod h1:5x7fWW0mshe9WQ1lvSMmmHBYC3BeHH9gpwW5tz7cbfw=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/bits-and-blooms/bitset v1.3.1/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bitset v1.5.0 h1:NpE8frKRLGHIcEzkR+gZhiioW1+WbYV6fKwD6ZIpQT8=
github.com/bits-and-blooms/bitset v1.5.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/bits-and-blooms/bloom/v3 v3.3.1 h1:K2+A19bXT8gJR5mU7y+1yW6hsKfNCjcP2uNfLFKncjQ=
github.com/bits-and-blooms/bloom/v3 v3.3.1/go.mod h1:bhUUknWd5khVbTe4UgMCSiOOVJzr3tMoijSK3WwvW90=
github.com/cheggaaa/pb/v3 v3.0.8 h1:bC8oemdChbke2FHIIGy9mn4DPJ2caZYQnfbRqwmdCoA=
github.com/cheggaaa/pb/v3 v3.0.8/go.mod h1:UICbiLec/XO6Hw6k+BHEtHeQFzzBH4i2/qk/ow1EJTA=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -14,66 +19,81 @@ github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/kardianos/minwinsvc v1.0.0 h1:+JfAi8IBJna0jY2dJGZqi7o15z13JelFIklJCAENALA=
github.com/kardianos/minwinsvc v1.0.0/go.mod h1:Bgd0oc+D0Qo3bBytmNtyRKVlp85dAloLKhfxanPFFRc=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/hjson/hjson-go/v4 v4.3.0 h1:dyrzJdqqFGhHt+FSrs5n9s6b0fPM8oSJdWo+oS3YnJw=
github.com/hjson/hjson-go/v4 v4.3.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/kardianos/minwinsvc v1.0.2 h1:JmZKFJQrmTGa/WiW+vkJXKmfzdjabuEW4Tirj5lLdR0=
github.com/kardianos/minwinsvc v1.0.2/go.mod h1:LUZNYhNmxujx2tR7FbdxqYJ9XDDoCd3MQcl1o//FWl4=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/mobile v0.0.0-20221110043201-43a038452099 h1:aIu0lKmfdgtn2uTj7JI2oN4TUrQvgB+wzTPO23bCKt8=
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b h1:k+E048sYJHyVnsr1GDrRZWQ32D2C7lWs9JRc0bel53A=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273 h1:faDu4veV+8pcThn4fewv6TVlNCezafGoC1gM/mxQLbQ=
golang.org/x/sys v0.0.0-20210611083646-a4fc73990273/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f h1:yQJrRE0hDxDFmZLlRaw+3vusO4fwNHgHIjUOMO7bHYI=
golang.org/x/text v0.3.7-0.20210503195748-5c7c50ebbd4f/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.0-20210510202332-9844c74f67ec/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2 h1:wfOOSvHgIzTZ9h5Vb6yUFZNn7uf3bT7PeYsHOO7tYDM=
golang.zx2c4.com/wireguard v0.0.0-20210604143328-f9b48a961cd2/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard/windows v0.3.14 h1:5yIDYyrQyGkLqV+tzY4ilMNeIvQeMXAz0glZz9u179A=
golang.zx2c4.com/wireguard/windows v0.3.14/go.mod h1:3P4IEAsb+BjlKZmpUXgy74c0iX9AVwwr3WcVJ8nPgME=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a h1:tTbyylK9/D3u/wEP26Vx7L700UpY48nhioJWZM1vhZw=
golang.zx2c4.com/wireguard v0.0.0-20211017052713-f87e87af0d9a/go.mod h1:id8Oh3eCCmpj9uVGWVjsUAl6UPX5ysMLzu6QxJU2UOU=
golang.zx2c4.com/wireguard/windows v0.4.12 h1:CUmbdWKVNzTSsVb4yUAiEwL3KsabdJkEPdDjCHxBlhA=
golang.zx2c4.com/wireguard/windows v0.4.12/go.mod h1:PW4y+d9oY83XU9rRwRwrJDwEMuhVjMxu2gfD1cfzS7w=

View File

@@ -64,7 +64,7 @@ func AddrForKey(publicKey ed25519.PublicKey) *Address {
buf[idx] = ^buf[idx]
}
var addr Address
var temp []byte
var temp = make([]byte, 0, 32)
done := false
ones := byte(0)
bits := byte(0)
@@ -108,7 +108,7 @@ func SubnetForKey(publicKey ed25519.PublicKey) *Subnet {
}
var snet Subnet
copy(snet[:], addr[:])
prefix := GetPrefix()
prefix := GetPrefix() // nolint:staticcheck
snet[len(prefix)-1] |= 0x01
return &snet
}
@@ -117,7 +117,7 @@ func SubnetForKey(publicKey ed25519.PublicKey) *Subnet {
// This is used for key lookup.
func (a *Address) GetKey() ed25519.PublicKey {
var key [ed25519.PublicKeySize]byte
prefix := GetPrefix()
prefix := GetPrefix() // nolint:staticcheck
ones := int(a[len(prefix)])
for idx := 0; idx < ones; idx++ {
key[idx/8] |= 0x80 >> byte(idx%8)
@@ -129,7 +129,11 @@ func (a *Address) GetKey() ed25519.PublicKey {
bits <<= byte(idx % 8)
keyIdx := keyOffset + (idx - addrOffset)
bits >>= byte(keyIdx % 8)
key[keyIdx/8] |= bits
idx := keyIdx / 8
if idx >= len(key) {
break
}
key[idx] |= bits
}
for idx := range key {
key[idx] = ^key[idx]

114
src/address/address_test.go Normal file
View File

@@ -0,0 +1,114 @@
package address
import (
"bytes"
"crypto/ed25519"
"math/rand"
"testing"
)
func TestAddress_Address_IsValid(t *testing.T) {
var address Address
rand.Read(address[:])
address[0] = 0
if address.IsValid() {
t.Fatal("invalid address marked as valid")
}
address[0] = 0x03
if address.IsValid() {
t.Fatal("invalid address marked as valid")
}
address[0] = 0x02
if !address.IsValid() {
t.Fatal("valid address marked as invalid")
}
}
func TestAddress_Subnet_IsValid(t *testing.T) {
var subnet Subnet
rand.Read(subnet[:])
subnet[0] = 0
if subnet.IsValid() {
t.Fatal("invalid subnet marked as valid")
}
subnet[0] = 0x02
if subnet.IsValid() {
t.Fatal("invalid subnet marked as valid")
}
subnet[0] = 0x03
if !subnet.IsValid() {
t.Fatal("valid subnet marked as invalid")
}
}
func TestAddress_AddrForKey(t *testing.T) {
publicKey := ed25519.PublicKey{
189, 186, 207, 216, 34, 64, 222, 61, 205, 18, 57, 36, 203, 181, 82, 86,
251, 141, 171, 8, 170, 152, 227, 5, 82, 138, 184, 79, 65, 158, 110, 251,
}
expectedAddress := Address{
2, 0, 132, 138, 96, 79, 187, 126, 67, 132, 101, 219, 141, 182, 104, 149,
}
if *AddrForKey(publicKey) != expectedAddress {
t.Fatal("invalid address returned")
}
}
func TestAddress_SubnetForKey(t *testing.T) {
publicKey := ed25519.PublicKey{
189, 186, 207, 216, 34, 64, 222, 61, 205, 18, 57, 36, 203, 181, 82, 86,
251, 141, 171, 8, 170, 152, 227, 5, 82, 138, 184, 79, 65, 158, 110, 251,
}
expectedSubnet := Subnet{3, 0, 132, 138, 96, 79, 187, 126}
if *SubnetForKey(publicKey) != expectedSubnet {
t.Fatal("invalid subnet returned")
}
}
func TestAddress_Address_GetKey(t *testing.T) {
address := Address{
2, 0, 132, 138, 96, 79, 187, 126, 67, 132, 101, 219, 141, 182, 104, 149,
}
expectedPublicKey := ed25519.PublicKey{
189, 186, 207, 216, 34, 64, 222, 61,
205, 18, 57, 36, 203, 181, 127, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
}
if !bytes.Equal(address.GetKey(), expectedPublicKey) {
t.Fatal("invalid public key returned")
}
}
func TestAddress_Subnet_GetKey(t *testing.T) {
subnet := Subnet{3, 0, 132, 138, 96, 79, 187, 126}
expectedPublicKey := ed25519.PublicKey{
189, 186, 207, 216, 34, 64, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255,
}
if !bytes.Equal(subnet.GetKey(), expectedPublicKey) {
t.Fatal("invalid public key returned")
}
}

21
src/admin/addpeer.go Normal file
View File

@@ -0,0 +1,21 @@
package admin
import (
"fmt"
"net/url"
)
type AddPeerRequest struct {
Uri string `json:"uri"`
Sintf string `json:"interface,omitempty"`
}
type AddPeerResponse struct{}
func (a *AdminSocket) addPeerHandler(req *AddPeerRequest, res *AddPeerResponse) error {
u, err := url.Parse(req.Uri)
if err != nil {
return fmt.Errorf("unable to parse peering URI: %w", err)
}
return a.core.AddPeer(u, req.Sintf)
}

View File

@@ -7,55 +7,63 @@ import (
"net"
"net/url"
"os"
"sort"
"strings"
"time"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
)
// TODO: Add authentication
type AdminSocket struct {
core *core.Core
log *log.Logger
listenaddr string
listener net.Listener
handlers map[string]handler
done chan struct{}
core *core.Core
log core.Logger
listener net.Listener
handlers map[string]handler
done chan struct{}
config struct {
listenaddr ListenAddress
}
}
type AdminSocketRequest struct {
Name string `json:"request"`
Arguments json.RawMessage `json:"arguments,omitempty"`
KeepAlive bool `json:"keepalive,omitempty"`
}
type AdminSocketResponse struct {
Status string `json:"status"`
Request struct {
Name string `json:"request"`
KeepAlive bool `json:"keepalive"`
} `json:"request"`
Response interface{} `json:"response"`
Status string `json:"status"`
Error string `json:"error,omitempty"`
Request AdminSocketRequest `json:"request"`
Response json.RawMessage `json:"response"`
}
type handler struct {
args []string // List of human-readable argument names
handler func(json.RawMessage) (interface{}, error) // First is input map, second is output
desc string // What does the endpoint do?
args []string // List of human-readable argument names
handler core.AddHandlerFunc // First is input map, second is output
}
type ListResponse struct {
List map[string]ListEntry `json:"list"`
List []ListEntry `json:"list"`
}
type ListEntry struct {
Fields []string `json:"fields"`
Command string `json:"command"`
Description string `json:"description"`
Fields []string `json:"fields,omitempty"`
}
// AddHandler is called for each admin function to add the handler and help documentation to the API.
func (a *AdminSocket) AddHandler(name string, args []string, handlerfunc func(json.RawMessage) (interface{}, error)) error {
func (a *AdminSocket) AddHandler(name, desc string, args []string, handlerfunc core.AddHandlerFunc) error {
if _, ok := a.handlers[strings.ToLower(name)]; ok {
return errors.New("handler already exists")
}
a.handlers[strings.ToLower(name)] = handler{
desc: desc,
args: args,
handler: handlerfunc,
}
@@ -63,101 +71,142 @@ func (a *AdminSocket) AddHandler(name string, args []string, handlerfunc func(js
}
// Init runs the initial admin setup.
func (a *AdminSocket) Init(c *core.Core, nc *config.NodeConfig, log *log.Logger, options interface{}) error {
a.core = c
a.log = log
a.handlers = make(map[string]handler)
nc.RLock()
a.listenaddr = nc.AdminListen
nc.RUnlock()
a.done = make(chan struct{})
close(a.done) // Start in a done / not-started state
_ = a.AddHandler("list", []string{}, func(_ json.RawMessage) (interface{}, error) {
res := &ListResponse{
List: map[string]ListEntry{},
}
func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, error) {
a := &AdminSocket{
core: c,
log: log,
handlers: make(map[string]handler),
}
for _, opt := range opts {
a._applyOption(opt)
}
if a.config.listenaddr == "none" || a.config.listenaddr == "" {
return nil, nil
}
_ = a.AddHandler("list", "List available commands", []string{}, func(_ json.RawMessage) (interface{}, error) {
res := &ListResponse{}
for name, handler := range a.handlers {
res.List[name] = ListEntry{
Fields: handler.args,
}
res.List = append(res.List, ListEntry{
Command: name,
Description: handler.desc,
Fields: handler.args,
})
}
sort.SliceStable(res.List, func(i, j int) bool {
return strings.Compare(res.List[i].Command, res.List[j].Command) < 0
})
return res, nil
})
a.core.SetAdmin(a)
return nil
a.done = make(chan struct{})
go a.listen()
return a, a.core.SetAdmin(a)
}
func (a *AdminSocket) SetupAdminHandlers(na *AdminSocket) {
_ = a.AddHandler("getSelf", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetSelfRequest{}
res := &GetSelfResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getSelfHandler(req, res); err != nil {
return nil, err
}
return res, nil
})
_ = a.AddHandler("getPeers", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetPeersRequest{}
res := &GetPeersResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getPeersHandler(req, res); err != nil {
return nil, err
}
return res, nil
})
_ = a.AddHandler("getDHT", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetDHTRequest{}
res := &GetDHTResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getDHTHandler(req, res); err != nil {
return nil, err
}
return res, nil
})
_ = a.AddHandler("getPaths", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetPathsRequest{}
res := &GetPathsResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getPathsHandler(req, res); err != nil {
return nil, err
}
return res, nil
})
_ = a.AddHandler("getSessions", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetSessionsRequest{}
res := &GetSessionsResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getSessionsHandler(req, res); err != nil {
return nil, err
}
return res, nil
})
func (a *AdminSocket) SetupAdminHandlers() {
_ = a.AddHandler(
"getSelf", "Show details about this node", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetSelfRequest{}
res := &GetSelfResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getSelfHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getPeers", "Show directly connected peers", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetPeersRequest{}
res := &GetPeersResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getPeersHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getTree", "Show known Tree entries", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetTreeRequest{}
res := &GetTreeResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getTreeHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getPaths", "Show established paths through this node", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetPathsRequest{}
res := &GetPathsResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getPathsHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getSessions", "Show established traffic sessions with remote nodes", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetSessionsRequest{}
res := &GetSessionsResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getSessionsHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"addPeer", "Add a peer to the peer list", []string{"uri", "interface"},
func(in json.RawMessage) (interface{}, error) {
req := &AddPeerRequest{}
res := &AddPeerResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.addPeerHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"removePeer", "Remove a peer from the peer list", []string{"uri", "interface"},
func(in json.RawMessage) (interface{}, error) {
req := &RemovePeerRequest{}
res := &RemovePeerResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.removePeerHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
//_ = a.AddHandler("getNodeInfo", []string{"key"}, t.proto.nodeinfo.nodeInfoAdminHandler)
//_ = a.AddHandler("debug_remoteGetSelf", []string{"key"}, t.proto.getSelfHandler)
//_ = a.AddHandler("debug_remoteGetPeers", []string{"key"}, t.proto.getPeersHandler)
//_ = a.AddHandler("debug_remoteGetDHT", []string{"key"}, t.proto.getDHTHandler)
}
// Start runs the admin API socket to listen for / respond to admin API calls.
func (a *AdminSocket) Start() error {
if a.listenaddr != "none" && a.listenaddr != "" {
a.done = make(chan struct{})
go a.listen()
}
return nil
}
// IsStarted returns true if the module has been started.
func (a *AdminSocket) IsStarted() bool {
select {
@@ -172,6 +221,9 @@ func (a *AdminSocket) IsStarted() bool {
// Stop will stop the admin API and close the socket.
func (a *AdminSocket) Stop() error {
if a == nil {
return nil
}
if a.listener != nil {
select {
case <-a.done:
@@ -185,31 +237,32 @@ func (a *AdminSocket) Stop() error {
// listen is run by start and manages API connections.
func (a *AdminSocket) listen() {
u, err := url.Parse(a.listenaddr)
listenaddr := string(a.config.listenaddr)
u, err := url.Parse(listenaddr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
if _, err := os.Stat(a.listenaddr[7:]); err == nil {
a.log.Debugln("Admin socket", a.listenaddr[7:], "already exists, trying to clean up")
if _, err := net.DialTimeout("unix", a.listenaddr[7:], time.Second*2); err == nil || err.(net.Error).Timeout() {
a.log.Errorln("Admin socket", a.listenaddr[7:], "already exists and is in use by another process")
if _, err := os.Stat(listenaddr[7:]); err == nil {
a.log.Debugln("Admin socket", listenaddr[7:], "already exists, trying to clean up")
if _, err := net.DialTimeout("unix", listenaddr[7:], time.Second*2); err == nil || err.(net.Error).Timeout() {
a.log.Errorln("Admin socket", listenaddr[7:], "already exists and is in use by another process")
os.Exit(1)
} else {
if err := os.Remove(a.listenaddr[7:]); err == nil {
a.log.Debugln(a.listenaddr[7:], "was cleaned up")
if err := os.Remove(listenaddr[7:]); err == nil {
a.log.Debugln(listenaddr[7:], "was cleaned up")
} else {
a.log.Errorln(a.listenaddr[7:], "already exists and was not cleaned up:", err)
a.log.Errorln(listenaddr[7:], "already exists and was not cleaned up:", err)
os.Exit(1)
}
}
}
a.listener, err = net.Listen("unix", a.listenaddr[7:])
a.listener, err = net.Listen("unix", listenaddr[7:])
if err == nil {
switch a.listenaddr[7:8] {
switch listenaddr[7:8] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(a.listenaddr[7:], 0660); err != nil {
a.log.Warnln("WARNING:", a.listenaddr[:7], "may have unsafe permissions!")
if err := os.Chmod(listenaddr[7:], 0660); err != nil {
a.log.Warnln("WARNING:", listenaddr[:7], "may have unsafe permissions!")
}
}
}
@@ -217,10 +270,10 @@ func (a *AdminSocket) listen() {
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)
a.listener, err = net.Listen("tcp", listenaddr)
}
} else {
a.listener, err = net.Listen("tcp", a.listenaddr)
a.listener, err = net.Listen("tcp", listenaddr)
}
if err != nil {
a.log.Errorf("Admin socket failed to listen: %v", err)
@@ -256,53 +309,80 @@ func (a *AdminSocket) handleRequest(conn net.Conn) {
defer conn.Close()
defer func() {
r := recover()
if r != nil {
a.log.Debugln("Admin socket error:", r)
if err := encoder.Encode(&ErrorResponse{
Error: "Check your syntax and input types",
}); err != nil {
a.log.Debugln("Admin socket JSON encode error:", err)
/*
defer func() {
r := recover()
if r != nil {
fmt.Println("ERROR:", r)
a.log.Debugln("Admin socket error:", r)
if err := encoder.Encode(&ErrorResponse{
Error: "Check your syntax and input types",
}); err != nil {
fmt.Println("ERROR 2:", err)
a.log.Debugln("Admin socket JSON encode error:", err)
}
conn.Close()
}
conn.Close()
}
}()
}()
*/
for {
var err error
var buf json.RawMessage
_ = decoder.Decode(&buf)
var req AdminSocketRequest
var resp AdminSocketResponse
resp.Status = "success"
if err = json.Unmarshal(buf, &resp.Request); err == nil {
if resp.Request.Name == "" {
resp.Status = "error"
resp.Response = &ErrorResponse{
Error: "No request specified",
}
} else if h, ok := a.handlers[strings.ToLower(resp.Request.Name)]; ok {
resp.Response, err = h.handler(buf)
if err != nil {
resp.Status = "error"
resp.Response = &ErrorResponse{
Error: err.Error(),
}
}
} else {
resp.Status = "error"
resp.Response = &ErrorResponse{
Error: fmt.Sprintf("Unknown action '%s', try 'list' for help", resp.Request.Name),
}
req.Arguments = []byte("{}")
if err := func() error {
if err = decoder.Decode(&buf); err != nil {
return fmt.Errorf("Failed to find request")
}
if err = json.Unmarshal(buf, &req); err != nil {
return fmt.Errorf("Failed to unmarshal request")
}
resp.Request = req
if req.Name == "" {
return fmt.Errorf("No request specified")
}
reqname := strings.ToLower(req.Name)
handler, ok := a.handlers[reqname]
if !ok {
return fmt.Errorf("Unknown action '%s', try 'list' for help", reqname)
}
res, err := handler.handler(req.Arguments)
if err != nil {
return err
}
if resp.Response, err = json.Marshal(res); err != nil {
return fmt.Errorf("Failed to marshal response: %w", err)
}
resp.Status = "success"
return nil
}(); err != nil {
resp.Status = "error"
resp.Error = err.Error()
}
if err = encoder.Encode(resp); err != nil {
a.log.Debugln("Encode error:", err)
}
if !resp.Request.KeepAlive {
if !req.KeepAlive {
break
} else {
continue
}
}
}
type DataUnit uint64
func (d DataUnit) String() string {
switch {
case d > 1024*1024*1024*1024:
return fmt.Sprintf("%2.ftb", float64(d)/1024/1024/1024/1024)
case d > 1024*1024*1024:
return fmt.Sprintf("%2.fgb", float64(d)/1024/1024/1024)
case d > 1024*1024:
return fmt.Sprintf("%2.fmb", float64(d)/1024/1024)
default:
return fmt.Sprintf("%2.fkb", float64(d)/1024)
}
}

View File

@@ -1,34 +0,0 @@
package admin
import (
"encoding/hex"
"net"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type GetDHTRequest struct{}
type GetDHTResponse struct {
DHT map[string]DHTEntry `json:"dht"`
}
type DHTEntry struct {
PublicKey string `json:"key"`
Port uint64 `json:"port"`
Rest uint64 `json:"rest"`
}
func (a *AdminSocket) getDHTHandler(req *GetDHTRequest, res *GetDHTResponse) error {
res.DHT = map[string]DHTEntry{}
for _, d := range a.core.GetDHT() {
addr := address.AddrForKey(d.Key)
so := net.IP(addr[:]).String()
res.DHT[so] = DHTEntry{
PublicKey: hex.EncodeToString(d.Key[:]),
Port: d.Port,
Rest: d.Rest,
}
}
return nil
}

View File

@@ -3,6 +3,8 @@ package admin
import (
"encoding/hex"
"net"
"sort"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
@@ -11,23 +13,30 @@ type GetPathsRequest struct {
}
type GetPathsResponse struct {
Paths map[string]PathEntry `json:"paths"`
Paths []PathEntry `json:"paths"`
}
type PathEntry struct {
IPAddress string `json:"address"`
PublicKey string `json:"key"`
Path []uint64 `json:"path"`
Sequence uint64 `json:"sequence"`
}
func (a *AdminSocket) getPathsHandler(req *GetPathsRequest, res *GetPathsResponse) error {
res.Paths = map[string]PathEntry{}
for _, p := range a.core.GetPaths() {
paths := a.core.GetPaths()
res.Paths = make([]PathEntry, 0, len(paths))
for _, p := range paths {
addr := address.AddrForKey(p.Key)
so := net.IP(addr[:]).String()
res.Paths[so] = PathEntry{
res.Paths = append(res.Paths, PathEntry{
IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(p.Key),
Path: p.Path,
}
Sequence: p.Sequence,
})
}
sort.SliceStable(res.Paths, func(i, j int) bool {
return strings.Compare(res.Paths[i].PublicKey, res.Paths[j].PublicKey) < 0
})
return nil
}

View File

@@ -3,6 +3,8 @@ package admin
import (
"encoding/hex"
"net"
"sort"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
@@ -11,27 +13,53 @@ type GetPeersRequest struct {
}
type GetPeersResponse struct {
Peers map[string]PeerEntry `json:"peers"`
Peers []PeerEntry `json:"peers"`
}
type PeerEntry struct {
PublicKey string `json:"key"`
Port uint64 `json:"port"`
Coords []uint64 `json:"coords"`
Remote string `json:"remote"`
URI string `json:"remote,omitempty"`
Up bool `json:"up"`
Inbound bool `json:"inbound"`
IPAddress string `json:"address,omitempty"`
PublicKey string `json:"key"`
Port uint64 `json:"port"`
Priority uint64 `json:"priority"`
RXBytes DataUnit `json:"bytes_recvd,omitempty"`
TXBytes DataUnit `json:"bytes_sent,omitempty"`
Uptime float64 `json:"uptime,omitempty"`
LastError string `json:"last_error,omitempty"`
LastErrorTime time.Duration `json:"last_error_time,omitempty"`
}
func (a *AdminSocket) getPeersHandler(req *GetPeersRequest, res *GetPeersResponse) error {
res.Peers = map[string]PeerEntry{}
for _, p := range a.core.GetPeers() {
addr := address.AddrForKey(p.Key)
so := net.IP(addr[:]).String()
res.Peers[so] = PeerEntry{
PublicKey: hex.EncodeToString(p.Key),
Port: p.Port,
Coords: p.Coords,
Remote: p.Remote,
peers := a.core.GetPeers()
res.Peers = make([]PeerEntry, 0, len(peers))
for _, p := range peers {
peer := PeerEntry{
Port: p.Port,
Up: p.Up,
Inbound: p.Inbound,
Priority: uint64(p.Priority), // can't be uint8 thanks to gobind
URI: p.URI,
RXBytes: DataUnit(p.RXBytes),
TXBytes: DataUnit(p.TXBytes),
Uptime: p.Uptime.Seconds(),
}
if addr := address.AddrForKey(p.Key); addr != nil {
peer.PublicKey = hex.EncodeToString(p.Key)
peer.IPAddress = net.IP(addr[:]).String()
}
if p.LastError != nil {
peer.LastError = p.LastError.Error()
peer.LastErrorTime = time.Since(p.LastErrorTime)
}
res.Peers = append(res.Peers, peer)
}
sort.Slice(res.Peers, func(i, j int) bool {
if res.Peers[i].Port == res.Peers[j].Port {
return res.Peers[i].Priority < res.Peers[j].Priority
}
return res.Peers[i].Port < res.Peers[j].Port
})
return nil
}

View File

@@ -9,28 +9,22 @@ import (
type GetSelfRequest struct{}
type GetSelfResponse struct {
Self map[string]SelfEntry `json:"self"`
}
type SelfEntry struct {
BuildName string `json:"build_name"`
BuildVersion string `json:"build_version"`
PublicKey string `json:"key"`
Coords []uint64 `json:"coords"`
Subnet string `json:"subnet"`
BuildName string `json:"build_name"`
BuildVersion string `json:"build_version"`
PublicKey string `json:"key"`
IPAddress string `json:"address"`
RoutingEntries uint64 `json:"routing_entries"`
Subnet string `json:"subnet"`
}
func (a *AdminSocket) getSelfHandler(req *GetSelfRequest, res *GetSelfResponse) error {
res.Self = make(map[string]SelfEntry)
self := a.core.GetSelf()
addr := a.core.Address().String()
snet := a.core.Subnet()
res.Self[addr] = SelfEntry{
BuildName: version.BuildName(),
BuildVersion: version.BuildVersion(),
PublicKey: hex.EncodeToString(self.Key[:]),
Subnet: snet.String(),
Coords: self.Coords,
}
res.BuildName = version.BuildName()
res.BuildVersion = version.BuildVersion()
res.PublicKey = hex.EncodeToString(self.Key[:])
res.IPAddress = a.core.Address().String()
res.Subnet = snet.String()
res.RoutingEntries = self.RoutingEntries
return nil
}

View File

@@ -3,6 +3,8 @@ package admin
import (
"encoding/hex"
"net"
"sort"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
@@ -10,21 +12,32 @@ import (
type GetSessionsRequest struct{}
type GetSessionsResponse struct {
Sessions map[string]SessionEntry `json:"sessions"`
Sessions []SessionEntry `json:"sessions"`
}
type SessionEntry struct {
PublicKey string `json:"key"`
IPAddress string `json:"address"`
PublicKey string `json:"key"`
RXBytes DataUnit `json:"bytes_recvd"`
TXBytes DataUnit `json:"bytes_sent"`
Uptime float64 `json:"uptime"`
}
func (a *AdminSocket) getSessionsHandler(req *GetSessionsRequest, res *GetSessionsResponse) error {
res.Sessions = map[string]SessionEntry{}
for _, s := range a.core.GetSessions() {
sessions := a.core.GetSessions()
res.Sessions = make([]SessionEntry, 0, len(sessions))
for _, s := range sessions {
addr := address.AddrForKey(s.Key)
so := net.IP(addr[:]).String()
res.Sessions[so] = SessionEntry{
res.Sessions = append(res.Sessions, SessionEntry{
IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(s.Key[:]),
}
RXBytes: DataUnit(s.RXBytes),
TXBytes: DataUnit(s.TXBytes),
Uptime: s.Uptime.Seconds(),
})
}
sort.SliceStable(res.Sessions, func(i, j int) bool {
return strings.Compare(res.Sessions[i].PublicKey, res.Sessions[j].PublicKey) < 0
})
return nil
}

45
src/admin/gettree.go Normal file
View File

@@ -0,0 +1,45 @@
package admin
import (
"encoding/hex"
"net"
"sort"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type GetTreeRequest struct{}
type GetTreeResponse struct {
Tree []TreeEntry `json:"tree"`
}
type TreeEntry struct {
IPAddress string `json:"address"`
PublicKey string `json:"key"`
Parent string `json:"parent"`
Sequence uint64 `json:"sequence"`
//Port uint64 `json:"port"`
//Rest uint64 `json:"rest"`
}
func (a *AdminSocket) getTreeHandler(req *GetTreeRequest, res *GetTreeResponse) error {
tree := a.core.GetTree()
res.Tree = make([]TreeEntry, 0, len(tree))
for _, d := range tree {
addr := address.AddrForKey(d.Key)
res.Tree = append(res.Tree, TreeEntry{
IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(d.Key[:]),
Parent: hex.EncodeToString(d.Parent[:]),
Sequence: d.Sequence,
//Port: d.Port,
//Rest: d.Rest,
})
}
sort.SliceStable(res.Tree, func(i, j int) bool {
return strings.Compare(res.Tree[i].PublicKey, res.Tree[j].PublicKey) < 0
})
return nil
}

16
src/admin/options.go Normal file
View File

@@ -0,0 +1,16 @@
package admin
func (c *AdminSocket) _applyOption(opt SetupOption) {
switch v := opt.(type) {
case ListenAddress:
c.config.listenaddr = v
}
}
type SetupOption interface {
isSetupOption()
}
type ListenAddress string
func (a ListenAddress) isSetupOption() {}

12
src/admin/removepeer.go Normal file
View File

@@ -0,0 +1,12 @@
package admin
type RemovePeerRequest struct {
Uri string `json:"uri"`
Sintf string `json:"interface,omitempty"`
}
type RemovePeerResponse struct{}
func (a *AdminSocket) removePeerHandler(req *RemovePeerRequest, res *RemovePeerResponse) error {
return a.core.RemovePeer(req.Uri, req.Sintf)
}

View File

@@ -17,45 +17,309 @@ configuration option that is not provided.
package config
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"sync"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"os"
"time"
"github.com/hjson/hjson-go/v4"
"golang.org/x/text/encoding/unicode"
)
// NodeConfig is the main configuration structure, containing configuration
// options that are necessary for an Yggdrasil node to run. You will need to
// supply one of these structs to the Yggdrasil core when starting a node.
type NodeConfig struct {
sync.RWMutex `json:"-"`
Peers []string `comment:"List of connection strings for outbound peer connections in URI format,\ne.g. tls://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections\nwill obey the operating system routing table, therefore you should\nuse this section when you may connect via different interfaces."`
InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ tls://a.b.c.d:e ] }.\nNote that SOCKS peerings will NOT be affected by this option and should\ngo in the \"Peers\" section instead."`
Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."`
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."`
MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Each entry in the list should be a json object which may\ncontain Regex, Beacon, Listen, and Port. Regex is a regular expression\nwhich is matched against an interface name, and interfaces use the\nfirst configuration that they match gainst. Beacon configures whether\nor not the node should send link-local multicast beacons to advertise\ntheir presence, while listening for incoming connections on Port.\nListen controls whether or not the node listens for multicast beacons\nand opens outgoing connections."`
AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast."`
PublicKey string `comment:"Your public key. Your peers may ask you for this to put\ninto their AllowedPublicKeys configuration."`
PrivateKey string `comment:"Your private key. DO NOT share this with anyone!"`
IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."`
IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
NodeInfo map[string]interface{} `comment:"Optional node info. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
PrivateKey KeyBytes `comment:"Your private key. DO NOT share this with anyone!"`
PrivateKeyPath string `json:",omitempty"`
Certificate *tls.Certificate `json:"-"`
CertificatePath string `json:",omitempty"`
RootCertificates []*x509.Certificate `json:"-"`
RootCertificatePaths []string `json:",omitempty"`
Peers []string `comment:"List of connection strings for outbound peer connections in URI format,\ne.g. tls://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections\nwill obey the operating system routing table, therefore you should\nuse this section when you may connect via different interfaces."`
InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ \"tls://a.b.c.d:e\" ] }.\nNote that SOCKS peerings will NOT be affected by this option and should\ngo in the \"Peers\" section instead."`
Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."`
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."`
MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Each entry in the list should be a json object which may\ncontain Regex, Beacon, Listen, and Port. Regex is a regular expression\nwhich is matched against an interface name, and interfaces use the\nfirst configuration that they match gainst. Beacon configures whether\nor not the node should send link-local multicast beacons to advertise\ntheir presence, while listening for incoming connections on Port.\nListen controls whether or not the node listens for multicast beacons\nand opens outgoing connections."`
AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast."`
IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."`
IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
NodeInfo map[string]interface{} `comment:"Optional node info. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
}
type MulticastInterfaceConfig struct {
Regex string
Beacon bool
Listen bool
Port uint16
Regex string
Beacon bool
Listen bool
Port uint16
Priority uint64 // really uint8, but gobind won't export it
}
// NewSigningKeys replaces the signing keypair in the NodeConfig with a new
// signing keypair. The signing keys are used by the switch to derive the
// structure of the spanning tree.
func (cfg *NodeConfig) NewKeys() {
spub, spriv, err := ed25519.GenerateKey(nil)
// Generates default configuration and returns a pointer to the resulting
// NodeConfig. This is used when outputting the -genconf parameter and also when
// using -autoconf.
func GenerateConfig() *NodeConfig {
// Get the defaults for the platform.
defaults := GetDefaults()
// Create a node configuration and populate it.
cfg := new(NodeConfig)
cfg.NewPrivateKey()
cfg.Listen = []string{}
cfg.AdminListen = defaults.DefaultAdminListen
cfg.Peers = []string{}
cfg.InterfacePeers = map[string][]string{}
cfg.AllowedPublicKeys = []string{}
cfg.MulticastInterfaces = defaults.DefaultMulticastInterfaces
cfg.IfName = defaults.DefaultIfName
cfg.IfMTU = defaults.DefaultIfMTU
cfg.NodeInfoPrivacy = false
if err := cfg.postprocessConfig(); err != nil {
panic(err)
}
return cfg
}
func (cfg *NodeConfig) ReadFrom(r io.Reader) (int64, error) {
conf, err := io.ReadAll(r)
if err != nil {
return 0, err
}
n := int64(len(conf))
// 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.Equal(conf[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
conf, err = decoder.Bytes(conf)
if err != nil {
return n, 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
// configuration will use a sane default.
*cfg = *GenerateConfig()
if err := cfg.UnmarshalHJSON(conf); err != nil {
return n, err
}
return n, nil
}
func (cfg *NodeConfig) UnmarshalHJSON(b []byte) error {
if err := hjson.Unmarshal(b, cfg); err != nil {
return err
}
return cfg.postprocessConfig()
}
func (cfg *NodeConfig) postprocessConfig() error {
if cfg.PrivateKeyPath != "" {
cfg.PrivateKey = nil
f, err := os.ReadFile(cfg.PrivateKeyPath)
if err != nil {
return err
}
if err := cfg.UnmarshalPEMPrivateKey(f); err != nil {
return err
}
}
if cfg.CertificatePath != "" {
if cfg.PrivateKeyPath == "" {
return fmt.Errorf("CertificatePath requires PrivateKeyPath")
}
cfg.Certificate = nil
f, err := os.ReadFile(cfg.CertificatePath)
if err != nil {
return err
}
if err := cfg.UnmarshalPEMCertificate(f); err != nil {
return err
}
}
if cfg.Certificate == nil {
if err := cfg.GenerateSelfSignedCertificate(); err != nil {
return err
}
}
cfg.RootCertificates = cfg.RootCertificates[:0]
for _, path := range cfg.RootCertificatePaths {
f, err := os.ReadFile(path)
if err != nil {
return err
}
if err := cfg.UnmarshalRootCertificate(f); err != nil {
return err
}
}
return nil
}
func (cfg *NodeConfig) UnmarshalRootCertificate(b []byte) error {
p, _ := pem.Decode(b)
if p == nil {
return fmt.Errorf("failed to parse PEM file")
}
if p.Type != "CERTIFICATE" {
return fmt.Errorf("unexpected PEM type %q", p.Type)
}
cert, err := x509.ParseCertificate(p.Bytes)
if err != nil {
return fmt.Errorf("failed to load X.509 keypair: %w", err)
}
if !cert.IsCA {
return fmt.Errorf("supplied root certificate is not a certificate authority")
}
cfg.RootCertificates = append(cfg.RootCertificates, cert)
return nil
}
// RFC5280 section 4.1.2.5
var notAfterNeverExpires = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)
func (cfg *NodeConfig) GenerateSelfSignedCertificate() error {
key, err := cfg.MarshalPEMPrivateKey()
if err != nil {
return err
}
cert, err := cfg.MarshalPEMCertificate()
if err != nil {
return err
}
tlsCert, err := tls.X509KeyPair(cert, key)
if err != nil {
return err
}
cfg.Certificate = &tlsCert
return nil
}
func (cfg *NodeConfig) GenerateCertificateSigningRequest() ([]byte, error) {
template := &x509.CertificateRequest{
Subject: pkix.Name{
CommonName: hex.EncodeToString(cfg.PrivateKey),
},
SignatureAlgorithm: x509.PureEd25519,
}
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, ed25519.PrivateKey(cfg.PrivateKey))
if err != nil {
return nil, err
}
pemBytes := bytes.NewBuffer(nil)
if err := pem.Encode(pemBytes, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}); err != nil {
return nil, err
}
return pemBytes.Bytes(), nil
}
func (cfg *NodeConfig) MarshalPEMCertificate() ([]byte, error) {
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
publicKey := privateKey.Public().(ed25519.PublicKey)
cert := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: hex.EncodeToString(publicKey),
},
NotBefore: time.Now(),
NotAfter: notAfterNeverExpires,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
certbytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey, privateKey)
if err != nil {
return nil, err
}
block := &pem.Block{
Type: "CERTIFICATE",
Bytes: certbytes,
}
return pem.EncodeToMemory(block), nil
}
func (cfg *NodeConfig) UnmarshalPEMCertificate(b []byte) error {
tlsCert, err := tls.LoadX509KeyPair(cfg.CertificatePath, cfg.PrivateKeyPath)
if err != nil {
return fmt.Errorf("failed to load X.509 keypair: %w", err)
}
cfg.Certificate = &tlsCert
return nil
}
func (cfg *NodeConfig) NewPrivateKey() {
_, spriv, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
cfg.PublicKey = hex.EncodeToString(spub[:])
cfg.PrivateKey = hex.EncodeToString(spriv[:])
cfg.PrivateKey = KeyBytes(spriv)
}
func (cfg *NodeConfig) MarshalPEMPrivateKey() ([]byte, error) {
b, err := x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(cfg.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to marshal PKCS8 key: %w", err)
}
block := &pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
}
return pem.EncodeToMemory(block), nil
}
func (cfg *NodeConfig) UnmarshalPEMPrivateKey(b []byte) error {
p, _ := pem.Decode(b)
if p == nil {
return fmt.Errorf("failed to parse PEM file")
}
if p.Type != "PRIVATE KEY" {
return fmt.Errorf("unexpected PEM type %q", p.Type)
}
k, err := x509.ParsePKCS8PrivateKey(p.Bytes)
if err != nil {
return fmt.Errorf("failed to unmarshal PKCS8 key: %w", err)
}
key, ok := k.(ed25519.PrivateKey)
if !ok {
return fmt.Errorf("private key must be ed25519 key")
}
if len(key) != ed25519.PrivateKeySize {
return fmt.Errorf("unexpected ed25519 private key length")
}
cfg.PrivateKey = KeyBytes(key)
return nil
}
type KeyBytes []byte
func (k KeyBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(hex.EncodeToString(k))
}
func (k *KeyBytes) UnmarshalJSON(b []byte) error {
var s string
var err error
if err = json.Unmarshal(b, &s); err != nil {
return err
}
*k, err = hex.DecodeString(s)
return err
}

54
src/config/config_test.go Normal file
View File

@@ -0,0 +1,54 @@
package config
import (
"testing"
)
func TestConfig_Keys(t *testing.T) {
/*
var nodeConfig NodeConfig
nodeConfig.NewKeys()
publicKey1, err := hex.DecodeString(nodeConfig.PublicKey)
if err != nil {
t.Fatal("can not decode generated public key")
}
if len(publicKey1) == 0 {
t.Fatal("empty public key generated")
}
privateKey1, err := hex.DecodeString(nodeConfig.PrivateKey)
if err != nil {
t.Fatal("can not decode generated private key")
}
if len(privateKey1) == 0 {
t.Fatal("empty private key generated")
}
nodeConfig.NewKeys()
publicKey2, err := hex.DecodeString(nodeConfig.PublicKey)
if err != nil {
t.Fatal("can not decode generated public key")
}
if bytes.Equal(publicKey2, publicKey1) {
t.Fatal("same public key generated")
}
privateKey2, err := hex.DecodeString(nodeConfig.PrivateKey)
if err != nil {
t.Fatal("can not decode generated private key")
}
if bytes.Equal(privateKey2, privateKey1) {
t.Fatal("same private key generated")
}
*/
}

34
src/config/defaults.go Normal file
View File

@@ -0,0 +1,34 @@
package config
var defaultConfig = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultConfig=/path/to/config
var defaultAdminListen = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultAdminListen=unix://path/to/sock'
// 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
// Multicast interfaces
DefaultMulticastInterfaces []MulticastInterfaceConfig
// TUN
MaximumIfMTU uint64
DefaultIfMTU uint64
DefaultIfName string
}
func GetDefaults() platformDefaultParameters {
defaults := getDefaults()
if defaultConfig != "" {
defaults.DefaultConfigFile = defaultConfig
}
if defaultAdminListen != "" {
defaults.DefaultAdminListen = defaultAdminListen
}
return defaults
}

View File

@@ -1,10 +1,11 @@
//go:build darwin
// +build darwin
package defaults
package config
// Sane defaults for the macOS/Darwin platform. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
func getDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@@ -18,7 +19,7 @@ func GetDefaults() platformDefaultParameters {
{Regex: "bridge.*", Beacon: true, Listen: true},
},
// TUN/TAP
// TUN
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "auto",

View File

@@ -1,10 +1,11 @@
//go:build freebsd
// +build freebsd
package defaults
package config
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
func getDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@@ -17,7 +18,7 @@ func GetDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true},
},
// TUN/TAP
// TUN
MaximumIfMTU: 32767,
DefaultIfMTU: 32767,
DefaultIfName: "/dev/tun0",

View File

@@ -1,10 +1,11 @@
//go:build linux
// +build linux
package defaults
package config
// Sane defaults for the Linux platform. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
func getDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@@ -17,7 +18,7 @@ func GetDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true},
},
// TUN/TAP
// TUN
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "auto",

View File

@@ -1,10 +1,11 @@
//go:build openbsd
// +build openbsd
package defaults
package config
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
func getDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@@ -17,7 +18,7 @@ func GetDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true},
},
// TUN/TAP
// TUN
MaximumIfMTU: 16384,
DefaultIfMTU: 16384,
DefaultIfName: "tun0",

View File

@@ -1,10 +1,11 @@
//go:build !linux && !darwin && !windows && !openbsd && !freebsd
// +build !linux,!darwin,!windows,!openbsd,!freebsd
package defaults
package config
// Sane defaults for the other platforms. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
func getDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "tcp://localhost:9001",
@@ -17,7 +18,7 @@ func GetDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true},
},
// TUN/TAP
// TUN
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "none",

View File

@@ -1,10 +1,11 @@
//go:build windows
// +build windows
package defaults
package config
// Sane defaults for the Windows platform. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
func getDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "tcp://localhost:9001",
@@ -17,7 +18,7 @@ func GetDefaults() platformDefaultParameters {
{Regex: ".*", Beacon: true, Listen: true},
},
// TUN/TAP
// TUN
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "Yggdrasil",

View File

@@ -2,114 +2,142 @@ package core
import (
"crypto/ed25519"
//"encoding/hex"
"encoding/json"
//"errors"
//"fmt"
"fmt"
"net"
"net/url"
//"sort"
//"time"
"sync/atomic"
"time"
"github.com/gologme/log"
"github.com/Arceliar/phony"
"github.com/Arceliar/ironwood/network"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
//"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
//"github.com/Arceliar/phony"
)
type Self struct {
Key ed25519.PublicKey
Root ed25519.PublicKey
Coords []uint64
type SelfInfo struct {
Key ed25519.PublicKey
RoutingEntries uint64
}
type Peer struct {
Key ed25519.PublicKey
Root ed25519.PublicKey
Coords []uint64
Port uint64
Remote string
type PeerInfo struct {
URI string
Up bool
Inbound bool
LastError error
LastErrorTime time.Time
Key ed25519.PublicKey
Root ed25519.PublicKey
Coords []uint64
Port uint64
Priority uint8
RXBytes uint64
TXBytes uint64
Uptime time.Duration
}
type DHTEntry struct {
Key ed25519.PublicKey
Port uint64
Rest uint64
type TreeEntryInfo struct {
Key ed25519.PublicKey
Parent ed25519.PublicKey
Sequence uint64
//Port uint64
//Rest uint64
}
type PathEntry struct {
Key ed25519.PublicKey
Path []uint64
type PathEntryInfo struct {
Key ed25519.PublicKey
Path []uint64
Sequence uint64
}
type Session struct {
Key ed25519.PublicKey
type SessionInfo struct {
Key ed25519.PublicKey
RXBytes uint64
TXBytes uint64
Uptime time.Duration
}
func (c *Core) GetSelf() Self {
var self Self
s := c.pc.PacketConn.Debug.GetSelf()
func (c *Core) GetSelf() SelfInfo {
var self SelfInfo
s := c.PacketConn.PacketConn.Debug.GetSelf()
self.Key = s.Key
self.Root = s.Root
self.Coords = s.Coords
self.RoutingEntries = s.RoutingEntries
return self
}
func (c *Core) GetPeers() []Peer {
var peers []Peer
names := make(map[net.Conn]string)
c.links.mutex.Lock()
for _, info := range c.links.links {
names[info.conn] = info.lname
func (c *Core) GetPeers() []PeerInfo {
peers := []PeerInfo{}
conns := map[net.Conn]network.DebugPeerInfo{}
iwpeers := c.PacketConn.PacketConn.Debug.GetPeers()
for _, p := range iwpeers {
conns[p.Conn] = p
}
c.links.mutex.Unlock()
ps := c.pc.PacketConn.Debug.GetPeers()
for _, p := range ps {
var info Peer
info.Key = p.Key
info.Root = p.Root
info.Coords = p.Coords
info.Port = p.Port
info.Remote = p.Conn.RemoteAddr().String()
if name := names[p.Conn]; name != "" {
info.Remote = name
phony.Block(&c.links, func() {
for info, state := range c.links._links {
var peerinfo PeerInfo
var conn net.Conn
peerinfo.URI = info.uri
peerinfo.LastError = state._err
peerinfo.LastErrorTime = state._errtime
if c := state._conn; c != nil {
conn = c
peerinfo.Up = true
peerinfo.Inbound = state.linkType == linkTypeIncoming
peerinfo.RXBytes = atomic.LoadUint64(&c.rx)
peerinfo.TXBytes = atomic.LoadUint64(&c.tx)
peerinfo.Uptime = time.Since(c.up)
}
if p, ok := conns[conn]; ok {
peerinfo.Key = p.Key
peerinfo.Root = p.Root
peerinfo.Port = p.Port
peerinfo.Priority = p.Priority
}
peers = append(peers, peerinfo)
}
peers = append(peers, info)
}
})
return peers
}
func (c *Core) GetDHT() []DHTEntry {
var dhts []DHTEntry
ds := c.pc.PacketConn.Debug.GetDHT()
for _, d := range ds {
var info DHTEntry
info.Key = d.Key
info.Port = d.Port
info.Rest = d.Rest
dhts = append(dhts, info)
func (c *Core) GetTree() []TreeEntryInfo {
var trees []TreeEntryInfo
ts := c.PacketConn.PacketConn.Debug.GetTree()
for _, t := range ts {
var info TreeEntryInfo
info.Key = t.Key
info.Parent = t.Parent
info.Sequence = t.Sequence
//info.Port = d.Port
//info.Rest = d.Rest
trees = append(trees, info)
}
return dhts
return trees
}
func (c *Core) GetPaths() []PathEntry {
var paths []PathEntry
ps := c.pc.PacketConn.Debug.GetPaths()
func (c *Core) GetPaths() []PathEntryInfo {
var paths []PathEntryInfo
ps := c.PacketConn.PacketConn.Debug.GetPaths()
for _, p := range ps {
var info PathEntry
var info PathEntryInfo
info.Key = p.Key
info.Sequence = p.Sequence
info.Path = p.Path
paths = append(paths, info)
}
return paths
}
func (c *Core) GetSessions() []Session {
var sessions []Session
ss := c.pc.Debug.GetSessions()
func (c *Core) GetSessions() []SessionInfo {
var sessions []SessionInfo
ss := c.PacketConn.Debug.GetSessions()
for _, s := range ss {
var info Session
var info SessionInfo
info.Key = s.Key
info.RXBytes = s.RX
info.TXBytes = s.TX
info.Uptime = s.Uptime
sessions = append(sessions, info)
}
return sessions
@@ -118,8 +146,8 @@ func (c *Core) GetSessions() []Session {
// Listen starts a new listener (either TCP or TLS). The input should be a url.URL
// parsed from a string of the form e.g. "tcp://a.b.c.d:e". In the case of a
// link-local address, the interface should be provided as the second argument.
func (c *Core) Listen(u *url.URL, sintf string) (*TcpListener, error) {
return c.links.tcp.listenURL(u, sintf)
func (c *Core) Listen(u *url.URL, sintf string) (*Listener, error) {
return c.links.listen(u, sintf)
}
// Address gets the IPv6 address of the Yggdrasil node. This is always a /128
@@ -147,154 +175,96 @@ func (c *Core) Subnet() net.IPNet {
// may be useful if you want to redirect the output later. Note that this
// expects a Logger from the github.com/gologme/log package and not from Go's
// built-in log package.
func (c *Core) SetLogger(log *log.Logger) {
func (c *Core) SetLogger(log Logger) {
c.log = log
}
// AddPeer adds a peer. This should be specified in the peer URI format, e.g.:
// tcp://a.b.c.d:e
// socks://a.b.c.d:e/f.g.h.i:j
//
// tcp://a.b.c.d:e
// socks://a.b.c.d:e/f.g.h.i:j
//
// This adds the peer to the peer list, so that they will be called again if the
// connection drops.
/*
func (c *Core) AddPeer(addr string, sintf string) error {
if err := c.CallPeer(addr, sintf); err != nil {
// TODO: We maybe want this to write the peer to the persistent
// configuration even if a connection attempt fails, but first we'll need to
// move the code to check the peer URI so that we don't deliberately save a
// peer with a known bad URI. Loading peers from config should really do the
// same thing too but I don't think that happens today
func (c *Core) AddPeer(u *url.URL, sintf string) error {
return c.links.add(u, sintf, linkTypePersistent)
}
// RemovePeer removes a peer. The peer should be specified in URI format, see AddPeer.
// The peer is not disconnected immediately.
func (c *Core) RemovePeer(uri string, sourceInterface string) error {
return fmt.Errorf("not implemented yet")
/*
var err error
phony.Block(c, func() {
peer := Peer{uri, sourceInterface}
linkInfo, ok := c.config._peers[peer]
if !ok {
err = fmt.Errorf("peer not configured")
return
}
if ok && linkInfo != nil {
c.links.Act(nil, func() {
if link := c.links._links[*linkInfo]; link != nil {
_ = link.conn.Close()
}
})
}
delete(c.config._peers, peer)
})
return err
}
c.config.Mutex.Lock()
defer c.config.Mutex.Unlock()
if sintf == "" {
for _, peer := range c.config.Current.Peers {
if peer == addr {
return errors.New("peer already added")
}
}
c.config.Current.Peers = append(c.config.Current.Peers, addr)
} else {
if _, ok := c.config.Current.InterfacePeers[sintf]; ok {
for _, peer := range c.config.Current.InterfacePeers[sintf] {
if peer == addr {
return errors.New("peer already added")
}
}
}
if _, ok := c.config.Current.InterfacePeers[sintf]; !ok {
c.config.Current.InterfacePeers[sintf] = []string{addr}
} else {
c.config.Current.InterfacePeers[sintf] = append(c.config.Current.InterfacePeers[sintf], addr)
}
}
return nil
*/
}
*/
/*
func (c *Core) RemovePeer(addr string, sintf string) error {
if sintf == "" {
for i, peer := range c.config.Current.Peers {
if peer == addr {
c.config.Current.Peers = append(c.config.Current.Peers[:i], c.config.Current.Peers[i+1:]...)
break
}
}
} else if _, ok := c.config.Current.InterfacePeers[sintf]; ok {
for i, peer := range c.config.Current.InterfacePeers[sintf] {
if peer == addr {
c.config.Current.InterfacePeers[sintf] = append(c.config.Current.InterfacePeers[sintf][:i], c.config.Current.InterfacePeers[sintf][i+1:]...)
break
}
}
}
panic("TODO") // Get the net.Conn to this peer (if any) and close it
c.peers.Act(nil, func() {
ports := c.peers.ports
for _, peer := range ports {
if addr == peer.intf.name() {
c.peers._removePeer(peer)
}
}
})
return nil
}
*/
// CallPeer calls a peer once. This should be specified in the peer URI format,
// e.g.:
// tcp://a.b.c.d:e
// socks://a.b.c.d:e/f.g.h.i:j
//
// tcp://a.b.c.d:e
// socks://a.b.c.d:e/f.g.h.i:j
//
// This does not add the peer to the peer list, so if the connection drops, the
// peer will not be called again automatically.
func (c *Core) CallPeer(u *url.URL, sintf string) error {
return c.links.call(u, sintf)
return c.links.add(u, sintf, linkTypeEphemeral)
}
func (c *Core) PublicKey() ed25519.PublicKey {
return c.public
}
func (c *Core) MaxMTU() uint64 {
return c.store.maxSessionMTU()
}
func (c *Core) SetMTU(mtu uint64) {
if mtu < 1280 {
mtu = 1280
}
c.store.mutex.Lock()
c.store.mtu = mtu
c.store.mutex.Unlock()
}
func (c *Core) MTU() uint64 {
c.store.mutex.Lock()
mtu := c.store.mtu
c.store.mutex.Unlock()
return mtu
}
// Implement io.ReadWriteCloser
func (c *Core) Read(p []byte) (n int, err error) {
n, err = c.store.readPC(p)
return
}
func (c *Core) Write(p []byte) (n int, err error) {
n, err = c.store.writePC(p)
return
}
func (c *Core) Close() error {
c.Stop()
return nil
}
// Hack to get the admin stuff working, TODO something cleaner
type AddHandler interface {
AddHandler(name string, args []string, handlerfunc func(json.RawMessage) (interface{}, error)) error
AddHandler(name, desc string, args []string, handlerfunc AddHandlerFunc) error
}
type AddHandlerFunc func(json.RawMessage) (interface{}, error)
// SetAdmin must be called after Init and before Start.
// It sets the admin handler for NodeInfo and the Debug admin functions.
func (c *Core) SetAdmin(a AddHandler) error {
if err := a.AddHandler("getNodeInfo", []string{"key"}, c.proto.nodeinfo.nodeInfoAdminHandler); err != nil {
if err := a.AddHandler(
"getNodeInfo", "Request nodeinfo from a remote node by its public key", []string{"key"},
c.proto.nodeinfo.nodeInfoAdminHandler,
); err != nil {
return err
}
if err := a.AddHandler("debug_remoteGetSelf", []string{"key"}, c.proto.getSelfHandler); err != nil {
if err := a.AddHandler(
"debug_remoteGetSelf", "Debug use only", []string{"key"},
c.proto.getSelfHandler,
); err != nil {
return err
}
if err := a.AddHandler("debug_remoteGetPeers", []string{"key"}, c.proto.getPeersHandler); err != nil {
if err := a.AddHandler(
"debug_remoteGetPeers", "Debug use only", []string{"key"},
c.proto.getPeersHandler,
); err != nil {
return err
}
if err := a.AddHandler("debug_remoteGetDHT", []string{"key"}, c.proto.getDHTHandler); err != nil {
if err := a.AddHandler(
"debug_remoteGetTree", "Debug use only", []string{"key"},
c.proto.getTreeHandler,
); err != nil {
return err
}
return nil

View File

@@ -3,19 +3,22 @@ package core
import (
"context"
"crypto/ed25519"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"errors"
"fmt"
"io/ioutil"
"io"
"net"
"net/url"
"time"
iw "github.com/Arceliar/ironwood/encrypted"
iwe "github.com/Arceliar/ironwood/encrypted"
iwn "github.com/Arceliar/ironwood/network"
iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
//"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
@@ -26,113 +29,36 @@ type Core struct {
// We're going to keep our own copy of the provided config - that way we can
// guarantee that it will be covered by the mutex
phony.Inbox
pc *iw.PacketConn
config *config.NodeConfig // Config
*iwe.PacketConn
ctx context.Context
cancel context.CancelFunc
secret ed25519.PrivateKey
public ed25519.PublicKey
links links
proto protoHandler
store keyStore
log *log.Logger
log Logger
addPeerTimer *time.Timer
ctx context.Context
ctxCancel context.CancelFunc
config struct {
tls *tls.Config // immutable after startup
roots *x509.CertPool // immutable after startup
//_peers map[Peer]*linkInfo // configurable after startup
_listeners map[ListenAddress]struct{} // configurable after startup
nodeinfo NodeInfo // immutable after startup
nodeinfoPrivacy NodeInfoPrivacy // immutable after startup
_allowedPublicKeys map[[32]byte]struct{} // configurable after startup
}
pathNotify func(ed25519.PublicKey)
}
func (c *Core) _init() error {
// TODO separate init and start functions
// Init sets up structs
// Start launches goroutines that depend on structs being set up
// This is pretty much required to completely avoid race conditions
c.config.RLock()
defer c.config.RUnlock()
func New(cert *tls.Certificate, logger Logger, opts ...SetupOption) (*Core, error) {
c := &Core{
log: logger,
}
c.ctx, c.cancel = context.WithCancel(context.Background())
if c.log == nil {
c.log = log.New(ioutil.Discard, "", 0)
c.log = log.New(io.Discard, "", 0)
}
sigPriv, err := hex.DecodeString(c.config.PrivateKey)
if err != nil {
return err
}
if len(sigPriv) < ed25519.PrivateKeySize {
return errors.New("PrivateKey is incorrect length")
}
c.secret = ed25519.PrivateKey(sigPriv)
c.public = c.secret.Public().(ed25519.PublicKey)
// TODO check public against current.PublicKey, error if they don't match
c.pc, err = iw.NewPacketConn(c.secret)
c.ctx, c.ctxCancel = context.WithCancel(context.Background())
c.store.init(c)
c.proto.init(c)
if err := c.proto.nodeinfo.setNodeInfo(c.config.NodeInfo, c.config.NodeInfoPrivacy); err != nil {
return fmt.Errorf("setNodeInfo: %w", err)
}
return err
}
// If any static peers were provided in the configuration above then we should
// configure them. The loop ensures that disconnected peers will eventually
// be reconnected with.
func (c *Core) _addPeerLoop() {
c.config.RLock()
defer c.config.RUnlock()
if c.addPeerTimer == nil {
return
}
// Add peers from the Peers section
for _, peer := range c.config.Peers {
go func(peer string, intf string) {
u, err := url.Parse(peer)
if err != nil {
c.log.Errorln("Failed to parse peer url:", peer, err)
}
if err := c.CallPeer(u, intf); err != nil {
c.log.Errorln("Failed to add peer:", err)
}
}(peer, "") // TODO: this should be acted and not in a goroutine?
}
// Add peers from the InterfacePeers section
for intf, intfpeers := range c.config.InterfacePeers {
for _, peer := range intfpeers {
go func(peer string, intf string) {
u, err := url.Parse(peer)
if err != nil {
c.log.Errorln("Failed to parse peer url:", peer, err)
}
if err := c.CallPeer(u, intf); err != nil {
c.log.Errorln("Failed to add peer:", err)
}
}(peer, intf) // TODO: this should be acted and not in a goroutine?
}
}
c.addPeerTimer = time.AfterFunc(time.Minute, func() {
c.Act(nil, c._addPeerLoop)
})
}
// Start starts up Yggdrasil using the provided config.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. A config.NodeState is returned which contains both the
// current and previous configurations (from reconfigures).
func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) (err error) {
phony.Block(c, func() {
err = c._start(nc, log)
})
return
}
// This function is unsafe and should only be ran by the core actor.
func (c *Core) _start(nc *config.NodeConfig, log *log.Logger) error {
c.log = log
c.config = nc
if name := version.BuildName(); name != "unknown" {
c.log.Infoln("Build name:", name)
}
@@ -140,44 +66,193 @@ func (c *Core) _start(nc *config.NodeConfig, log *log.Logger) error {
c.log.Infoln("Build version:", version)
}
c.log.Infoln("Starting up...")
if err := c._init(); err != nil {
c.log.Errorln("Failed to initialize core")
return err
var err error
c.config._listeners = map[ListenAddress]struct{}{}
c.config._allowedPublicKeys = map[[32]byte]struct{}{}
for _, opt := range opts {
switch opt.(type) {
case Peer, ListenAddress:
// We can't do peers yet as the links aren't set up.
continue
default:
if err = c._applyOption(opt); err != nil {
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
}
}
}
if cert == nil || cert.PrivateKey == nil {
return nil, fmt.Errorf("no private key supplied")
}
var ok bool
if c.secret, ok = cert.PrivateKey.(ed25519.PrivateKey); !ok {
return nil, fmt.Errorf("private key must be ed25519")
}
if len(c.secret) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("private key is incorrect length")
}
c.public = c.secret.Public().(ed25519.PublicKey)
if c.config.tls, err = c.generateTLSConfig(cert); err != nil {
return nil, fmt.Errorf("error generating TLS config: %w", err)
}
keyXform := func(key ed25519.PublicKey) ed25519.PublicKey {
return address.SubnetForKey(key).GetKey()
}
if c.PacketConn, err = iwe.NewPacketConn(
c.secret,
iwn.WithBloomTransform(keyXform),
iwn.WithPeerMaxMessageSize(65535*2),
iwn.WithPathNotify(c.doPathNotify),
); err != nil {
return nil, fmt.Errorf("error creating encryption: %w", err)
}
address, subnet := c.Address(), c.Subnet()
c.log.Infof("Your public key is %s", hex.EncodeToString(c.public))
c.log.Infof("Your IPv6 address is %s", address.String())
c.log.Infof("Your IPv6 subnet is %s", subnet.String())
if c.config.roots != nil {
c.log.Println("Yggdrasil is running in TLS-only mode")
}
c.proto.init(c)
if err := c.links.init(c); err != nil {
c.log.Errorln("Failed to start link interfaces")
return err
return nil, fmt.Errorf("error initialising links: %w", err)
}
for _, opt := range opts {
switch opt.(type) {
case Peer, ListenAddress:
// Now do the peers and listeners.
if err = c._applyOption(opt); err != nil {
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
}
default:
continue
}
}
if err := c.proto.nodeinfo.setNodeInfo(c.config.nodeinfo, bool(c.config.nodeinfoPrivacy)); err != nil {
return nil, fmt.Errorf("error setting node info: %w", err)
}
for listenaddr := range c.config._listeners {
u, err := url.Parse(string(listenaddr))
if err != nil {
c.log.Errorf("Invalid listener URI %q specified, ignoring\n", listenaddr)
continue
}
if _, err = c.links.listen(u, ""); err != nil {
c.log.Errorf("Failed to start listener %q: %s\n", listenaddr, err)
}
}
return c, nil
}
c.addPeerTimer = time.AfterFunc(0, func() {
c.Act(nil, c._addPeerLoop)
})
c.log.Infoln("Startup complete")
return nil
func (c *Core) RetryPeersNow() {
// TODO: figure out a way to retrigger peer connections.
}
// Stop shuts down the Yggdrasil node.
func (c *Core) Stop() {
phony.Block(c, c._stop)
phony.Block(c, func() {
c.log.Infoln("Stopping...")
_ = c._close()
c.log.Infoln("Stopped")
})
}
// This function is unsafe and should only be ran by the core actor.
func (c *Core) _stop() {
c.log.Infoln("Stopping...")
c.ctxCancel()
c.pc.Close()
func (c *Core) _close() error {
c.cancel()
c.links.shutdown()
err := c.PacketConn.Close()
if c.addPeerTimer != nil {
c.addPeerTimer.Stop()
c.addPeerTimer = nil
}
_ = c.links.stop()
/* FIXME this deadlocks, need a waitgroup or something to coordinate shutdown
for _, peer := range c.GetPeers() {
c.DisconnectPeer(peer.Port)
}
*/
c.log.Infoln("Stopped")
return err
}
func (c *Core) isTLSOnly() bool {
return c.config.roots != nil
}
func (c *Core) MTU() uint64 {
const sessionTypeOverhead = 1
MTU := c.PacketConn.MTU() - sessionTypeOverhead
if MTU > 65535 {
MTU = 65535
}
return MTU
}
func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) {
buf := allocBytes(int(c.PacketConn.MTU()))
defer freeBytes(buf)
for {
bs := buf
n, from, err = c.PacketConn.ReadFrom(bs)
if err != nil {
return 0, from, err
}
if n == 0 {
continue
}
switch bs[0] {
case typeSessionTraffic:
// This is what we want to handle here
case typeSessionProto:
var key keyArray
copy(key[:], from.(iwt.Addr))
data := append([]byte(nil), bs[1:n]...)
c.proto.handleProto(nil, key, data)
continue
default:
continue
}
bs = bs[1:n]
copy(p, bs)
if len(p) < len(bs) {
n = len(p)
} else {
n = len(bs)
}
return
}
}
func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) {
buf := allocBytes(0)
defer freeBytes(buf)
buf = append(buf, typeSessionTraffic)
buf = append(buf, p...)
n, err = c.PacketConn.WriteTo(buf, addr)
if n > 0 {
n -= 1
}
return
}
func (c *Core) doPathNotify(key ed25519.PublicKey) {
c.Act(nil, func() {
if c.pathNotify != nil {
c.pathNotify(key)
}
})
}
func (c *Core) SetPathNotify(notify func(ed25519.PublicKey)) {
c.Act(nil, func() {
c.pathNotify = notify
})
}
type Logger interface {
Printf(string, ...interface{})
Println(...interface{})
Infof(string, ...interface{})
Infoln(...interface{})
Warnf(string, ...interface{})
Warnln(...interface{})
Errorf(string, ...interface{})
Errorln(...interface{})
Debugf(string, ...interface{})
Debugln(...interface{})
Traceln(...interface{})
}

View File

@@ -9,21 +9,9 @@ import (
"time"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
)
// GenerateConfig produces default configuration with suitable modifications for tests.
func GenerateConfig() *config.NodeConfig {
cfg := defaults.GenerateConfig()
cfg.AdminListen = "none"
cfg.Listen = []string{"tcp://127.0.0.1:0"}
cfg.IfName = "none"
return cfg
}
// GetLoggerWithPrefix creates a new logger instance with prefix.
// If verbose is set to true, three log levels are enabled: "info", "warn", "error".
func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger {
@@ -40,26 +28,41 @@ func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger {
// CreateAndConnectTwo creates two nodes. nodeB connects to nodeA.
// Verbosity flag is passed to logger.
func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) {
nodeA = new(Core)
if err := nodeA.Start(GenerateConfig(), GetLoggerWithPrefix("A: ", verbose)); err != nil {
var err error
cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig()
if err = cfgA.GenerateSelfSignedCertificate(); err != nil {
t.Fatal(err)
}
nodeA.SetMTU(1500)
nodeB = new(Core)
if err := nodeB.Start(GenerateConfig(), GetLoggerWithPrefix("B: ", verbose)); err != nil {
if err = cfgB.GenerateSelfSignedCertificate(); err != nil {
t.Fatal(err)
}
nodeB.SetMTU(1500)
u, err := url.Parse("tcp://" + nodeA.links.tcp.getAddr().String())
logger := GetLoggerWithPrefix("", false)
logger.EnableLevel("debug")
if nodeA, err = New(cfgA.Certificate, logger); err != nil {
t.Fatal(err)
}
if nodeB, err = New(cfgB.Certificate, logger); err != nil {
t.Fatal(err)
}
nodeAListenURL, err := url.Parse("tcp://localhost:0")
if err != nil {
t.Fatal(err)
}
err = nodeB.CallPeer(u, "")
nodeAListener, err := nodeA.Listen(nodeAListenURL, "")
if err != nil {
t.Fatal(err)
}
nodeAURL, err := url.Parse("tcp://" + nodeAListener.Addr().String())
if err != nil {
t.Fatal(err)
}
if err = nodeB.CallPeer(nodeAURL, ""); err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond)
@@ -78,7 +81,13 @@ func WaitConnected(nodeA, nodeB *Core) bool {
// It may take up to 3 seconds, but let's wait 5.
for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond)
if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 {
/*
if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 {
return true
}
*/
if len(nodeA.GetTree()) > 1 && len(nodeB.GetTree()) > 1 {
time.Sleep(3 * time.Second) // FIXME hack, there's still stuff happening internally
return true
}
}
@@ -94,7 +103,7 @@ func CreateEchoListener(t testing.TB, nodeA *Core, bufLen int, repeats int) chan
buf := make([]byte, bufLen)
res := make([]byte, bufLen)
for i := 0; i < repeats; i++ {
n, err := nodeA.Read(buf)
n, from, err := nodeA.ReadFrom(buf)
if err != nil {
t.Error(err)
return
@@ -106,7 +115,7 @@ func CreateEchoListener(t testing.TB, nodeA *Core, bufLen int, repeats int) chan
copy(res, buf)
copy(res[8:24], buf[24:40])
copy(res[24:40], buf[8:24])
_, err = nodeA.Write(res)
_, err = nodeA.WriteTo(res, from)
if err != nil {
t.Error(err)
}
@@ -141,12 +150,12 @@ func TestCore_Start_Transfer(t *testing.T) {
msg[0] = 0x60
copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address())
_, err := nodeB.Write(msg)
_, err := nodeB.WriteTo(msg, nodeA.LocalAddr())
if err != nil {
t.Fatal(err)
}
buf := make([]byte, msgLen)
_, err = nodeB.Read(buf)
_, _, err = nodeB.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
@@ -179,12 +188,13 @@ func BenchmarkCore_Start_Transfer(b *testing.B) {
b.SetBytes(int64(msgLen))
b.ResetTimer()
addr := nodeA.LocalAddr()
for i := 0; i < b.N; i++ {
_, err := nodeB.Write(msg)
_, err := nodeB.WriteTo(msg, addr)
if err != nil {
b.Fatal(err)
}
_, err = nodeB.Read(buf)
_, _, err = nodeB.ReadFrom(buf)
if err != nil {
b.Fatal(err)
}

View File

@@ -1,15 +1,17 @@
//go:build debug
// +build debug
package core
import "fmt"
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
import _ "net/http/pprof"
import "net/http"
import "runtime"
import "os"
import "github.com/gologme/log"
"github.com/gologme/log"
)
// Start the profiler in debug builds, if the required environment variable is set.
func init() {

View File

@@ -1,251 +1,586 @@
package core
import (
"crypto/ed25519"
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"math"
"net"
"net/netip"
"net/url"
"strconv"
"strings"
"sync"
//"sync/atomic"
"sync/atomic"
"time"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/util"
"golang.org/x/net/proxy"
//"github.com/Arceliar/phony" // TODO? use instead of mutexes
)
type linkType int
const (
linkTypePersistent linkType = iota // Statically configured
linkTypeEphemeral // Multicast discovered
linkTypeIncoming // Incoming connection
)
type links struct {
core *Core
mutex sync.RWMutex // protects links below
links map[linkInfo]*link
tcp tcp // TCP interface support
stopped chan struct{}
// TODO timeout (to remove from switch), read from config.ReadTimeout
phony.Inbox
core *Core
tcp *linkTCP // TCP interface support
tls *linkTLS // TLS interface support
unix *linkUNIX // UNIX interface support
socks *linkSOCKS // SOCKS interface support
// _links can only be modified safely from within the links actor
_links map[linkInfo]*link // *link is nil if connection in progress
}
type linkProtocol interface {
dial(url *url.URL, info linkInfo, options linkOptions) (net.Conn, error)
listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error)
}
// linkInfo is used as a map key
type linkInfo struct {
key keyArray
linkType string // Type of link, e.g. TCP, AWDL
local string // Local name or address
remote string // Remote name or address
uri string // Peering URI in complete form
sintf string // Peering source interface (i.e. from InterfacePeers)
}
// link tracks the state of a connection, either persistent or non-persistent
type link struct {
lname string
links *links
conn net.Conn
options linkOptions
info linkInfo
incoming bool
force bool
closed chan struct{}
kick chan struct{} // Attempt to reconnect now, if backing off
linkType linkType // Type of link, i.e. outbound/inbound, persistent/ephemeral
linkProto string // Protocol carrier of link, e.g. TCP, AWDL
// The remaining fields can only be modified safely from within the links actor
_conn *linkConn // Connected link, if any, nil if not connected
_err error // Last error on the connection, if any
_errtime time.Time // Last time an error occured
}
type linkOptions struct {
pinnedEd25519Keys map[keyArray]struct{}
priority uint8
tlsSNI string
}
type Listener struct {
listener net.Listener
ctx context.Context
Cancel context.CancelFunc
}
func (l *Listener) Addr() net.Addr {
return l.listener.Addr()
}
func (l *Listener) Close() error {
l.Cancel()
err := l.listener.Close()
<-l.ctx.Done()
return err
}
func (l *links) init(c *Core) error {
l.core = c
l.mutex.Lock()
l.links = make(map[linkInfo]*link)
l.mutex.Unlock()
l.stopped = make(chan struct{})
l.tcp = l.newLinkTCP()
l.tls = l.newLinkTLS(l.tcp)
l.unix = l.newLinkUNIX()
l.socks = l.newLinkSOCKS()
l._links = make(map[linkInfo]*link)
if err := l.tcp.init(l); err != nil {
c.log.Errorln("Failed to start TCP interface")
return err
}
var listeners []ListenAddress
phony.Block(c, func() {
listeners = make([]ListenAddress, 0, len(c.config._listeners))
for listener := range c.config._listeners {
listeners = append(listeners, listener)
}
})
return nil
}
func (l *links) call(u *url.URL, sintf string) error {
//u, err := url.Parse(uri)
//if err != nil {
// return fmt.Errorf("peer %s is not correctly formatted (%s)", uri, err)
//}
tcpOpts := tcpOptions{}
if pubkeys, ok := u.Query()["key"]; ok && len(pubkeys) > 0 {
tcpOpts.pinnedEd25519Keys = make(map[keyArray]struct{})
for _, pubkey := range pubkeys {
if sigPub, err := hex.DecodeString(pubkey); err == nil {
var sigPubKey keyArray
copy(sigPubKey[:], sigPub)
tcpOpts.pinnedEd25519Keys[sigPubKey] = struct{}{}
func (l *links) shutdown() {
phony.Block(l.tcp, func() {
for l := range l.tcp._listeners {
_ = l.Close()
}
})
phony.Block(l.tls, func() {
for l := range l.tls._listeners {
_ = l.Close()
}
})
phony.Block(l.unix, func() {
for l := range l.unix._listeners {
_ = l.Close()
}
})
}
type linkError string
func (e linkError) Error() string { return string(e) }
const ErrLinkAlreadyConfigured = linkError("peer is already configured")
const ErrLinkPriorityInvalid = linkError("priority value is invalid")
const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid")
const ErrLinkUnrecognisedSchema = linkError("link schema unknown")
func (l *links) add(u *url.URL, sintf string, linkType linkType) error {
var retErr error
phony.Block(l, func() {
// Generate the link info and see whether we think we already
// have an open peering to this peer.
lu := urlForLinkInfo(*u)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
}
// Collect together the link options, these are global options
// that are not specific to any given protocol.
var options linkOptions
for _, pubkey := range u.Query()["key"] {
sigPub, err := hex.DecodeString(pubkey)
if err != nil {
retErr = ErrLinkPinnedKeyInvalid
return
}
var sigPubKey keyArray
copy(sigPubKey[:], sigPub)
if options.pinnedEd25519Keys == nil {
options.pinnedEd25519Keys = map[keyArray]struct{}{}
}
options.pinnedEd25519Keys[sigPubKey] = struct{}{}
}
if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8)
if err != nil {
retErr = ErrLinkPriorityInvalid
return
}
options.priority = uint8(pi)
}
// If we think we're already connected to this peer, load up
// the existing peer state. Try to kick the peer if possible,
// which will cause an immediate connection attempt if it is
// backing off for some reason.
state, ok := l._links[info]
if ok && state != nil {
select {
case state.kick <- struct{}{}:
default:
}
retErr = ErrLinkAlreadyConfigured
return
}
// Create the link entry. This will contain the connection
// in progress (if any), any error details and a context that
// lets the link be cancelled later.
state = &link{
linkType: linkType,
linkProto: strings.ToUpper(u.Scheme),
kick: make(chan struct{}),
}
// Store the state of the link so that it can be queried later.
l._links[info] = state
// Track how many consecutive connection failures we have had,
// as we will back off exponentially rather than hammering the
// remote node endlessly.
var backoff int
// backoffNow is called when there's a connection error. It
// will wait for the specified amount of time and then return
// true, unless the peering context was cancelled (due to a
// peer removal most likely), in which case it returns false.
// The caller should check the return value to decide whether
// or not to give up trying.
backoffNow := func() bool {
backoff++
duration := time.Second * time.Duration(math.Exp2(float64(backoff)))
select {
case <-time.After(duration):
return true
case <-state.kick:
return true
case <-l.core.ctx.Done():
return false
}
}
}
switch u.Scheme {
// The goroutine is responsible for attempting the connection
// and then running the handler. If the connection is persistent
// then the loop will run endlessly, using backoffs as needed.
// Otherwise the loop will end, cleaning up the link entry.
go func() {
defer func() {
phony.Block(l, func() {
if l._links[info] == state {
delete(l._links, info)
}
})
}()
// This loop will run each and every time we want to attempt
// a connection to this peer.
// TODO get rid of this loop, this is *exactly* what time.AfterFunc is for, we should just send a signal to the links actor to kick off a goroutine as needed
for {
conn, err := l.connect(u, info, options)
if err != nil {
if linkType == linkTypePersistent {
// If the link is a persistent configured peering,
// store information about the connection error so
// that we can report it through the admin socket.
phony.Block(l, func() {
state._conn = nil
state._err = err
state._errtime = time.Now()
})
// Back off for a bit. If true is returned here, we
// can continue onto the next loop iteration to try
// the next connection.
if backoffNow() {
continue
} else {
return
}
} else {
// Ephemeral and incoming connections don't remain
// after a connection failure, so exit out of the
// loop and clean up the link entry.
break
}
}
// The linkConn wrapper allows us to track the number of
// bytes written to and read from this connection without
// the help of ironwood.
lc := &linkConn{
Conn: conn,
up: time.Now(),
}
// Update the link state with our newly wrapped connection.
// Clear the error state.
var doRet bool
phony.Block(l, func() {
if state._conn != nil {
// If a peering has come up in this time, abort this one.
doRet = true
}
state._conn = lc
})
if doRet {
return
}
// Give the connection to the handler. The handler will block
// for the lifetime of the connection.
if err = l.handler(linkType, options, lc); err != nil && err != io.EOF {
l.core.log.Debugf("Link %s error: %s\n", info.uri, err)
} else {
backoff = 0
}
// The handler has stopped running so the connection is dead,
// try to close the underlying socket just in case and then
// update the link state.
_ = lc.Close()
phony.Block(l, func() {
state._conn = nil
if state._err = err; state._err != nil {
state._errtime = time.Now()
}
})
// If the link is persistently configured, back off if needed
// and then try reconnecting. Otherwise, exit out.
if linkType == linkTypePersistent {
if backoffNow() {
continue
} else {
return
}
} else {
break
}
}
}()
})
return retErr
}
func (l *links) listen(u *url.URL, sintf string) (*Listener, error) {
ctx, cancel := context.WithCancel(l.core.ctx)
var protocol linkProtocol
switch strings.ToLower(u.Scheme) {
case "tcp":
l.tcp.call(u.Host, tcpOpts, sintf)
case "socks":
tcpOpts.socksProxyAddr = u.Host
if u.User != nil {
tcpOpts.socksProxyAuth = &proxy.Auth{}
tcpOpts.socksProxyAuth.User = u.User.Username()
tcpOpts.socksProxyAuth.Password, _ = u.User.Password()
}
tcpOpts.upgrade = l.tcp.tls.forDialer // TODO make this configurable
pathtokens := strings.Split(strings.Trim(u.Path, "/"), "/")
l.tcp.call(pathtokens[0], tcpOpts, sintf)
protocol = l.tcp
case "tls":
tcpOpts.upgrade = l.tcp.tls.forDialer
l.tcp.call(u.Host, tcpOpts, sintf)
protocol = l.tls
case "unix":
protocol = l.unix
default:
return errors.New("unknown call scheme: " + u.Scheme)
cancel()
return nil, ErrLinkUnrecognisedSchema
}
return nil
listener, err := protocol.listen(ctx, u, sintf)
if err != nil {
cancel()
return nil, err
}
li := &Listener{
listener: listener,
ctx: ctx,
Cancel: cancel,
}
var options linkOptions
if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8)
if err != nil {
return nil, ErrLinkPriorityInvalid
}
options.priority = uint8(pi)
}
go func() {
l.core.log.Printf("%s listener started on %s", strings.ToUpper(u.Scheme), listener.Addr())
defer l.core.log.Printf("%s listener stopped on %s", strings.ToUpper(u.Scheme), listener.Addr())
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go func(conn net.Conn) {
defer conn.Close()
// In order to populate a somewhat sane looking connection
// URI in the admin socket, we need to replace the host in
// the listener URL with the remote address.
pu := *u
pu.Host = conn.RemoteAddr().String()
lu := urlForLinkInfo(pu)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
}
// If there's an existing link state for this link, get it.
// If this node is already connected to us, just drop the
// connection. This prevents duplicate peerings.
var lc *linkConn
var state *link
phony.Block(l, func() {
var ok bool
state, ok = l._links[info]
if !ok || state == nil {
state = &link{
linkType: linkTypeIncoming,
linkProto: strings.ToUpper(u.Scheme),
kick: make(chan struct{}),
}
}
if state._conn != nil {
// If a connection has come up in this time, abort
// this one.
return
}
// The linkConn wrapper allows us to track the number of
// bytes written to and read from this connection without
// the help of ironwood.
lc = &linkConn{
Conn: conn,
up: time.Now(),
}
// Update the link state with our newly wrapped connection.
// Clear the error state.
state._conn = lc
state._err = nil
state._errtime = time.Time{}
// Store the state of the link so that it can be queried later.
l._links[info] = state
})
if lc == nil {
return
}
// Give the connection to the handler. The handler will block
// for the lifetime of the connection.
if err = l.handler(linkTypeIncoming, options, lc); err != nil && err != io.EOF {
l.core.log.Debugf("Link %s error: %s\n", u.Host, err)
}
// The handler has stopped running so the connection is dead,
// try to close the underlying socket just in case and then
// drop the link state.
_ = lc.Close()
phony.Block(l, func() {
if l._links[info] == state {
delete(l._links, info)
}
})
}(conn)
}
}()
return li, nil
}
func (l *links) create(conn net.Conn, name, linkType, local, remote string, incoming, force bool, options linkOptions) (*link, error) {
// Technically anything unique would work for names, but let's pick something human readable, just for debugging
intf := link{
conn: conn,
lname: name,
links: l,
options: options,
info: linkInfo{
linkType: linkType,
local: local,
remote: remote,
},
incoming: incoming,
force: force,
func (l *links) connect(u *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
var dialer linkProtocol
switch strings.ToLower(u.Scheme) {
case "tcp":
dialer = l.tcp
case "tls":
// SNI headers must contain hostnames and not IP addresses, so we must make sure
// that we do not populate the SNI with an IP literal. We do this by splitting
// the host-port combo from the query option and then seeing if it parses to an
// IP address successfully or not.
if sni := u.Query().Get("sni"); sni != "" {
if net.ParseIP(sni) == nil {
options.tlsSNI = sni
}
}
// If the SNI is not configured still because the above failed then we'll try
// again but this time we'll use the host part of the peering URI instead.
if options.tlsSNI == "" {
if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil {
options.tlsSNI = host
}
}
dialer = l.tls
case "socks":
dialer = l.socks
case "unix":
dialer = l.unix
default:
return nil, ErrLinkUnrecognisedSchema
}
return &intf, nil
return dialer.dial(u, info, options)
}
func (l *links) stop() error {
close(l.stopped)
if err := l.tcp.stop(); err != nil {
return err
}
return nil
}
func (intf *link) handler() (chan struct{}, error) {
// TODO split some of this into shorter functions, so it's easier to read, and for the FIXME duplicate peer issue mentioned later
defer intf.conn.Close()
func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn) error {
meta := version_getBaseMetadata()
meta.key = intf.links.core.public
meta.publicKey = l.core.public
metaBytes := meta.encode()
// TODO timeouts on send/recv (goroutine for send/recv, channel select w/ timer)
var err error
if !util.FuncTimeout(30*time.Second, func() {
var n int
n, err = intf.conn.Write(metaBytes)
if err == nil && n != len(metaBytes) {
err = errors.New("incomplete metadata send")
}
}) {
return nil, errors.New("timeout on metadata send")
if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil {
return fmt.Errorf("failed to set handshake deadline: %w", err)
}
if err != nil {
return nil, err
n, err := conn.Write(metaBytes)
switch {
case err != nil:
return fmt.Errorf("write handshake: %w", err)
case err == nil && n != len(metaBytes):
return fmt.Errorf("incomplete handshake send")
}
if !util.FuncTimeout(30*time.Second, func() {
var n int
n, err = io.ReadFull(intf.conn, metaBytes)
if err == nil && n != len(metaBytes) {
err = errors.New("incomplete metadata recv")
}
}) {
return nil, errors.New("timeout on metadata recv")
if _, err = io.ReadFull(conn, metaBytes); err != nil {
return fmt.Errorf("read handshake: %w", err)
}
if err != nil {
return nil, err
if err = conn.SetDeadline(time.Time{}); err != nil {
return fmt.Errorf("failed to clear handshake deadline: %w", err)
}
meta = version_metadata{}
base := version_getBaseMetadata()
if !meta.decode(metaBytes) {
return nil, errors.New("failed to decode metadata")
return errors.New("failed to decode metadata")
}
if !meta.check() {
intf.links.core.log.Errorf("Failed to connect to node: %s is incompatible version (local %s, remote %s)",
intf.lname,
fmt.Sprintf("%d.%d", base.ver, base.minorVer),
fmt.Sprintf("%d.%d", meta.ver, meta.minorVer),
return fmt.Errorf("remote node incompatible version (local %s, remote %s)",
fmt.Sprintf("%d.%d", base.majorVer, base.minorVer),
fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer),
)
return nil, errors.New("remote node is incompatible version")
}
// Check if the remote side matches the keys we expected. This is a bit of a weak
// check - in future versions we really should check a signature or something like that.
if pinned := intf.options.pinnedEd25519Keys; pinned != nil {
if pinned := options.pinnedEd25519Keys; len(pinned) > 0 {
var key keyArray
copy(key[:], meta.key)
copy(key[:], meta.publicKey)
if _, allowed := pinned[key]; !allowed {
intf.links.core.log.Errorf("Failed to connect to node: %q sent ed25519 key that does not match pinned keys", intf.name())
return nil, fmt.Errorf("failed to connect: host sent ed25519 key that does not match pinned keys")
return fmt.Errorf("node public key that does not match pinned keys")
}
}
// Check if we're authorized to connect to this key / IP
intf.links.core.config.RLock()
allowed := intf.links.core.config.AllowedPublicKeys
intf.links.core.config.RUnlock()
var allowed map[[32]byte]struct{}
phony.Block(l.core, func() {
allowed = l.core.config._allowedPublicKeys
})
isallowed := len(allowed) == 0
for _, k := range allowed {
if k == hex.EncodeToString(meta.key) { // TODO: this is yuck
for k := range allowed {
if bytes.Equal(k[:], meta.publicKey) {
isallowed = true
break
}
}
if intf.incoming && !intf.force && !isallowed {
intf.links.core.log.Warnf("%s connection from %s forbidden: AllowedEncryptionPublicKeys does not contain key %s",
strings.ToUpper(intf.info.linkType), intf.info.remote, hex.EncodeToString(meta.key))
intf.close()
return nil, nil
if linkType == linkTypeIncoming && !isallowed {
return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.publicKey))
}
// Check if we already have a link to this node
copy(intf.info.key[:], meta.key)
intf.links.mutex.Lock()
if oldIntf, isIn := intf.links.links[intf.info]; isIn {
intf.links.mutex.Unlock()
// FIXME we should really return an error and let the caller block instead
// That lets them do things like close connections on its own, avoid printing a connection message in the first place, etc.
intf.links.core.log.Debugln("DEBUG: found existing interface for", intf.name())
return oldIntf.closed, nil
} else {
intf.closed = make(chan struct{})
intf.links.links[intf.info] = intf
defer func() {
intf.links.mutex.Lock()
delete(intf.links.links, intf.info)
intf.links.mutex.Unlock()
close(intf.closed)
}()
intf.links.core.log.Debugln("DEBUG: registered interface for", intf.name())
dir := "outbound"
if linkType == linkTypeIncoming {
dir = "inbound"
}
intf.links.mutex.Unlock()
themAddr := address.AddrForKey(ed25519.PublicKey(intf.info.key[:]))
themAddrString := net.IP(themAddr[:]).String()
themString := fmt.Sprintf("%s@%s", themAddrString, intf.info.remote)
intf.links.core.log.Infof("Connected %s: %s, source %s",
strings.ToUpper(intf.info.linkType), themString, intf.info.local)
// Run the handler
err = intf.links.core.pc.HandleConn(ed25519.PublicKey(intf.info.key[:]), intf.conn)
// TODO don't report an error if it's just a 'use of closed network connection'
if err != nil {
intf.links.core.log.Infof("Disconnected %s: %s, source %s; error: %s",
strings.ToUpper(intf.info.linkType), themString, intf.info.local, err)
} else {
intf.links.core.log.Infof("Disconnected %s: %s, source %s",
strings.ToUpper(intf.info.linkType), themString, intf.info.local)
remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).String()
remoteStr := fmt.Sprintf("%s@%s", remoteAddr, conn.RemoteAddr())
localStr := conn.LocalAddr()
l.core.log.Infof("Connected %s: %s, source %s",
dir, remoteStr, localStr)
err = l.core.HandleConn(meta.publicKey, conn, options.priority)
switch err {
case io.EOF, net.ErrClosed, nil:
l.core.log.Infof("Disconnected %s: %s, source %s",
dir, remoteStr, localStr)
default:
l.core.log.Infof("Disconnected %s: %s, source %s; error: %s",
dir, remoteStr, localStr, err)
}
return nil, err
return nil
}
func (intf *link) close() {
intf.conn.Close()
func urlForLinkInfo(u url.URL) url.URL {
u.RawQuery = ""
if host, _, err := net.SplitHostPort(u.Host); err == nil {
if addr, err := netip.ParseAddr(host); err == nil {
// For peers that look like multicast peers (i.e.
// link-local addresses), we will ignore the port number,
// otherwise we might open multiple connections to them.
if addr.IsLinkLocalUnicast() {
u.Host = fmt.Sprintf("[%s]", addr.String())
}
}
}
return u
}
func (intf *link) name() string {
return intf.lname
type linkConn struct {
// tx and rx are at the beginning of the struct to ensure 64-bit alignment
// on 32-bit platforms, see https://pkg.go.dev/sync/atomic#pkg-note-BUG
rx uint64
tx uint64
up time.Time
net.Conn
}
func (c *linkConn) Read(p []byte) (n int, err error) {
n, err = c.Conn.Read(p)
atomic.AddUint64(&c.rx, uint64(n))
return
}
func (c *linkConn) Write(p []byte) (n int, err error) {
n, err = c.Conn.Write(p)
atomic.AddUint64(&c.tx, uint64(n))
return
}

42
src/core/link_socks.go Normal file
View File

@@ -0,0 +1,42 @@
package core
import (
"context"
"fmt"
"net"
"net/url"
"strings"
"golang.org/x/net/proxy"
)
type linkSOCKS struct {
*links
}
func (l *links) newLinkSOCKS() *linkSOCKS {
lt := &linkSOCKS{
links: l,
}
return lt
}
func (l *linkSOCKS) dial(url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
var proxyAuth *proxy.Auth
if url.User != nil && url.User.Username() != "" {
proxyAuth = &proxy.Auth{
User: url.User.Username(),
}
proxyAuth.Password, _ = url.User.Password()
}
dialer, err := proxy.SOCKS5("tcp", url.Host, proxyAuth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("failed to configure proxy")
}
pathtokens := strings.Split(strings.Trim(url.Path, "/"), "/")
return dialer.Dial("tcp", pathtokens[0])
}
func (l *linkSOCKS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return nil, fmt.Errorf("SOCKS listener not supported")
}

164
src/core/link_tcp.go Normal file
View File

@@ -0,0 +1,164 @@
package core
import (
"context"
"fmt"
"net"
"net/url"
"strconv"
"time"
"github.com/Arceliar/phony"
)
type linkTCP struct {
phony.Inbox
*links
listenconfig *net.ListenConfig
_listeners map[*Listener]context.CancelFunc
}
func (l *links) newLinkTCP() *linkTCP {
lt := &linkTCP{
links: l,
listenconfig: &net.ListenConfig{
KeepAlive: -1,
},
_listeners: map[*Listener]context.CancelFunc{},
}
lt.listenconfig.Control = lt.tcpContext
return lt
}
type tcpDialer struct {
info linkInfo
dialer *net.Dialer
addr *net.TCPAddr
}
func (l *linkTCP) dialersFor(url *url.URL, info linkInfo) ([]*tcpDialer, error) {
host, p, err := net.SplitHostPort(url.Host)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(p)
if err != nil {
return nil, err
}
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
dialers := make([]*tcpDialer, 0, len(ips))
for _, ip := range ips {
addr := &net.TCPAddr{
IP: ip,
Port: port,
}
dialer, err := l.dialerFor(addr, info.sintf)
if err != nil {
continue
}
dialers = append(dialers, &tcpDialer{
info: info,
dialer: dialer,
addr: addr,
})
}
return dialers, nil
}
func (l *linkTCP) dial(url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
if l.core.isTLSOnly() {
return nil, fmt.Errorf("TCP peer prohibited in TLS-only mode")
}
dialers, err := l.dialersFor(url, info)
if err != nil {
return nil, err
}
if len(dialers) == 0 {
return nil, nil
}
for _, d := range dialers {
var conn net.Conn
conn, err = d.dialer.DialContext(l.core.ctx, "tcp", d.addr.String())
if err != nil {
l.core.log.Warnf("Failed to connect to %s: %s", d.addr, err)
continue
}
return conn, nil
}
return nil, err
}
func (l *linkTCP) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
if l.core.isTLSOnly() {
return nil, fmt.Errorf("TCP listener prohibited in TLS-only mode")
}
hostport := url.Host
if sintf != "" {
if host, port, err := net.SplitHostPort(hostport); err == nil {
hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port)
}
}
return l.listenconfig.Listen(ctx, "tcp", hostport)
}
func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error) {
if dst.IP.IsLinkLocalUnicast() {
if sintf != "" {
dst.Zone = sintf
}
if dst.Zone == "" {
return nil, fmt.Errorf("link-local address requires a zone")
}
}
dialer := &net.Dialer{
Timeout: time.Second * 5,
KeepAlive: -1,
Control: l.tcpContext,
}
if sintf != "" {
dialer.Control = l.getControl(sintf)
ief, err := net.InterfaceByName(sintf)
if err != nil {
return nil, fmt.Errorf("interface %q not found", sintf)
}
if ief.Flags&net.FlagUp == 0 {
return nil, fmt.Errorf("interface %q is not up", sintf)
}
addrs, err := ief.Addrs()
if err != nil {
return nil, fmt.Errorf("interface %q addresses not available: %w", sintf, err)
}
for addrindex, addr := range addrs {
src, _, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if !src.IsGlobalUnicast() && !src.IsLinkLocalUnicast() {
continue
}
bothglobal := src.IsGlobalUnicast() == dst.IP.IsGlobalUnicast()
bothlinklocal := src.IsLinkLocalUnicast() == dst.IP.IsLinkLocalUnicast()
if !bothglobal && !bothlinklocal {
continue
}
if (src.To4() != nil) != (dst.IP.To4() != nil) {
continue
}
if bothglobal || bothlinklocal || addrindex == len(addrs)-1 {
dialer.LocalAddr = &net.TCPAddr{
IP: src,
Port: 0,
Zone: sintf,
}
break
}
}
if dialer.LocalAddr == nil {
return nil, fmt.Errorf("no suitable source address found on interface %q", sintf)
}
}
return dialer, nil
}

View File

@@ -1,3 +1,4 @@
//go:build darwin
// +build darwin
package core
@@ -10,7 +11,7 @@ import (
// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go
func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error {
func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error {
var control error
var recvanyif error
@@ -27,6 +28,6 @@ func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error {
}
}
func (t *tcp) getControl(sintf string) func(string, string, syscall.RawConn) error {
func (t *linkTCP) getControl(sintf string) func(string, string, syscall.RawConn) error {
return t.tcpContext
}

View File

@@ -1,3 +1,4 @@
//go:build linux
// +build linux
package core
@@ -10,7 +11,7 @@ import (
// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go
func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error {
func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error {
var control error
var bbr error
@@ -30,7 +31,7 @@ func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error {
return nil
}
func (t *tcp) getControl(sintf string) func(string, string, syscall.RawConn) error {
func (t *linkTCP) getControl(sintf string) func(string, string, syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) error {
var err error
btd := func(fd uintptr) {

View File

@@ -0,0 +1,18 @@
//go:build !darwin && !linux
// +build !darwin,!linux
package core
import (
"syscall"
)
// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go
func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error {
return nil
}
func (t *linkTCP) getControl(sintf string) func(string, string, syscall.RawConn) error {
return t.tcpContext
}

74
src/core/link_tls.go Normal file
View File

@@ -0,0 +1,74 @@
package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/url"
"github.com/Arceliar/phony"
)
type linkTLS struct {
phony.Inbox
*links
tcp *linkTCP
listener *net.ListenConfig
config *tls.Config
_listeners map[*Listener]context.CancelFunc
}
func (l *links) newLinkTLS(tcp *linkTCP) *linkTLS {
lt := &linkTLS{
links: l,
tcp: tcp,
listener: &net.ListenConfig{
Control: tcp.tcpContext,
KeepAlive: -1,
},
config: l.core.config.tls.Clone(),
_listeners: map[*Listener]context.CancelFunc{},
}
return lt
}
func (l *linkTLS) dial(url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
dialers, err := l.tcp.dialersFor(url, info)
if err != nil {
return nil, err
}
if len(dialers) == 0 {
return nil, nil
}
for _, d := range dialers {
tlsconfig := l.config.Clone()
tlsconfig.ServerName = options.tlsSNI
tlsdialer := &tls.Dialer{
NetDialer: d.dialer,
Config: tlsconfig,
}
var conn net.Conn
conn, err = tlsdialer.DialContext(l.core.ctx, "tcp", d.addr.String())
if err != nil {
continue
}
return conn, nil
}
return nil, err
}
func (l *linkTLS) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
hostport := url.Host
if sintf != "" {
if host, port, err := net.SplitHostPort(hostport); err == nil {
hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port)
}
}
listener, err := l.listener.Listen(ctx, "tcp", hostport)
if err != nil {
return nil, err
}
tlslistener := tls.NewListener(listener, l.config)
return tlslistener, nil
}

45
src/core/link_unix.go Normal file
View File

@@ -0,0 +1,45 @@
package core
import (
"context"
"net"
"net/url"
"time"
"github.com/Arceliar/phony"
)
type linkUNIX struct {
phony.Inbox
*links
dialer *net.Dialer
listener *net.ListenConfig
_listeners map[*Listener]context.CancelFunc
}
func (l *links) newLinkUNIX() *linkUNIX {
lt := &linkUNIX{
links: l,
dialer: &net.Dialer{
Timeout: time.Second * 5,
KeepAlive: -1,
},
listener: &net.ListenConfig{
KeepAlive: -1,
},
_listeners: map[*Listener]context.CancelFunc{},
}
return lt
}
func (l *linkUNIX) dial(url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
addr, err := net.ResolveUnixAddr("unix", url.Path)
if err != nil {
return nil, err
}
return l.dialer.DialContext(l.core.ctx, "unix", addr.String())
}
func (l *linkUNIX) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return l.listener.Listen(ctx, "unix", url.Path)
}

View File

@@ -4,31 +4,24 @@ import (
"encoding/hex"
"encoding/json"
"errors"
"net"
"fmt"
"runtime"
"strings"
"time"
iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony"
//"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
// NodeInfoPayload represents a RequestNodeInfo response, in bytes.
type NodeInfoPayload []byte
type nodeinfo struct {
phony.Inbox
proto *protoHandler
myNodeInfo NodeInfoPayload
myNodeInfo json.RawMessage
callbacks map[keyArray]nodeinfoCallback
}
type nodeinfoCallback struct {
call func(nodeinfo NodeInfoPayload)
call func(nodeinfo json.RawMessage)
created time.Time
}
@@ -57,7 +50,7 @@ func (m *nodeinfo) _cleanup() {
})
}
func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo NodeInfoPayload)) {
func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo json.RawMessage)) {
m.callbacks[sender] = nodeinfoCallback{
created: time.Now(),
call: call,
@@ -65,71 +58,59 @@ func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo NodeInfoPayl
}
// Handles the callback, if there is one
func (m *nodeinfo) _callback(sender keyArray, nodeinfo NodeInfoPayload) {
func (m *nodeinfo) _callback(sender keyArray, nodeinfo json.RawMessage) {
if callback, ok := m.callbacks[sender]; ok {
callback.call(nodeinfo)
delete(m.callbacks, sender)
}
}
func (m *nodeinfo) _getNodeInfo() NodeInfoPayload {
func (m *nodeinfo) _getNodeInfo() json.RawMessage {
return m.myNodeInfo
}
// Set the current node's nodeinfo
func (m *nodeinfo) setNodeInfo(given interface{}, privacy bool) (err error) {
func (m *nodeinfo) setNodeInfo(given map[string]interface{}, privacy bool) (err error) {
phony.Block(m, func() {
err = m._setNodeInfo(given, privacy)
})
return
}
func (m *nodeinfo) _setNodeInfo(given interface{}, privacy bool) error {
defaults := map[string]interface{}{
"buildname": version.BuildName(),
"buildversion": version.BuildVersion(),
"buildplatform": runtime.GOOS,
"buildarch": runtime.GOARCH,
func (m *nodeinfo) _setNodeInfo(given map[string]interface{}, privacy bool) error {
newnodeinfo := make(map[string]interface{}, len(given))
for k, v := range given {
newnodeinfo[k] = v
}
newnodeinfo := make(map[string]interface{})
if !privacy {
for k, v := range defaults {
newnodeinfo[k] = v
}
}
if nodeinfomap, ok := given.(map[string]interface{}); ok {
for key, value := range nodeinfomap {
if _, ok := defaults[key]; ok {
if strvalue, strok := value.(string); strok && strings.EqualFold(strvalue, "null") || value == nil {
delete(newnodeinfo, key)
}
continue
}
newnodeinfo[key] = value
}
newnodeinfo["buildname"] = version.BuildName()
newnodeinfo["buildversion"] = version.BuildVersion()
newnodeinfo["buildplatform"] = runtime.GOOS
newnodeinfo["buildarch"] = runtime.GOARCH
}
newjson, err := json.Marshal(newnodeinfo)
if err == nil {
if len(newjson) > 16384 {
return errors.New("NodeInfo exceeds max length of 16384 bytes")
}
switch {
case err != nil:
return fmt.Errorf("NodeInfo marshalling failed: %w", err)
case len(newjson) > 16384:
return fmt.Errorf("NodeInfo exceeds max length of 16384 bytes")
default:
m.myNodeInfo = newjson
return nil
}
return err
}
func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo NodeInfoPayload)) {
func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo json.RawMessage)) {
m.Act(from, func() {
m._sendReq(key, callback)
})
}
func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo NodeInfoPayload)) {
func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo json.RawMessage)) {
if callback != nil {
m._addCallback(key, callback)
}
_, _ = m.proto.core.pc.WriteTo([]byte{typeSessionProto, typeProtoNodeInfoRequest}, iwt.Addr(key[:]))
_, _ = m.proto.core.PacketConn.WriteTo([]byte{typeSessionProto, typeProtoNodeInfoRequest}, iwt.Addr(key[:]))
}
func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) {
@@ -138,7 +119,7 @@ func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) {
})
}
func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info NodeInfoPayload) {
func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info json.RawMessage) {
m.Act(from, func() {
m._callback(key, info)
})
@@ -146,7 +127,7 @@ func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info NodeInfoPayloa
func (m *nodeinfo) _sendRes(key keyArray) {
bs := append([]byte{typeSessionProto, typeProtoNodeInfoResponse}, m._getNodeInfo()...)
_, _ = m.proto.core.pc.WriteTo(bs, iwt.Addr(key[:]))
_, _ = m.proto.core.PacketConn.WriteTo(bs, iwt.Addr(key[:]))
}
// Admin socket stuff
@@ -154,36 +135,39 @@ func (m *nodeinfo) _sendRes(key keyArray) {
type GetNodeInfoRequest struct {
Key string `json:"key"`
}
type GetNodeInfoResponse map[string]interface{}
type GetNodeInfoResponse map[string]json.RawMessage
func (m *nodeinfo) nodeInfoAdminHandler(in json.RawMessage) (interface{}, error) {
var req GetNodeInfoRequest
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if req.Key == "" {
return nil, fmt.Errorf("No remote public key supplied")
}
var key keyArray
var kbs []byte
var err error
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err
return nil, fmt.Errorf("Failed to decode public key: %w", err)
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
m.sendReq(nil, key, func(info NodeInfoPayload) {
m.sendReq(nil, key, func(info json.RawMessage) {
ch <- info
})
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
return nil, errors.New("timeout")
return nil, errors.New("Timed out waiting for response")
case info := <-ch:
var msg json.RawMessage
if err := msg.UnmarshalJSON(info); err != nil {
return nil, err
}
ip := net.IP(address.AddrForKey(kbs)[:])
res := GetNodeInfoResponse{ip.String(): msg}
key := hex.EncodeToString(kbs[:])
res := GetNodeInfoResponse{key: msg}
return res, nil
}
}

57
src/core/options.go Normal file
View File

@@ -0,0 +1,57 @@
package core
import (
"crypto/ed25519"
"crypto/x509"
"fmt"
"net/url"
)
func (c *Core) _applyOption(opt SetupOption) (err error) {
switch v := opt.(type) {
case RootCertificate:
cert := x509.Certificate(v)
if c.config.roots == nil {
c.config.roots = x509.NewCertPool()
}
c.config.roots.AddCert(&cert)
case Peer:
u, err := url.Parse(v.URI)
if err != nil {
return fmt.Errorf("unable to parse peering URI: %w", err)
}
return c.links.add(u, v.SourceInterface, linkTypePersistent)
case ListenAddress:
c.config._listeners[v] = struct{}{}
case NodeInfo:
c.config.nodeinfo = v
case NodeInfoPrivacy:
c.config.nodeinfoPrivacy = v
case AllowedPublicKey:
pk := [32]byte{}
copy(pk[:], v)
c.config._allowedPublicKeys[pk] = struct{}{}
}
return
}
type SetupOption interface {
isSetupOption()
}
type RootCertificate x509.Certificate
type ListenAddress string
type Peer struct {
URI string
SourceInterface string
}
type NodeInfo map[string]interface{}
type NodeInfoPrivacy bool
type AllowedPublicKey ed25519.PublicKey
func (a RootCertificate) isSetupOption() {}
func (a ListenAddress) isSetupOption() {}
func (a Peer) isSetupOption() {}
func (a NodeInfo) isSetupOption() {}
func (a NodeInfoPrivacy) isSetupOption() {}
func (a AllowedPublicKey) isSetupOption() {}

17
src/core/pool.go Normal file
View File

@@ -0,0 +1,17 @@
package core
import "sync"
var bytePool = sync.Pool{New: func() interface{} { return []byte(nil) }}
func allocBytes(size int) []byte {
bs := bytePool.Get().([]byte)
if cap(bs) < size {
bs = make([]byte, size)
}
return bs[:size]
}
func freeBytes(bs []byte) {
bytePool.Put(bs[:0]) //nolint:staticcheck
}

View File

@@ -1,6 +1,7 @@
package core
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"errors"
@@ -20,8 +21,8 @@ const (
typeDebugGetSelfResponse
typeDebugGetPeersRequest
typeDebugGetPeersResponse
typeDebugGetDHTRequest
typeDebugGetDHTResponse
typeDebugGetTreeRequest
typeDebugGetTreeResponse
)
type reqInfo struct {
@@ -29,23 +30,30 @@ type reqInfo struct {
timer *time.Timer // time.AfterFunc cleanup
}
type keyArray [ed25519.PublicKeySize]byte
type protoHandler struct {
phony.Inbox
core *Core
nodeinfo nodeinfo
sreqs map[keyArray]*reqInfo
preqs map[keyArray]*reqInfo
dreqs map[keyArray]*reqInfo
selfRequests map[keyArray]*reqInfo
peersRequests map[keyArray]*reqInfo
treeRequests map[keyArray]*reqInfo
}
func (p *protoHandler) init(core *Core) {
p.core = core
p.nodeinfo.init(p)
p.sreqs = make(map[keyArray]*reqInfo)
p.preqs = make(map[keyArray]*reqInfo)
p.dreqs = make(map[keyArray]*reqInfo)
p.selfRequests = make(map[keyArray]*reqInfo)
p.peersRequests = make(map[keyArray]*reqInfo)
p.treeRequests = make(map[keyArray]*reqInfo)
}
// Common functions
func (p *protoHandler) handleProto(from phony.Actor, key keyArray, bs []byte) {
if len(bs) == 0 {
return
@@ -57,10 +65,16 @@ func (p *protoHandler) handleProto(from phony.Actor, key keyArray, bs []byte) {
case typeProtoNodeInfoResponse:
p.nodeinfo.handleRes(p, key, bs[1:])
case typeProtoDebug:
p._handleDebug(key, bs[1:])
p.handleDebug(from, key, bs[1:])
}
}
func (p *protoHandler) handleDebug(from phony.Actor, key keyArray, bs []byte) {
p.Act(from, func() {
p._handleDebug(key, bs)
})
}
func (p *protoHandler) _handleDebug(key keyArray, bs []byte) {
if len(bs) == 0 {
return
@@ -75,29 +89,36 @@ func (p *protoHandler) _handleDebug(key keyArray, bs []byte) {
p._handleGetPeersRequest(key)
case typeDebugGetPeersResponse:
p._handleGetPeersResponse(key, bs[1:])
case typeDebugGetDHTRequest:
p._handleGetDHTRequest(key)
case typeDebugGetDHTResponse:
p._handleGetDHTResponse(key, bs[1:])
case typeDebugGetTreeRequest:
p._handleGetTreeRequest(key)
case typeDebugGetTreeResponse:
p._handleGetTreeResponse(key, bs[1:])
}
}
func (p *protoHandler) _sendDebug(key keyArray, dType uint8, data []byte) {
bs := append([]byte{typeSessionProto, typeProtoDebug, dType}, data...)
_, _ = p.core.PacketConn.WriteTo(bs, iwt.Addr(key[:]))
}
// Get self
func (p *protoHandler) sendGetSelfRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() {
if info := p.sreqs[key]; info != nil {
if info := p.selfRequests[key]; info != nil {
info.timer.Stop()
delete(p.sreqs, key)
delete(p.selfRequests, key)
}
info := new(reqInfo)
info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() {
if p.sreqs[key] == info {
delete(p.sreqs, key)
if p.selfRequests[key] == info {
delete(p.selfRequests, key)
}
})
})
p.sreqs[key] = info
p.selfRequests[key] = info
p._sendDebug(key, typeDebugGetSelfRequest, nil)
})
}
@@ -105,8 +126,8 @@ func (p *protoHandler) sendGetSelfRequest(key keyArray, callback func([]byte)) {
func (p *protoHandler) _handleGetSelfRequest(key keyArray) {
self := p.core.GetSelf()
res := map[string]string{
"key": hex.EncodeToString(self.Key[:]),
"coords": fmt.Sprintf("%v", self.Coords),
"key": hex.EncodeToString(self.Key[:]),
"routing_entries": fmt.Sprintf("%v", self.RoutingEntries),
}
bs, err := json.Marshal(res) // FIXME this puts keys in base64, not hex
if err != nil {
@@ -116,29 +137,31 @@ func (p *protoHandler) _handleGetSelfRequest(key keyArray) {
}
func (p *protoHandler) _handleGetSelfResponse(key keyArray, bs []byte) {
if info := p.sreqs[key]; info != nil {
if info := p.selfRequests[key]; info != nil {
info.timer.Stop()
info.callback(bs)
delete(p.sreqs, key)
delete(p.selfRequests, key)
}
}
// Get peers
func (p *protoHandler) sendGetPeersRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() {
if info := p.preqs[key]; info != nil {
if info := p.peersRequests[key]; info != nil {
info.timer.Stop()
delete(p.preqs, key)
delete(p.peersRequests, key)
}
info := new(reqInfo)
info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() {
if p.preqs[key] == info {
delete(p.preqs, key)
if p.peersRequests[key] == info {
delete(p.peersRequests, key)
}
})
})
p.preqs[key] = info
p.peersRequests[key] = info
p._sendDebug(key, typeDebugGetPeersRequest, nil)
})
}
@@ -149,7 +172,7 @@ func (p *protoHandler) _handleGetPeersRequest(key keyArray) {
for _, pinfo := range peers {
tmp := append(bs, pinfo.Key[:]...)
const responseOverhead = 2 // 1 debug type, 1 getpeers type
if uint64(len(tmp))+responseOverhead > p.core.store.maxSessionMTU() {
if uint64(len(tmp))+responseOverhead > p.core.MTU() {
break
}
bs = tmp
@@ -158,61 +181,58 @@ func (p *protoHandler) _handleGetPeersRequest(key keyArray) {
}
func (p *protoHandler) _handleGetPeersResponse(key keyArray, bs []byte) {
if info := p.preqs[key]; info != nil {
if info := p.peersRequests[key]; info != nil {
info.timer.Stop()
info.callback(bs)
delete(p.preqs, key)
delete(p.peersRequests, key)
}
}
func (p *protoHandler) sendGetDHTRequest(key keyArray, callback func([]byte)) {
// Get Tree
func (p *protoHandler) sendGetTreeRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() {
if info := p.dreqs[key]; info != nil {
if info := p.treeRequests[key]; info != nil {
info.timer.Stop()
delete(p.dreqs, key)
delete(p.treeRequests, key)
}
info := new(reqInfo)
info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() {
if p.dreqs[key] == info {
delete(p.dreqs, key)
if p.treeRequests[key] == info {
delete(p.treeRequests, key)
}
})
})
p.dreqs[key] = info
p._sendDebug(key, typeDebugGetDHTRequest, nil)
p.treeRequests[key] = info
p._sendDebug(key, typeDebugGetTreeRequest, nil)
})
}
func (p *protoHandler) _handleGetDHTRequest(key keyArray) {
dinfos := p.core.GetDHT()
func (p *protoHandler) _handleGetTreeRequest(key keyArray) {
dinfos := p.core.GetTree()
var bs []byte
for _, dinfo := range dinfos {
tmp := append(bs, dinfo.Key[:]...)
const responseOverhead = 2 // 1 debug type, 1 getdht type
if uint64(len(tmp))+responseOverhead > p.core.store.maxSessionMTU() {
const responseOverhead = 2 // 1 debug type, 1 gettree type
if uint64(len(tmp))+responseOverhead > p.core.MTU() {
break
}
bs = tmp
}
p._sendDebug(key, typeDebugGetDHTResponse, bs)
p._sendDebug(key, typeDebugGetTreeResponse, bs)
}
func (p *protoHandler) _handleGetDHTResponse(key keyArray, bs []byte) {
if info := p.dreqs[key]; info != nil {
func (p *protoHandler) _handleGetTreeResponse(key keyArray, bs []byte) {
if info := p.treeRequests[key]; info != nil {
info.timer.Stop()
info.callback(bs)
delete(p.dreqs, key)
delete(p.treeRequests, key)
}
}
func (p *protoHandler) _sendDebug(key keyArray, dType uint8, data []byte) {
bs := append([]byte{typeSessionProto, typeProtoDebug, dType}, data...)
_, _ = p.core.pc.WriteTo(bs, iwt.Addr(key[:]))
}
// Admin socket stuff
// Admin socket stuff for "Get self"
type DebugGetSelfRequest struct {
Key string `json:"key"`
@@ -252,6 +272,8 @@ func (p *protoHandler) getSelfHandler(in json.RawMessage) (interface{}, error) {
}
}
// Admin socket stuff for "Get peers"
type DebugGetPeersRequest struct {
Key string `json:"key"`
}
@@ -300,14 +322,16 @@ func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error)
}
}
type DebugGetDHTRequest struct {
// Admin socket stuff for "Get Tree"
type DebugGetTreeRequest struct {
Key string `json:"key"`
}
type DebugGetDHTResponse map[string]interface{}
type DebugGetTreeResponse map[string]interface{}
func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetDHTRequest
func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetTreeRequest
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
@@ -319,7 +343,7 @@ func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) {
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
p.sendGetDHTRequest(key, func(info []byte) {
p.sendGetTreeRequest(key, func(info []byte) {
ch <- info
})
timer := time.NewTimer(6 * time.Second)
@@ -343,7 +367,7 @@ func (p *protoHandler) getDHTHandler(in json.RawMessage) (interface{}, error) {
return nil, err
}
ip := net.IP(address.AddrForKey(kbs)[:])
res := DebugGetDHTResponse{ip.String(): msg}
res := DebugGetTreeResponse{ip.String(): msg}
return res, nil
}
}

View File

@@ -1,417 +0,0 @@
package core
// This sends packets to peers using TCP as a transport
// It's generally better tested than the UDP implementation
// Using it regularly is insane, but I find TCP easier to test/debug with it
// Updating and optimizing the UDP version is a higher priority
// TODO:
// Something needs to make sure we're getting *valid* packets
// Could be used to DoS (connect, give someone else's keys, spew garbage)
// I guess the "peer" part should watch for link packets, disconnect?
// TCP connections start with a metadata exchange.
// It involves exchanging version numbers and crypto keys
// See version.go for version metadata format
import (
"context"
"fmt"
"math/rand"
"net"
"net/url"
"strings"
"sync"
"time"
"golang.org/x/net/proxy"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
//"github.com/yggdrasil-network/yggdrasil-go/src/util"
)
const default_timeout = 6 * time.Second
// The TCP listener and information about active TCP connections, to avoid duplication.
type tcp struct {
links *links
waitgroup sync.WaitGroup
mutex sync.Mutex // Protecting the below
listeners map[string]*TcpListener
calls map[string]struct{}
conns map[linkInfo](chan struct{})
tls tcptls
}
// TcpListener is a stoppable TCP listener interface. These are typically
// returned from calls to the ListenTCP() function and are also used internally
// to represent listeners created by the "Listen" configuration option and for
// multicast interfaces.
type TcpListener struct {
Listener net.Listener
opts tcpOptions
stop chan struct{}
}
type TcpUpgrade struct {
upgrade func(c net.Conn, o *tcpOptions) (net.Conn, error)
name string
}
type tcpOptions struct {
linkOptions
upgrade *TcpUpgrade
socksProxyAddr string
socksProxyAuth *proxy.Auth
socksPeerAddr string
}
func (l *TcpListener) Stop() {
defer func() { _ = recover() }()
close(l.stop)
}
// Wrapper function to set additional options for specific connection types.
func (t *tcp) setExtraOptions(c net.Conn) {
switch sock := c.(type) {
case *net.TCPConn:
_ = sock.SetNoDelay(true)
// TODO something for socks5
default:
}
}
// Returns the address of the listener.
func (t *tcp) getAddr() *net.TCPAddr {
// TODO: Fix this, because this will currently only give a single address
// to multicast.go, which obviously is not great, but right now multicast.go
// doesn't have the ability to send more than one address in a packet either
t.mutex.Lock()
defer t.mutex.Unlock()
for _, l := range t.listeners {
return l.Listener.Addr().(*net.TCPAddr)
}
return nil
}
// Initializes the struct.
func (t *tcp) init(l *links) error {
t.links = l
t.tls.init(t)
t.mutex.Lock()
t.calls = make(map[string]struct{})
t.conns = make(map[linkInfo](chan struct{}))
t.listeners = make(map[string]*TcpListener)
t.mutex.Unlock()
t.links.core.config.RLock()
defer t.links.core.config.RUnlock()
for _, listenaddr := range t.links.core.config.Listen {
u, err := url.Parse(listenaddr)
if err != nil {
t.links.core.log.Errorln("Failed to parse listener: listener", listenaddr, "is not correctly formatted, ignoring")
}
if _, err := t.listenURL(u, ""); err != nil {
return err
}
}
return nil
}
func (t *tcp) stop() error {
t.mutex.Lock()
for _, listener := range t.listeners {
listener.Stop()
}
t.mutex.Unlock()
t.waitgroup.Wait()
return nil
}
func (t *tcp) listenURL(u *url.URL, sintf string) (*TcpListener, error) {
var listener *TcpListener
var err error
hostport := u.Host // Used for tcp and tls
if len(sintf) != 0 {
host, port, err := net.SplitHostPort(hostport)
if err == nil {
hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port)
}
}
switch u.Scheme {
case "tcp":
listener, err = t.listen(hostport, nil)
case "tls":
listener, err = t.listen(hostport, t.tls.forListener)
default:
t.links.core.log.Errorln("Failed to add listener: listener", u.String(), "is not correctly formatted, ignoring")
}
return listener, err
}
func (t *tcp) listen(listenaddr string, upgrade *TcpUpgrade) (*TcpListener, error) {
var err error
ctx := t.links.core.ctx
lc := net.ListenConfig{
Control: t.tcpContext,
}
listener, err := lc.Listen(ctx, "tcp", listenaddr)
if err == nil {
l := TcpListener{
Listener: listener,
opts: tcpOptions{upgrade: upgrade},
stop: make(chan struct{}),
}
t.waitgroup.Add(1)
go t.listener(&l, listenaddr)
return &l, nil
}
return nil, err
}
// Runs the listener, which spawns off goroutines for incoming connections.
func (t *tcp) listener(l *TcpListener, listenaddr string) {
defer t.waitgroup.Done()
if l == nil {
return
}
// Track the listener so that we can find it again in future
t.mutex.Lock()
if _, isIn := t.listeners[listenaddr]; isIn {
t.mutex.Unlock()
l.Listener.Close()
return
}
callproto := "TCP"
if l.opts.upgrade != nil {
callproto = strings.ToUpper(l.opts.upgrade.name)
}
t.listeners[listenaddr] = l
t.mutex.Unlock()
// And here we go!
defer func() {
t.links.core.log.Infoln("Stopping", callproto, "listener on:", l.Listener.Addr().String())
l.Listener.Close()
t.mutex.Lock()
delete(t.listeners, listenaddr)
t.mutex.Unlock()
}()
t.links.core.log.Infoln("Listening for", callproto, "on:", l.Listener.Addr().String())
go func() {
<-l.stop
l.Listener.Close()
}()
defer l.Stop()
for {
sock, err := l.Listener.Accept()
if err != nil {
t.links.core.log.Errorln("Failed to accept connection:", err)
select {
case <-l.stop:
return
default:
}
time.Sleep(time.Second) // So we don't busy loop
continue
}
t.waitgroup.Add(1)
options := l.opts
go t.handler(sock, true, options)
}
}
// Checks if we already are calling this address
func (t *tcp) startCalling(saddr string) bool {
t.mutex.Lock()
defer t.mutex.Unlock()
_, isIn := t.calls[saddr]
t.calls[saddr] = struct{}{}
return !isIn
}
// 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 (t *tcp) call(saddr string, options tcpOptions, sintf string) {
go func() {
callname := saddr
callproto := "TCP"
if options.upgrade != nil {
callproto = strings.ToUpper(options.upgrade.name)
}
if sintf != "" {
callname = fmt.Sprintf("%s/%s/%s", callproto, saddr, sintf)
}
if !t.startCalling(callname) {
return
}
defer func() {
// Block new calls for a little while, to mitigate livelock scenarios
rand.Seed(time.Now().UnixNano())
delay := default_timeout + time.Duration(rand.Intn(10000))*time.Millisecond
time.Sleep(delay)
t.mutex.Lock()
delete(t.calls, callname)
t.mutex.Unlock()
}()
var conn net.Conn
var err error
if options.socksProxyAddr != "" {
if sintf != "" {
return
}
dialerdst, er := net.ResolveTCPAddr("tcp", options.socksProxyAddr)
if er != nil {
return
}
var dialer proxy.Dialer
dialer, err = proxy.SOCKS5("tcp", dialerdst.String(), options.socksProxyAuth, proxy.Direct)
if err != nil {
return
}
ctx, done := context.WithTimeout(t.links.core.ctx, default_timeout)
conn, err = dialer.(proxy.ContextDialer).DialContext(ctx, "tcp", saddr)
done()
if err != nil {
return
}
t.waitgroup.Add(1)
options.socksPeerAddr = saddr
if ch := t.handler(conn, false, options); ch != nil {
<-ch
}
} else {
dst, err := net.ResolveTCPAddr("tcp", saddr)
if err != nil {
return
}
if dst.IP.IsLinkLocalUnicast() {
dst.Zone = sintf
if dst.Zone == "" {
return
}
}
dialer := net.Dialer{
Control: t.tcpContext,
}
if sintf != "" {
dialer.Control = t.getControl(sintf)
ief, err := net.InterfaceByName(sintf)
if err != nil {
return
}
if ief.Flags&net.FlagUp == 0 {
return
}
addrs, err := ief.Addrs()
if err == nil {
for addrindex, addr := range addrs {
src, _, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if src.Equal(dst.IP) {
continue
}
if !src.IsGlobalUnicast() && !src.IsLinkLocalUnicast() {
continue
}
bothglobal := src.IsGlobalUnicast() == dst.IP.IsGlobalUnicast()
bothlinklocal := src.IsLinkLocalUnicast() == dst.IP.IsLinkLocalUnicast()
if !bothglobal && !bothlinklocal {
continue
}
if (src.To4() != nil) != (dst.IP.To4() != nil) {
continue
}
if bothglobal || bothlinklocal || addrindex == len(addrs)-1 {
dialer.LocalAddr = &net.TCPAddr{
IP: src,
Port: 0,
Zone: sintf,
}
break
}
}
if dialer.LocalAddr == nil {
return
}
}
}
ctx, done := context.WithTimeout(t.links.core.ctx, default_timeout)
conn, err = dialer.DialContext(ctx, "tcp", dst.String())
done()
if err != nil {
t.links.core.log.Debugf("Failed to dial %s: %s", callproto, err)
return
}
t.waitgroup.Add(1)
if ch := t.handler(conn, false, options); ch != nil {
<-ch
}
}
}()
}
func (t *tcp) handler(sock net.Conn, incoming bool, options tcpOptions) chan struct{} {
defer t.waitgroup.Done() // Happens after sock.close
defer sock.Close()
t.setExtraOptions(sock)
var upgraded bool
if options.upgrade != nil {
var err error
if sock, err = options.upgrade.upgrade(sock, &options); err != nil {
t.links.core.log.Errorln("TCP handler upgrade failed:", err)
return nil
}
upgraded = true
}
var name, proto, local, remote string
if options.socksProxyAddr != "" {
name = "socks://" + sock.RemoteAddr().String() + "/" + options.socksPeerAddr
proto = "socks"
local, _, _ = net.SplitHostPort(sock.LocalAddr().String())
remote, _, _ = net.SplitHostPort(options.socksPeerAddr)
} else {
if upgraded {
proto = options.upgrade.name
name = proto + "://" + sock.RemoteAddr().String()
} else {
proto = "tcp"
name = proto + "://" + sock.RemoteAddr().String()
}
local, _, _ = net.SplitHostPort(sock.LocalAddr().String())
remote, _, _ = net.SplitHostPort(sock.RemoteAddr().String())
}
localIP := net.ParseIP(local)
if localIP = localIP.To16(); localIP != nil {
var laddr address.Address
var lsubnet address.Subnet
copy(laddr[:], localIP)
copy(lsubnet[:], localIP)
if laddr.IsValid() || lsubnet.IsValid() {
// The local address is with the network address/prefix range
// This would route ygg over ygg, which we don't want
// FIXME ideally this check should happen outside of the core library
// Maybe dial/listen at the application level
// Then pass a net.Conn to the core library (after these kinds of checks are done)
t.links.core.log.Debugln("Dropping ygg-tunneled connection", local, remote)
return nil
}
}
force := net.ParseIP(strings.Split(remote, "%")[0]).IsLinkLocalUnicast()
link, err := t.links.create(sock, name, proto, local, remote, incoming, force, options.linkOptions)
if err != nil {
t.links.core.log.Println(err)
panic(err)
}
t.links.core.log.Debugln("DEBUG: starting handler for", name)
ch, err := link.handler()
t.links.core.log.Debugln("DEBUG: stopped handler for", name, err)
return ch
}

View File

@@ -1,17 +0,0 @@
// +build !darwin,!linux
package core
import (
"syscall"
)
// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go
func (t *tcp) tcpContext(network, address string, c syscall.RawConn) error {
return nil
}
func (t *tcp) getControl(sintf string) func(string, string, syscall.RawConn) error {
return t.tcpContext
}

View File

@@ -1,125 +1,63 @@
package core
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"errors"
"log"
"math/big"
"net"
"time"
"fmt"
)
type tcptls struct {
tcp *tcp
config *tls.Config
forDialer *TcpUpgrade
forListener *TcpUpgrade
func (c *Core) generateTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
config := &tls.Config{
Certificates: []tls.Certificate{*cert},
ClientAuth: tls.RequireAnyClientCert,
GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
return cert, nil
},
VerifyPeerCertificate: c.verifyTLSCertificate,
VerifyConnection: c.verifyTLSConnection,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
}
return config, nil
}
func (t *tcptls) init(tcp *tcp) {
t.tcp = tcp
t.forDialer = &TcpUpgrade{
upgrade: t.upgradeDialer,
name: "tls",
}
t.forListener = &TcpUpgrade{
upgrade: t.upgradeListener,
name: "tls",
}
edpriv := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
copy(edpriv[:], tcp.links.core.secret[:])
certBuf := &bytes.Buffer{}
// TODO: because NotAfter is finite, we should add some mechanism to regenerate the certificate and restart the listeners periodically for nodes with very high uptimes. Perhaps regenerate certs and restart listeners every few months or so.
pubtemp := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: hex.EncodeToString(tcp.links.core.public[:]),
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
derbytes, err := x509.CreateCertificate(rand.Reader, &pubtemp, &pubtemp, edpriv.Public(), edpriv)
if err != nil {
log.Fatalf("Failed to create certificate: %s", err)
}
if err := pem.Encode(certBuf, &pem.Block{Type: "CERTIFICATE", Bytes: derbytes}); err != nil {
panic("failed to encode certificate into PEM")
}
cpool := x509.NewCertPool()
cpool.AppendCertsFromPEM(derbytes)
t.config = &tls.Config{
RootCAs: cpool,
Certificates: []tls.Certificate{
{
Certificate: [][]byte{derbytes},
PrivateKey: edpriv,
},
},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
}
}
func (t *tcptls) configForOptions(options *tcpOptions) *tls.Config {
config := *t.config
config.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) != 1 {
return errors.New("tls not exactly 1 cert")
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return errors.New("tls failed to parse cert")
}
if cert.PublicKeyAlgorithm != x509.Ed25519 {
return errors.New("tls wrong cert algorithm")
}
pk := cert.PublicKey.(ed25519.PublicKey)
var key keyArray
copy(key[:], pk)
// If options does not have a pinned key, then pin one now
if options.pinnedEd25519Keys == nil {
options.pinnedEd25519Keys = make(map[keyArray]struct{})
options.pinnedEd25519Keys[key] = struct{}{}
}
if _, isIn := options.pinnedEd25519Keys[key]; !isIn {
return errors.New("tls key does not match pinned key")
}
func (c *Core) verifyTLSCertificate(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if c.config.roots == nil {
// If there's no certificate pool configured then we will
// accept all TLS certificates.
return nil
}
return &config
if len(rawCerts) == 0 {
return fmt.Errorf("expected at least one certificate")
}
opts := x509.VerifyOptions{
Roots: c.config.roots,
}
for i, rawCert := range rawCerts {
if i == 0 {
// The first certificate is the leaf certificate. All other
// certificates in the list are intermediates, so add them
// into the VerifyOptions.
continue
}
cert, err := x509.ParseCertificate(rawCert)
if err != nil {
return fmt.Errorf("failed to parse intermediate certificate: %w", err)
}
opts.Intermediates.AddCert(cert)
}
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("failed to parse leaf certificate: %w", err)
}
_, err = cert.Verify(opts)
return err
}
func (t *tcptls) upgradeListener(c net.Conn, options *tcpOptions) (net.Conn, error) {
config := t.configForOptions(options)
conn := tls.Server(c, config)
if err := conn.Handshake(); err != nil {
return c, err
}
return conn, nil
}
func (t *tcptls) upgradeDialer(c net.Conn, options *tcpOptions) (net.Conn, error) {
config := t.configForOptions(options)
conn := tls.Client(c, config)
if err := conn.Handshake(); err != nil {
return c, err
}
return conn, nil
func (c *Core) verifyTLSConnection(cs tls.ConnectionState) error {
return nil
}

View File

@@ -1,12 +1,5 @@
package core
// Out-of-band packet types
const (
typeKeyDummy = iota // nolint:deadcode,varcheck
typeKeyLookup
typeKeyResponse
)
// In-band packet types
const (
typeSessionDummy = iota // nolint:deadcode,varcheck

View File

@@ -4,65 +4,107 @@ package core
// Used in the initial connection setup and key exchange
// Some of this could arguably go in wire.go instead
import "crypto/ed25519"
import (
"bytes"
"crypto/ed25519"
"encoding/binary"
)
// This is the version-specific metadata exchanged at the start of a connection.
// It must always begin with the 4 bytes "meta" and a wire formatted uint64 major version number.
// The current version also includes a minor version number, and the box/sig/link keys that need to be exchanged to open a connection.
type version_metadata struct {
meta [4]byte
ver uint8 // 1 byte in this version
// Everything after this point potentially depends on the version number, and is subject to change in future versions
minorVer uint8 // 1 byte in this version
key ed25519.PublicKey
majorVer uint16
minorVer uint16
publicKey ed25519.PublicKey
priority uint8
}
const (
ProtocolVersionMajor uint16 = 0
ProtocolVersionMinor uint16 = 5
)
const (
metaVersionMajor uint16 = iota // uint16
metaVersionMinor // uint16
metaPublicKey // [32]byte
metaPriority // uint8
)
// Gets a base metadata with no keys set, but with the correct version numbers.
func version_getBaseMetadata() version_metadata {
return version_metadata{
meta: [4]byte{'m', 'e', 't', 'a'},
ver: 0,
minorVer: 4,
majorVer: ProtocolVersionMajor,
minorVer: ProtocolVersionMinor,
}
}
// Gets the length of the metadata for this version, used to know how many bytes to read from the start of a connection.
func version_getMetaLength() (mlen int) {
mlen += 4 // meta
mlen++ // ver, as long as it's < 127, which it is in this version
mlen++ // minorVer, as long as it's < 127, which it is in this version
mlen += ed25519.PublicKeySize // key
return
}
// Encodes version metadata into its wire format.
func (m *version_metadata) encode() []byte {
bs := make([]byte, 0, version_getMetaLength())
bs = append(bs, m.meta[:]...)
bs = append(bs, m.ver)
bs = append(bs, m.minorVer)
bs = append(bs, m.key[:]...)
if len(bs) != version_getMetaLength() {
panic("Inconsistent metadata length")
}
bs := make([]byte, 0, 64)
bs = append(bs, 'm', 'e', 't', 'a')
bs = binary.BigEndian.AppendUint16(bs, metaVersionMajor)
bs = binary.BigEndian.AppendUint16(bs, 2)
bs = binary.BigEndian.AppendUint16(bs, m.majorVer)
bs = binary.BigEndian.AppendUint16(bs, metaVersionMinor)
bs = binary.BigEndian.AppendUint16(bs, 2)
bs = binary.BigEndian.AppendUint16(bs, m.minorVer)
bs = binary.BigEndian.AppendUint16(bs, metaPublicKey)
bs = binary.BigEndian.AppendUint16(bs, ed25519.PublicKeySize)
bs = append(bs, m.publicKey[:]...)
bs = binary.BigEndian.AppendUint16(bs, metaPriority)
bs = binary.BigEndian.AppendUint16(bs, 1)
bs = append(bs, m.priority)
return bs
}
// Decodes version metadata from its wire format into the struct.
func (m *version_metadata) decode(bs []byte) bool {
if len(bs) != version_getMetaLength() {
meta := [4]byte{'m', 'e', 't', 'a'}
if !bytes.Equal(bs[:4], meta[:]) {
return false
}
offset := 0
offset += copy(m.meta[:], bs[offset:])
m.ver, offset = bs[offset], offset+1
m.minorVer, offset = bs[offset], offset+1
m.key = append([]byte(nil), bs[offset:]...)
for bs = bs[4:]; len(bs) >= 4; {
op := binary.BigEndian.Uint16(bs[:2])
oplen := binary.BigEndian.Uint16(bs[2:4])
if bs = bs[4:]; len(bs) < int(oplen) {
break
}
switch op {
case metaVersionMajor:
m.majorVer = binary.BigEndian.Uint16(bs[:2])
case metaVersionMinor:
m.minorVer = binary.BigEndian.Uint16(bs[:2])
case metaPublicKey:
m.publicKey = make(ed25519.PublicKey, ed25519.PublicKeySize)
copy(m.publicKey, bs[:ed25519.PublicKeySize])
case metaPriority:
m.priority = bs[0]
}
bs = bs[oplen:]
}
return true
}
// Checks that the "meta" bytes and the version numbers are the expected values.
func (m *version_metadata) check() bool {
base := version_getBaseMetadata()
return base.meta == m.meta && base.ver == m.ver && base.minorVer == m.minorVer
switch {
case m.majorVer != ProtocolVersionMajor:
return false
case m.minorVer != ProtocolVersionMinor:
return false
case len(m.publicKey) != ed25519.PublicKeySize:
return false
default:
return true
}
}

34
src/core/version_test.go Normal file
View File

@@ -0,0 +1,34 @@
package core
import (
"crypto/ed25519"
"math/rand"
"reflect"
"testing"
)
func TestVersionRoundtrip(t *testing.T) {
for _, test := range []*version_metadata{
{majorVer: 1},
{majorVer: 256},
{majorVer: 2, minorVer: 4},
{majorVer: 2, minorVer: 257},
{majorVer: 258, minorVer: 259},
{majorVer: 3, minorVer: 5, priority: 6},
{majorVer: 260, minorVer: 261, priority: 7},
} {
// Generate a random public key for each time, since it is
// a required field.
test.publicKey = make(ed25519.PublicKey, ed25519.PublicKeySize)
rand.Read(test.publicKey)
encoded := test.encode()
decoded := &version_metadata{}
if !decoded.decode(encoded) {
t.Fatalf("failed to decode")
}
if !reflect.DeepEqual(test, decoded) {
t.Fatalf("round-trip failed\nwant: %+v\n got: %+v", test, decoded)
}
}
}

View File

@@ -1,44 +0,0 @@
package defaults
import "github.com/yggdrasil-network/yggdrasil-go/src/config"
type MulticastInterfaceConfig = config.MulticastInterfaceConfig
// 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
// Multicast interfaces
DefaultMulticastInterfaces []MulticastInterfaceConfig
// TUN/TAP
MaximumIfMTU uint64
DefaultIfMTU uint64
DefaultIfName string
}
// Generates default configuration and returns a pointer to the resulting
// NodeConfig. This is used when outputting the -genconf parameter and also when
// using -autoconf.
func GenerateConfig() *config.NodeConfig {
// Create a node configuration and populate it.
cfg := new(config.NodeConfig)
cfg.NewKeys()
cfg.Listen = []string{}
cfg.AdminListen = GetDefaults().DefaultAdminListen
cfg.Peers = []string{}
cfg.InterfacePeers = map[string][]string{}
cfg.AllowedPublicKeys = []string{}
cfg.MulticastInterfaces = GetDefaults().DefaultMulticastInterfaces
cfg.IfName = GetDefaults().DefaultIfName
cfg.IfMTU = GetDefaults().DefaultIfMTU
cfg.NodeInfoPrivacy = false
return cfg
}

View File

@@ -1,4 +1,4 @@
package core
package ipv6rwc
// The ICMPv6 module implements functions to easily create ICMPv6
// packets. These functions, when mixed with the built-in Go IPv6

View File

@@ -1,4 +1,4 @@
package core
package ipv6rwc
import (
"crypto/ed25519"
@@ -14,14 +14,24 @@ import (
iwt "github.com/Arceliar/ironwood/types"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
)
const keyStoreTimeout = 2 * time.Minute
/*
// Out-of-band packet types
const (
typeKeyDummy = iota // nolint:deadcode,varcheck
typeKeyLookup
typeKeyResponse
)
*/
type keyArray [ed25519.PublicKeySize]byte
type keyStore struct {
core *Core
core *core.Core
address address.Address
subnet address.Subnet
mutex sync.Mutex
@@ -45,14 +55,17 @@ type buffer struct {
timeout *time.Timer
}
func (k *keyStore) init(core *Core) {
k.core = core
k.address = *address.AddrForKey(k.core.public)
k.subnet = *address.SubnetForKey(k.core.public)
if err := k.core.pc.SetOutOfBandHandler(k.oobHandler); err != nil {
func (k *keyStore) init(c *core.Core) {
k.core = c
k.address = *address.AddrForKey(k.core.PublicKey())
k.subnet = *address.SubnetForKey(k.core.PublicKey())
/*if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil {
err = fmt.Errorf("tun.core.SetOutOfBandHander: %w", err)
panic(err)
}
}*/
k.core.SetPathNotify(func(key ed25519.PublicKey) {
k.update(key)
})
k.keyToInfo = make(map[keyArray]*keyInfo)
k.addrToInfo = make(map[address.Address]*keyInfo)
k.addrBuffer = make(map[address.Address]*buffer)
@@ -66,7 +79,7 @@ func (k *keyStore) sendToAddress(addr address.Address, bs []byte) {
if info := k.addrToInfo[addr]; info != nil {
k.resetTimeout(info)
k.mutex.Unlock()
_, _ = k.core.pc.WriteTo(bs, iwt.Addr(info.key[:]))
_, _ = k.core.WriteTo(bs, iwt.Addr(info.key[:]))
} else {
var buf *buffer
if buf = k.addrBuffer[addr]; buf == nil {
@@ -95,7 +108,7 @@ func (k *keyStore) sendToSubnet(subnet address.Subnet, bs []byte) {
if info := k.subnetToInfo[subnet]; info != nil {
k.resetTimeout(info)
k.mutex.Unlock()
_, _ = k.core.pc.WriteTo(bs, iwt.Addr(info.key[:]))
_, _ = k.core.WriteTo(bs, iwt.Addr(info.key[:]))
} else {
var buf *buffer
if buf = k.subnetBuffer[subnet]; buf == nil {
@@ -124,6 +137,7 @@ func (k *keyStore) update(key ed25519.PublicKey) *keyInfo {
var kArray keyArray
copy(kArray[:], key)
var info *keyInfo
var packets [][]byte
if info = k.keyToInfo[kArray]; info == nil {
info = new(keyInfo)
info.key = kArray
@@ -132,19 +146,19 @@ func (k *keyStore) update(key ed25519.PublicKey) *keyInfo {
k.keyToInfo[info.key] = info
k.addrToInfo[info.address] = info
k.subnetToInfo[info.subnet] = info
k.resetTimeout(info)
k.mutex.Unlock()
if buf := k.addrBuffer[info.address]; buf != nil {
k.core.pc.WriteTo(buf.packet, iwt.Addr(info.key[:]))
packets = append(packets, buf.packet)
delete(k.addrBuffer, info.address)
}
if buf := k.subnetBuffer[info.subnet]; buf != nil {
k.core.pc.WriteTo(buf.packet, iwt.Addr(info.key[:]))
packets = append(packets, buf.packet)
delete(k.subnetBuffer, info.subnet)
}
} else {
k.resetTimeout(info)
k.mutex.Unlock()
}
k.resetTimeout(info)
k.mutex.Unlock()
for _, packet := range packets {
_, _ = k.core.WriteTo(packet, iwt.Addr(info.key[:]))
}
return info
}
@@ -168,7 +182,8 @@ func (k *keyStore) resetTimeout(info *keyInfo) {
})
}
func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) {
/*
func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { // nolint:unused
if len(data) != 1+ed25519.SignatureSize {
return
}
@@ -189,48 +204,39 @@ func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) {
}
}
}
*/
func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) {
sig := ed25519.Sign(k.core.secret, partial[:])
bs := append([]byte{typeKeyLookup}, sig...)
_ = k.core.pc.SendOutOfBand(partial, bs)
/*
sig := ed25519.Sign(k.core.PrivateKey(), partial[:])
bs := append([]byte{typeKeyLookup}, sig...)
//_ = k.core.SendOutOfBand(partial, bs)
_ = bs
*/
k.core.SendLookup(partial)
}
func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) {
sig := ed25519.Sign(k.core.secret, dest[:])
/*
func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) { // nolint:unused
sig := ed25519.Sign(k.core.PrivateKey(), dest[:])
bs := append([]byte{typeKeyResponse}, sig...)
_ = k.core.pc.SendOutOfBand(dest, bs)
}
func (k *keyStore) maxSessionMTU() uint64 {
const sessionTypeOverhead = 1
return k.core.pc.MTU() - sessionTypeOverhead
//_ = k.core.SendOutOfBand(dest, bs)
_ = bs
}
*/
func (k *keyStore) readPC(p []byte) (int, error) {
buf := make([]byte, k.core.pc.MTU(), 65535)
buf := make([]byte, k.core.MTU(), 65535)
for {
bs := buf
n, from, err := k.core.pc.ReadFrom(bs)
n, from, err := k.core.ReadFrom(bs)
if err != nil {
return n, err
}
if n == 0 {
continue
}
switch bs[0] {
case typeSessionTraffic:
// This is what we want to handle here
case typeSessionProto:
var key keyArray
copy(key[:], from.(iwt.Addr))
data := append([]byte(nil), bs[1:n]...)
k.core.proto.handleProto(nil, key, data)
continue
default:
continue
}
bs = bs[1:n]
bs = bs[:n]
if len(bs) == 0 {
continue
}
@@ -245,11 +251,11 @@ func (k *keyStore) readPC(p []byte) (int, error) {
k.mutex.Unlock()
if len(bs) > mtu {
// Using bs would make it leak off the stack, so copy to buf
buf := make([]byte, 40)
copy(buf, bs)
buf := make([]byte, 512)
cn := copy(buf, bs)
ptb := &icmp.PacketTooBig{
MTU: mtu,
Data: buf[:40],
Data: buf[:cn],
}
if packet, err := CreateICMPv6(buf[8:24], buf[24:40], ipv6.ICMPTypePacketTooBig, 0, ptb); err == nil {
_, _ = k.writePC(packet)
@@ -294,15 +300,69 @@ func (k *keyStore) writePC(bs []byte) (int, error) {
strErr := fmt.Sprint("incorrect source address: ", net.IP(srcAddr[:]).String())
return 0, errors.New(strErr)
}
buf := make([]byte, 1+len(bs), 65535)
buf[0] = typeSessionTraffic
copy(buf[1:], bs)
if dstAddr.IsValid() {
k.sendToAddress(dstAddr, buf)
k.sendToAddress(dstAddr, bs)
} else if dstSubnet.IsValid() {
k.sendToSubnet(dstSubnet, buf)
k.sendToSubnet(dstSubnet, bs)
} else {
return 0, errors.New("invalid destination address")
}
return len(bs), nil
}
// Exported API
func (k *keyStore) MaxMTU() uint64 {
return k.core.MTU()
}
func (k *keyStore) SetMTU(mtu uint64) {
if mtu > k.MaxMTU() {
mtu = k.MaxMTU()
}
if mtu < 1280 {
mtu = 1280
}
k.mutex.Lock()
k.mtu = mtu
k.mutex.Unlock()
}
func (k *keyStore) MTU() uint64 {
k.mutex.Lock()
mtu := k.mtu
k.mutex.Unlock()
return mtu
}
type ReadWriteCloser struct {
keyStore
}
func NewReadWriteCloser(c *core.Core) *ReadWriteCloser {
rwc := new(ReadWriteCloser)
rwc.init(c)
return rwc
}
func (rwc *ReadWriteCloser) Address() address.Address {
return rwc.address
}
func (rwc *ReadWriteCloser) Subnet() address.Subnet {
return rwc.subnet
}
func (rwc *ReadWriteCloser) Read(p []byte) (n int, err error) {
return rwc.readPC(p)
}
func (rwc *ReadWriteCloser) Write(p []byte) (n int, err error) {
return rwc.writePC(p)
}
func (rwc *ReadWriteCloser) Close() error {
err := rwc.core.Close()
rwc.core.Stop()
return err
}

View File

@@ -20,15 +20,18 @@ func (m *Multicast) getMulticastInterfacesHandler(req *GetMulticastInterfacesReq
}
func (m *Multicast) SetupAdminHandlers(a *admin.AdminSocket) {
_ = a.AddHandler("getMulticastInterfaces", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetMulticastInterfacesRequest{}
res := &GetMulticastInterfacesResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := m.getMulticastInterfacesHandler(req, res); err != nil {
return nil, err
}
return res, nil
})
_ = a.AddHandler(
"getMulticastInterfaces", "Show which interfaces multicast is enabled on", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetMulticastInterfacesRequest{}
res := &GetMulticastInterfacesResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := m.getMulticastInterfacesHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
}

View File

@@ -0,0 +1,28 @@
package multicast
import (
"crypto/ed25519"
"encoding/binary"
"fmt"
)
type multicastAdvertisement struct {
PublicKey ed25519.PublicKey
Port uint16
}
func (m *multicastAdvertisement) MarshalBinary() ([]byte, error) {
b := make([]byte, 0, ed25519.PublicKeySize+2)
b = append(b, m.PublicKey...)
b = binary.BigEndian.AppendUint16(b, m.Port)
return b, nil
}
func (m *multicastAdvertisement) UnmarshalBinary(b []byte) error {
if len(b) < ed25519.PublicKeySize+2 {
return fmt.Errorf("invalid multicast beacon")
}
m.PublicKey = b[:ed25519.PublicKeySize]
m.Port = binary.BigEndian.Uint16(b[ed25519.PublicKeySize:])
return nil
}

View File

@@ -1,21 +1,17 @@
package multicast
import (
"bytes"
"context"
"crypto/ed25519"
"encoding/binary"
"encoding/hex"
"fmt"
"math/rand"
"net"
"net/url"
"regexp"
"time"
"github.com/Arceliar/phony"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"golang.org/x/net/ipv6"
)
@@ -27,64 +23,70 @@ import (
type Multicast struct {
phony.Inbox
core *core.Core
config *config.NodeConfig
log *log.Logger
sock *ipv6.PacketConn
groupAddr string
listeners map[string]*listenerInfo
isOpen bool
_interfaces map[string]interfaceInfo
_isOpen bool
_listeners map[string]*listenerInfo
_interfaces map[string]*interfaceInfo
_timer *time.Timer
config struct {
_groupAddr GroupAddress
_interfaces map[MulticastInterface]struct{}
}
}
type interfaceInfo struct {
iface net.Interface
addrs []net.Addr
beacon bool
listen bool
port uint16
iface net.Interface
addrs []net.Addr
beacon bool
listen bool
port uint16
priority uint8
}
type listenerInfo struct {
listener *core.TcpListener
listener *core.Listener
time time.Time
interval time.Duration
port uint16
}
// Init prepares the multicast interface for use.
func (m *Multicast) Init(core *core.Core, nc *config.NodeConfig, log *log.Logger, options interface{}) error {
m.core = core
m.config = nc
m.log = log
m.listeners = make(map[string]*listenerInfo)
m._interfaces = make(map[string]interfaceInfo)
m.groupAddr = "[ff02::114]:9001"
return nil
}
// Start starts the multicast interface. This launches goroutines which will
// listen for multicast beacons from other hosts and will advertise multicast
// beacons out to the network.
func (m *Multicast) Start() error {
func New(core *core.Core, log *log.Logger, opts ...SetupOption) (*Multicast, error) {
m := &Multicast{
core: core,
log: log,
_listeners: make(map[string]*listenerInfo),
_interfaces: make(map[string]*interfaceInfo),
}
m.config._interfaces = map[MulticastInterface]struct{}{}
m.config._groupAddr = GroupAddress("[ff02::114]:9001")
for _, opt := range opts {
m._applyOption(opt)
}
var err error
phony.Block(m, func() {
err = m._start()
})
m.log.Debugln("Started multicast module")
return err
return m, err
}
func (m *Multicast) _start() error {
if m.isOpen {
if m._isOpen {
return fmt.Errorf("multicast module is already started")
}
m.config.RLock()
defer m.config.RUnlock()
if len(m.config.MulticastInterfaces) == 0 {
var anyEnabled bool
for intf := range m.config._interfaces {
anyEnabled = anyEnabled || intf.Beacon || intf.Listen
}
if !anyEnabled {
return nil
}
m.log.Infoln("Starting multicast module")
addr, err := net.ResolveUDPAddr("udp", m.groupAddr)
m.log.Debugln("Starting multicast module")
defer m.log.Debugln("Started multicast module")
addr, err := net.ResolveUDPAddr("udp", string(m.config._groupAddr))
if err != nil {
return err
}
@@ -101,7 +103,7 @@ func (m *Multicast) _start() error {
// Windows can't set this flag, so we need to handle it in other ways
}
m.isOpen = true
m._isOpen = true
go m.listen()
m.Act(nil, m._multicastStarted)
m.Act(nil, m._announce)
@@ -113,7 +115,7 @@ func (m *Multicast) _start() error {
func (m *Multicast) IsStarted() bool {
var isOpen bool
phony.Block(m, func() {
isOpen = m.isOpen
isOpen = m._isOpen
})
return isOpen
}
@@ -130,7 +132,7 @@ func (m *Multicast) Stop() error {
func (m *Multicast) _stop() error {
m.log.Infoln("Stopping multicast module")
m.isOpen = false
m._isOpen = false
if m.sock != nil {
m.sock.Close()
}
@@ -138,7 +140,7 @@ func (m *Multicast) _stop() error {
}
func (m *Multicast) _updateInterfaces() {
interfaces := m.getAllowedInterfaces()
interfaces := m._getAllowedInterfaces()
for name, info := range interfaces {
addrs, err := info.iface.Addrs()
if err != nil {
@@ -163,10 +165,8 @@ func (m *Multicast) Interfaces() map[string]net.Interface {
}
// getAllowedInterfaces returns the currently known/enabled multicast interfaces.
func (m *Multicast) getAllowedInterfaces() map[string]interfaceInfo {
interfaces := make(map[string]interfaceInfo)
// Get interface expressions from config
ifcfgs := m.config.MulticastInterfaces
func (m *Multicast) _getAllowedInterfaces() map[string]*interfaceInfo {
interfaces := make(map[string]*interfaceInfo)
// Ask the system for network interfaces
allifaces, err := net.Interfaces()
if err != nil {
@@ -176,62 +176,65 @@ func (m *Multicast) getAllowedInterfaces() map[string]interfaceInfo {
}
// Work out which interfaces to announce on
for _, iface := range allifaces {
if iface.Flags&net.FlagUp == 0 {
// Ignore interfaces that are down
continue
switch {
case iface.Flags&net.FlagUp == 0:
continue // Ignore interfaces that are down
case iface.Flags&net.FlagMulticast == 0:
continue // Ignore non-multicast interfaces
case iface.Flags&net.FlagPointToPoint != 0:
continue // Ignore point-to-point interfaces
}
if iface.Flags&net.FlagMulticast == 0 {
// Ignore non-multicast interfaces
continue
}
if iface.Flags&net.FlagPointToPoint != 0 {
// Ignore point-to-point interfaces
continue
}
for _, ifcfg := range ifcfgs {
for ifcfg := range m.config._interfaces {
// Compile each regular expression
e, err := regexp.Compile(ifcfg.Regex)
if err != nil {
panic(err)
}
// Does the interface match the regular expression? Store it if so
if e.MatchString(iface.Name) {
if ifcfg.Beacon || ifcfg.Listen {
info := interfaceInfo{
iface: iface,
beacon: ifcfg.Beacon,
listen: ifcfg.Listen,
port: ifcfg.Port,
}
interfaces[iface.Name] = info
}
break
if !ifcfg.Beacon && !ifcfg.Listen {
continue
}
if !ifcfg.Regex.MatchString(iface.Name) {
continue
}
interfaces[iface.Name] = &interfaceInfo{
iface: iface,
beacon: ifcfg.Beacon,
listen: ifcfg.Listen,
port: ifcfg.Port,
priority: ifcfg.Priority,
}
break
}
}
return interfaces
}
func (m *Multicast) AnnounceNow() {
phony.Block(m, func() {
if m._timer != nil && !m._timer.Stop() {
<-m._timer.C
}
m.Act(nil, m._announce)
})
}
func (m *Multicast) _announce() {
if !m.isOpen {
if !m._isOpen {
return
}
m._updateInterfaces()
groupAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr)
groupAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr))
if err != nil {
panic(err)
}
destAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr)
destAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr))
if err != nil {
panic(err)
}
// There might be interfaces that we configured listeners for but are no
// longer up - if that's the case then we should stop the listeners
for name, info := range m.listeners {
for name, info := range m._listeners {
// Prepare our stop function!
stop := func() {
info.listener.Stop()
delete(m.listeners, name)
info.listener.Close()
delete(m._listeners, name)
m.log.Debugln("No longer multicasting on", name)
}
// If the interface is no longer visible on the system then stop the
@@ -243,7 +246,7 @@ func (m *Multicast) _announce() {
// It's possible that the link-local listener address has changed so if
// that is the case then we should clean up the interface listener
found := false
listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Listener.Addr().String())
listenaddr, err := net.ResolveTCPAddr("tcp6", info.listener.Addr().String())
if err != nil {
stop()
continue
@@ -290,7 +293,7 @@ func (m *Multicast) _announce() {
}
// Try and see if we already have a TCP listener for this interface
var linfo *listenerInfo
if nfo, ok := m.listeners[iface.Name]; !ok || nfo.listener.Listener == nil {
if _, ok := m._listeners[iface.Name]; !ok {
// No listener was found - let's create one
urlString := fmt.Sprintf("tls://[%s]:%d", addrIP, info.port)
u, err := url.Parse(urlString)
@@ -300,14 +303,14 @@ func (m *Multicast) _announce() {
if li, err := m.core.Listen(u, iface.Name); err == nil {
m.log.Debugln("Started multicasting on", iface.Name)
// Store the listener so that we can stop it later if needed
linfo = &listenerInfo{listener: li, time: time.Now()}
m.listeners[iface.Name] = linfo
linfo = &listenerInfo{listener: li, time: time.Now(), port: info.port}
m._listeners[iface.Name] = linfo
} else {
m.log.Warnln("Not multicasting on", iface.Name, "due to error:", err)
}
} else {
// An existing listener was found
linfo = m.listeners[iface.Name]
linfo = m._listeners[iface.Name]
}
// Make sure nothing above failed for some reason
if linfo == nil {
@@ -316,17 +319,18 @@ func (m *Multicast) _announce() {
if time.Since(linfo.time) < linfo.interval {
continue
}
// Get the listener details and construct the multicast beacon
lladdr := linfo.listener.Listener.Addr().String()
if a, err := net.ResolveTCPAddr("tcp6", lladdr); err == nil {
a.Zone = ""
destAddr.Zone = iface.Name
msg := append([]byte(nil), m.core.GetSelf().Key...)
msg = append(msg, a.IP...)
pbs := make([]byte, 2)
binary.BigEndian.PutUint16(pbs, uint16(a.Port))
msg = append(msg, pbs...)
_, _ = m.sock.WriteTo(msg, nil, destAddr)
addr := linfo.listener.Addr().(*net.TCPAddr)
adv := multicastAdvertisement{
PublicKey: m.core.PublicKey(),
Port: uint16(addr.Port),
}
msg, err := adv.MarshalBinary()
if err != nil {
continue
}
destAddr.Zone = iface.Name
if _, err = m.sock.WriteTo(msg, nil, destAddr); err != nil {
m.log.Warn("Failed to send multicast beacon:", err)
}
if linfo.interval.Seconds() < 15 {
linfo.interval += time.Second
@@ -334,19 +338,20 @@ func (m *Multicast) _announce() {
break
}
}
time.AfterFunc(time.Second, func() {
annInterval := time.Second + time.Microsecond*(time.Duration(rand.Intn(1048576))) // Randomize delay
m._timer = time.AfterFunc(annInterval, func() {
m.Act(nil, m._announce)
})
}
func (m *Multicast) listen() {
groupAddr, err := net.ResolveUDPAddr("udp6", m.groupAddr)
groupAddr, err := net.ResolveUDPAddr("udp6", string(m.config._groupAddr))
if err != nil {
panic(err)
}
bs := make([]byte, 2048)
for {
nBytes, rcm, fromAddr, err := m.sock.ReadFrom(bs)
n, rcm, fromAddr, err := m.sock.ReadFrom(bs)
if err != nil {
if !m.IsStarted() {
return
@@ -364,40 +369,27 @@ func (m *Multicast) listen() {
continue
}
}
if nBytes < ed25519.PublicKeySize {
var adv multicastAdvertisement
if err := adv.UnmarshalBinary(bs[:n]); err != nil {
continue
}
var key ed25519.PublicKey
key = append(key, bs[:ed25519.PublicKeySize]...)
if bytes.Equal(key, m.core.GetSelf().Key) {
continue // don't bother trying to peer with self
}
begin := ed25519.PublicKeySize
end := nBytes - 2
if end <= begin {
continue // malformed address
}
ip := bs[begin:end]
port := binary.BigEndian.Uint16(bs[end:nBytes])
anAddr := net.TCPAddr{IP: ip, Port: int(port)}
addr, err := net.ResolveTCPAddr("tcp6", anAddr.String())
if err != nil {
if adv.PublicKey.Equal(m.core.PublicKey()) {
continue
}
from := fromAddr.(*net.UDPAddr)
if !from.IP.Equal(addr.IP) {
continue
}
var interfaces map[string]interfaceInfo
from.Port = int(adv.Port)
var interfaces map[string]*interfaceInfo
phony.Block(m, func() {
interfaces = m._interfaces
})
if info, ok := interfaces[from.Zone]; ok && info.listen {
addr.Zone = ""
pin := fmt.Sprintf("/?key=%s", hex.EncodeToString(key))
u, err := url.Parse("tls://" + addr.String() + pin)
if err != nil {
m.log.Debugln("Call from multicast failed, parse error:", addr.String(), err)
v := &url.Values{}
v.Add("key", hex.EncodeToString(adv.PublicKey))
v.Add("priority", fmt.Sprintf("%d", info.priority))
u := &url.URL{
Scheme: "tls",
Host: from.String(),
RawQuery: v.Encode(),
}
if err := m.core.CallPeer(u, from.Zone); err != nil {
m.log.Debugln("Call from multicast failed:", err)

View File

@@ -1,48 +1,17 @@
// +build darwin
//go:build !cgo && (darwin || ios)
// +build !cgo
// +build darwin ios
package multicast
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
NSNetServiceBrowser *serviceBrowser;
void StartAWDLBrowsing() {
if (serviceBrowser == nil) {
serviceBrowser = [[NSNetServiceBrowser alloc] init];
serviceBrowser.includesPeerToPeer = YES;
}
[serviceBrowser searchForServicesOfType:@"_yggdrasil._tcp" inDomain:@""];
}
void StopAWDLBrowsing() {
if (serviceBrowser == nil) {
return;
}
[serviceBrowser stop];
}
*/
import "C"
import (
"syscall"
"time"
"golang.org/x/sys/unix"
)
func (m *Multicast) _multicastStarted() {
if !m.isOpen {
return
}
C.StopAWDLBrowsing()
for intf := range m._interfaces {
if intf == "awdl0" {
C.StartAWDLBrowsing()
break
}
}
time.AfterFunc(time.Minute, func() {
m.Act(nil, m._multicastStarted)
})
}
func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error {

View File

@@ -0,0 +1,69 @@
//go:build (darwin && cgo) || (ios && cgo)
// +build darwin,cgo ios,cgo
package multicast
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
NSNetServiceBrowser *serviceBrowser;
void StartAWDLBrowsing() {
if (serviceBrowser == nil) {
serviceBrowser = [[NSNetServiceBrowser alloc] init];
serviceBrowser.includesPeerToPeer = YES;
}
[serviceBrowser searchForServicesOfType:@"_yggdrasil._tcp" inDomain:@""];
}
void StopAWDLBrowsing() {
if (serviceBrowser == nil) {
return;
}
[serviceBrowser stop];
}
*/
import "C"
import (
"syscall"
"time"
"golang.org/x/sys/unix"
)
func (m *Multicast) _multicastStarted() {
if !m._isOpen {
return
}
C.StopAWDLBrowsing()
for intf := range m._interfaces {
if intf == "awdl0" {
C.StartAWDLBrowsing()
break
}
}
time.AfterFunc(time.Minute, func() {
m.Act(nil, m._multicastStarted)
})
}
func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error {
var control error
var reuseport error
var recvanyif error
control = c.Control(func(fd uintptr) {
reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
// sys/socket.h: #define SO_RECV_ANYIF 0x1104
recvanyif = unix.SetsockoptInt(int(fd), syscall.SOL_SOCKET, 0x1104, 1)
})
switch {
case reuseport != nil:
return reuseport
case recvanyif != nil:
return recvanyif
default:
return control
}
}

View File

@@ -1,4 +1,5 @@
// +build !linux,!darwin,!netbsd,!freebsd,!openbsd,!dragonflybsd,!windows
//go:build !linux && !darwin && !ios && !netbsd && !freebsd && !openbsd && !dragonflybsd && !windows
// +build !linux,!darwin,!ios,!netbsd,!freebsd,!openbsd,!dragonflybsd,!windows
package multicast

View File

@@ -1,9 +1,13 @@
//go:build linux || netbsd || freebsd || openbsd || dragonflybsd
// +build linux netbsd freebsd openbsd dragonflybsd
package multicast
import "syscall"
import "golang.org/x/sys/unix"
import (
"syscall"
"golang.org/x/sys/unix"
)
func (m *Multicast) _multicastStarted() {
@@ -11,15 +15,19 @@ func (m *Multicast) _multicastStarted() {
func (m *Multicast) multicastReuse(network string, address string, c syscall.RawConn) error {
var control error
var reuseport error
var reuseaddr error
control = c.Control(func(fd uintptr) {
reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
// Previously we used SO_REUSEPORT here, but that meant that machines running
// Yggdrasil nodes as different users would inevitably fail with EADDRINUSE.
// The behaviour for multicast is similar with both, so we'll use SO_REUSEADDR
// instead.
reuseaddr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
})
switch {
case reuseport != nil:
return reuseport
case reuseaddr != nil:
return reuseaddr
default:
return control
}

View File

@@ -1,9 +1,13 @@
//go:build windows
// +build windows
package multicast
import "syscall"
import "golang.org/x/sys/windows"
import (
"syscall"
"golang.org/x/sys/windows"
)
func (m *Multicast) _multicastStarted() {

29
src/multicast/options.go Normal file
View File

@@ -0,0 +1,29 @@
package multicast
import "regexp"
func (m *Multicast) _applyOption(opt SetupOption) {
switch v := opt.(type) {
case MulticastInterface:
m.config._interfaces[v] = struct{}{}
case GroupAddress:
m.config._groupAddr = v
}
}
type SetupOption interface {
isSetupOption()
}
type MulticastInterface struct {
Regex *regexp.Regexp
Beacon bool
Listen bool
Port uint16
Priority uint8
}
type GroupAddress string
func (a MulticastInterface) isSetupOption() {}
func (a GroupAddress) isSetupOption() {}

45
src/tun/admin.go Normal file
View File

@@ -0,0 +1,45 @@
package tun
import (
"encoding/json"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
)
type GetTUNRequest struct{}
type GetTUNResponse struct {
Enabled bool `json:"enabled"`
Name string `json:"name,omitempty"`
MTU uint64 `json:"mtu,omitempty"`
}
type TUNEntry struct {
MTU uint64 `json:"mtu"`
}
func (t *TunAdapter) getTUNHandler(req *GetTUNRequest, res *GetTUNResponse) error {
res.Enabled = t.isEnabled
if !t.isEnabled {
return nil
}
res.Name = t.Name()
res.MTU = t.MTU()
return nil
}
func (t *TunAdapter) SetupAdminHandlers(a *admin.AdminSocket) {
_ = a.AddHandler(
"getTun", "Show information about the node's TUN interface", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetTUNRequest{}
res := &GetTUNResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := t.getTUNHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
}

View File

@@ -1,4 +1,4 @@
package tuntap
package tun
const TUN_OFFSET_BYTES = 4
@@ -17,7 +17,7 @@ func (tun *TunAdapter) read() {
begin := TUN_OFFSET_BYTES
end := begin + n
bs := buf[begin:end]
if _, err := tun.core.Write(bs); err != nil {
if _, err := tun.rwc.Write(bs); err != nil {
tun.log.Debugln("Unable to send packet:", err)
}
}
@@ -27,7 +27,7 @@ func (tun *TunAdapter) write() {
var buf [TUN_OFFSET_BYTES + 65535]byte
for {
bs := buf[TUN_OFFSET_BYTES:]
n, err := tun.core.Read(bs)
n, err := tun.rwc.Read(bs)
if err != nil {
tun.log.Errorln("Exiting tun writer due to core read error:", err)
return

20
src/tun/options.go Normal file
View File

@@ -0,0 +1,20 @@
package tun
func (m *TunAdapter) _applyOption(opt SetupOption) {
switch v := opt.(type) {
case InterfaceName:
m.config.name = v
case InterfaceMTU:
m.config.mtu = v
}
}
type SetupOption interface {
isSetupOption()
}
type InterfaceName string
type InterfaceMTU uint64
func (a InterfaceName) isSetupOption() {}
func (a InterfaceMTU) isSetupOption() {}

View File

@@ -1,10 +1,7 @@
package tuntap
package tun
// This manages the tun driver to send/recv packets to/from applications
// TODO: Crypto-key routing support
// TODO: Set MTU of session properly
// TODO: Reject packets that exceed session MTU with ICMPv6 for PMTU Discovery
// TODO: Connection timeouts (call Conn.Close() when we want to time out)
// TODO: Don't block in reader on writes that are pending searches
@@ -13,16 +10,13 @@ import (
"fmt"
"net"
//"sync"
"github.com/Arceliar/phony"
"github.com/gologme/log"
"golang.zx2c4.com/wireguard/tun"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
)
type MTU uint16
@@ -32,9 +26,8 @@ type MTU uint16
// should pass this object to the yggdrasil.SetRouterAdapter() function before
// calling yggdrasil.Start().
type TunAdapter struct {
core *core.Core
config *config.NodeConfig
log *log.Logger
rwc *ipv6rwc.ReadWriteCloser
log core.Logger
addr address.Address
subnet address.Subnet
mtu uint64
@@ -43,10 +36,14 @@ type TunAdapter struct {
//mutex sync.RWMutex // Protects the below
isOpen bool
isEnabled bool // Used by the writer to drop sessionTraffic if not enabled
config struct {
name InterfaceName
mtu InterfaceMTU
}
}
// Gets the maximum supported MTU for the platform based on the defaults in
// defaults.GetDefaults().
// config.GetDefaults().
func getSupportedMTU(mtu uint64) uint64 {
if mtu < 1280 {
return 1280
@@ -75,71 +72,60 @@ func (tun *TunAdapter) MTU() uint64 {
// DefaultName gets the default TUN interface name for your platform.
func DefaultName() string {
return defaults.GetDefaults().DefaultIfName
return config.GetDefaults().DefaultIfName
}
// DefaultMTU gets the default TUN interface MTU for your platform. This can
// be as high as MaximumMTU(), depending on platform, but is never lower than 1280.
func DefaultMTU() uint64 {
return defaults.GetDefaults().DefaultIfMTU
return config.GetDefaults().DefaultIfMTU
}
// MaximumMTU returns the maximum supported TUN interface MTU for your
// platform. This can be as high as 65535, depending on platform, but is never
// lower than 1280.
func MaximumMTU() uint64 {
return defaults.GetDefaults().MaximumIfMTU
return config.GetDefaults().MaximumIfMTU
}
// Init initialises the TUN module. You must have acquired a Listener from
// the Yggdrasil core before this point and it must not be in use elsewhere.
func (tun *TunAdapter) Init(core *core.Core, config *config.NodeConfig, log *log.Logger, options interface{}) error {
tun.core = core
tun.config = config
tun.log = log
return nil
}
// Start the setup process for the TUN adapter. If successful, starts the
// reader actor to handle packets on that interface.
func (tun *TunAdapter) Start() error {
var err error
phony.Block(tun, func() {
err = tun._start()
})
return err
func New(rwc *ipv6rwc.ReadWriteCloser, log core.Logger, opts ...SetupOption) (*TunAdapter, error) {
tun := &TunAdapter{
rwc: rwc,
log: log,
}
for _, opt := range opts {
tun._applyOption(opt)
}
return tun, tun._start()
}
func (tun *TunAdapter) _start() error {
if tun.isOpen {
return errors.New("TUN module is already started")
}
tun.config.RLock()
defer tun.config.RUnlock()
if tun.config == nil {
return errors.New("no configuration available to TUN")
}
pk := tun.core.PublicKey()
tun.addr = *address.AddrForKey(pk)
tun.subnet = *address.SubnetForKey(pk)
addr := fmt.Sprintf("%s/%d", net.IP(tun.addr[:]).String(), 8*len(address.GetPrefix())-1)
if tun.config.IfName == "none" || tun.config.IfName == "dummy" {
tun.addr = tun.rwc.Address()
tun.subnet = tun.rwc.Subnet()
prefix := address.GetPrefix()
addr := fmt.Sprintf("%s/%d", net.IP(tun.addr[:]).String(), 8*len(prefix[:])-1)
if tun.config.name == "none" || tun.config.name == "dummy" {
tun.log.Debugln("Not starting TUN as ifname is none or dummy")
tun.isEnabled = false
go tun.write()
return nil
}
mtu := tun.config.IfMTU
if tun.core.MaxMTU() < mtu {
mtu = tun.core.MaxMTU()
mtu := uint64(tun.config.mtu)
if tun.rwc.MaxMTU() < mtu {
mtu = tun.rwc.MaxMTU()
}
if err := tun.setup(tun.config.IfName, addr, mtu); err != nil {
if err := tun.setup(string(tun.config.name), addr, mtu); err != nil {
return err
}
if tun.MTU() != mtu {
tun.log.Warnf("Warning: Interface MTU %d automatically adjusted to %d (supported range is 1280-%d)", tun.config.IfMTU, tun.MTU(), MaximumMTU())
tun.log.Warnf("Warning: Interface MTU %d automatically adjusted to %d (supported range is 1280-%d)", tun.config.mtu, tun.MTU(), MaximumMTU())
}
tun.core.SetMTU(tun.MTU())
tun.rwc.SetMTU(tun.MTU())
tun.isOpen = true
tun.isEnabled = true
go tun.read()

View File

@@ -1,6 +1,7 @@
//go:build openbsd || freebsd
// +build openbsd freebsd
package tuntap
package tun
import (
"encoding/binary"

View File

@@ -1,6 +1,7 @@
//go:build !mobile
// +build !mobile
package tuntap
package tun
// The darwin platform specific tun parts

View File

@@ -1,6 +1,7 @@
//go:build !mobile
// +build !mobile
package tuntap
package tun
// The linux platform specific tun parts
@@ -27,7 +28,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
return tun.setupAddress(addr)
}
// Configures the TAP adapter with the correct IPv6 address and MTU. Netlink
// Configures the TUN adapter with the correct IPv6 address and MTU. Netlink
// is used to do this, so there is not a hard requirement on "ip" or "ifconfig"
// to exist on the system, but this will fail if Netlink is not present in the
// kernel (it nearly always is).

View File

@@ -1,6 +1,7 @@
//go:build !linux && !darwin && !windows && !openbsd && !freebsd && !mobile
// +build !linux,!darwin,!windows,!openbsd,!freebsd,!mobile
package tuntap
package tun
// This is to catch unsupported platforms
// If your platform supports tun devices, you could try configuring it manually

View File

@@ -1,6 +1,7 @@
//go:build windows
// +build windows
package tuntap
package tun
import (
"bytes"
@@ -8,7 +9,7 @@ import (
"log"
"net"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"golang.org/x/sys/windows"
wgtun "golang.zx2c4.com/wireguard/tun"
@@ -21,7 +22,7 @@ import (
// Configures the TUN adapter with the correct IPv6 address and MTU.
func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
if ifname == "auto" {
ifname = defaults.GetDefaults().DefaultIfName
ifname = config.GetDefaults().DefaultIfName
}
return elevate.DoAsSystem(func() error {
var err error
@@ -49,7 +50,7 @@ func (tun *TunAdapter) setup(ifname string, addr string, mtu uint64) error {
})
}
// Sets the MTU of the TAP adapter.
// Sets the MTU of the TUN adapter.
func (tun *TunAdapter) setupMTU(mtu uint64) error {
if tun.iface == nil || tun.Name() == "" {
return errors.New("Can't configure MTU as TUN adapter is not present")
@@ -76,7 +77,7 @@ func (tun *TunAdapter) setupMTU(mtu uint64) error {
return nil
}
// Sets the IPv6 address of the TAP adapter.
// Sets the IPv6 address of the TUN adapter.
func (tun *TunAdapter) setupAddress(addr string) error {
if tun.iface == nil || tun.Name() == "" {
return errors.New("Can't configure IPv6 address as TUN adapter is not present")

View File

@@ -1,37 +0,0 @@
package tuntap
import (
"encoding/json"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
)
type GetTUNRequest struct{}
type GetTUNResponse map[string]TUNEntry
type TUNEntry struct {
MTU uint64 `json:"mtu"`
}
func (t *TunAdapter) getTUNHandler(req *GetTUNRequest, res *GetTUNResponse) error {
*res = GetTUNResponse{
t.Name(): TUNEntry{
MTU: t.MTU(),
},
}
return nil
}
func (t *TunAdapter) SetupAdminHandlers(a *admin.AdminSocket) {
_ = a.AddHandler("getTunTap", []string{}, func(in json.RawMessage) (interface{}, error) {
req := &GetTUNRequest{}
res := &GetTUNResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := t.getTUNHandler(req, res); err != nil {
return nil, err
}
return res, nil
})
}

View File

@@ -1,37 +0,0 @@
// Package util contains miscellaneous utilities used by yggdrasil.
// In particular, this includes a crypto worker pool, Cancellation machinery, and a sync.Pool used to reuse []byte.
package util
// These are misc. utility functions that didn't really fit anywhere else
import (
"time"
)
// TimerStop stops a timer and makes sure the channel is drained, returns true if the timer was stopped before firing.
func TimerStop(t *time.Timer) bool {
stopped := t.Stop()
select {
case <-t.C:
default:
}
return stopped
}
// FuncTimeout runs the provided function in a separate goroutine, and returns true if the function finishes executing before the timeout passes, or false if the timeout passes.
// It includes no mechanism to stop the function if the timeout fires, so the user is expected to do so on their own (such as with a Cancellation or a context).
func FuncTimeout(timeout time.Duration, f func()) bool {
success := make(chan struct{})
go func() {
defer close(success)
f()
}()
timer := time.NewTimer(timeout)
defer TimerStop(timer)
select {
case <-success:
return true
case <-timer.C:
return false
}
}