Compare commits

...

364 Commits

Author SHA1 Message Date
Neil Alexander
7c51d9e971 Merge pull request #231 from yggdrasil-network/develop
Version 0.3
2018-12-12 10:02:00 +00:00
Arceliar
f3e5513f62 Merge pull request #243 from Arceliar/doc
minor whitepaper updates for v0.3
2018-12-12 01:55:43 -06:00
Arceliar
871d6119ec minor whitepaper updates for v0.3 2018-12-12 01:47:31 -06:00
Arceliar
6901e2fc9a Merge pull request #238 from neilalexander/afunix
Use AF_UNIX socket by default for admin API
2018-12-10 17:34:36 -06:00
Arceliar
977a0e7215 Merge pull request #239 from neilalexander/getroutes
CKR tweaks
2018-12-10 17:27:58 -06:00
Neil Alexander
90ace46587 Enforce CKR cache size more strongly 2018-12-10 22:30:31 +00:00
Neil Alexander
65e34bbbab Enforce maximum CKR routing cache size 2018-12-10 22:19:08 +00:00
Neil Alexander
f09adc2192 Update getRoutes format 2018-12-10 22:04:37 +00:00
Neil Alexander
3eb1a40c68 Update CHANGELOG.md 2018-12-10 11:34:37 +00:00
Neil Alexander
c78e1b98cc Show yggdrasilctl log buffer on panic 2018-12-10 11:29:42 +00:00
Neil Alexander
b4b3609678 Really use the correct endpoint 2018-12-10 11:12:40 +00:00
Neil Alexander
d29b5a074a Try to use default config file location to find AdminListen when yggdrasilctl is not given an endpoint 2018-12-10 11:09:10 +00:00
Neil Alexander
cd11d2eccd Produce more meaningful logging from yggdrasilctl when things go wrong 2018-12-10 10:54:41 +00:00
Neil Alexander
dff1dca19c Add DefaultConfigFile to defaults for yggdrasilctl 2018-12-10 10:20:59 +00:00
Neil Alexander
2928fcb046 Update CHANGELOG.md 2018-12-10 00:42:56 +00:00
Neil Alexander
8aaaeb26eb Default to /var/run/yggdrasil.sock for admin on Linux, BSDs 2018-12-10 00:37:32 +00:00
Neil Alexander
bbe2f56b74 Default to /var/run/yggdrasil.sock for admin on darwin/macOS 2018-12-10 00:31:31 +00:00
Neil Alexander
74a904d04c Don't os.Chmod if we suspect the socket belongs to an abstract namespace 2018-12-10 00:26:12 +00:00
Neil Alexander
06c6dfc67f Complain if socket file already exists 2018-12-10 00:19:21 +00:00
Neil Alexander
f791df4977 Try to chmod 660 the admin socket if using AF_UNIX 2018-12-10 00:00:23 +00:00
Neil Alexander
14f4da764c Merge pull request #236 from neilalexander/rpmspec
Add starting point for an RPM spec file
2018-12-09 23:36:18 +00:00
Neil Alexander
f6cdb8e38e Update CHANGELOG.md 2018-12-09 23:35:40 +00:00
Neil Alexander
08ad163dfe Add starting point for an RPM spec file 2018-12-09 23:10:12 +00:00
Neil Alexander
1892fdc429 Merge pull request #235 from neilalexander/build
Add -t, -c and -l to build script
2018-12-09 22:36:04 +00:00
Neil Alexander
bec044346e Add -t, -c and -l to build script for specifying DWARF tables, GCFLAGS and LDFLAGS 2018-12-09 22:31:58 +00:00
Neil Alexander
ff49ca6d30 Merge pull request #234 from neilalexander/admin
Allow disabling admin socket
2018-12-09 17:54:37 +00:00
Neil Alexander
6801d713a7 Also don't start if AdminListen is empty 2018-12-09 17:53:31 +00:00
Neil Alexander
5ed197b3ca Update CHANGELOG.md 2018-12-09 17:48:35 +00:00
Neil Alexander
80d087404f Allow disabling admin socket with AdminListen="none" 2018-12-09 17:46:48 +00:00
Arceliar
9724dd2351 Merge pull request #232 from neilalexander/yggdrasilctl
Update yggdrasilctl help text
2018-12-09 10:28:11 -06:00
Arceliar
43993a3460 Merge pull request #233 from neilalexander/build
Allow specifying PKGSRC=, PKGVER= and PKGNAME= to build script
2018-12-09 10:26:49 -06:00
Neil Alexander
0fdc814c4a Allow specifying PKGSRC=, PKGVER= and PKGNAME= to build script 2018-12-09 14:32:24 +00:00
Neil Alexander
e759fb1c38 Update CHANGELOG.md 2018-12-09 13:37:19 +00:00
Neil Alexander
ba4507c02e Update yggdrasilctl help (fixes #194) 2018-12-09 13:33:18 +00:00
Neil Alexander
40e02f01b0 Merge pull request #230 from neilalexander/module
Fix go.mod for neilalexander/hjson-go
2018-12-08 13:22:54 +00:00
Neil Alexander
34949f7c32 Fix go.mod for neilalexander/hjson-go 2018-12-08 13:21:58 +00:00
Neil Alexander
9eace183c6 Merge pull request #229 from neilalexander/semver
Fix circleci/semver bugs
2018-12-08 11:37:51 +00:00
Neil Alexander
3b2044666d Fix bug in semver version.sh 2018-12-08 11:31:20 +00:00
Neil Alexander
22e6505079 Fix bug from #228 2018-12-08 11:28:47 +00:00
Neil Alexander
467002bbf7 Merge pull request #228 from neilalexander/semver
Strip v from version during imprint
2018-12-08 11:01:41 +00:00
Neil Alexander
02f98a2592 Only show build name and version if it is known 2018-12-08 11:01:05 +00:00
Neil Alexander
f2d01aa54d Use bare version in deb/macos packages instead of cut 2018-12-08 10:54:47 +00:00
Neil Alexander
9d0b8ac6f4 Strip v from version during imprint 2018-12-08 10:51:31 +00:00
Neil Alexander
262c93504f Merge pull request #227 from Arceliar/module
convert to go module
2018-12-08 10:34:55 +00:00
Neil Alexander
5315bc25c5 Return 1 instead of -1 from semver/deb 2018-12-08 10:33:33 +00:00
Neil Alexander
2da3ef420c Update documentation, remove stray .DS_Store file 2018-12-08 10:30:43 +00:00
Arceliar
af478e0e45 fix very special case bug when trying to dhtPing the root via the admin api 2018-12-08 00:42:13 -06:00
Arceliar
bd2d706745 fix bug from go vet while I'm at it 2018-12-07 20:36:30 -06:00
Arceliar
2d5f99a008 remove working_directory from circleci config, let it use the default, as per their blog post on go modules in 1.11 2018-12-07 20:19:19 -06:00
Arceliar
586781b49c convert to go module 2018-12-07 19:56:04 -06:00
Arceliar
caa7b739af Merge pull request #226 from neilalexander/imprint
Imprint version number onto builds
2018-12-07 17:37:20 -06:00
Arceliar
ffbdef292f Fix typo in changelog 2018-12-07 17:30:07 -06:00
Neil Alexander
f99c224171 Update CHANGELOG.md 2018-12-07 22:31:37 +00:00
Neil Alexander
5149c6c349 Show build name and version at startup if available 2018-12-07 22:24:01 +00:00
Neil Alexander
3524c6eff6 Add build name and version to getSelf call on admin socket 2018-12-07 22:22:46 +00:00
Neil Alexander
8e784438c7 Imprint build name and version number if available 2018-12-07 22:20:11 +00:00
Neil Alexander
4bc009d845 Update semver behaviour 2018-12-07 22:17:09 +00:00
Neil Alexander
8bd566d4d8 Remove VERSION 2018-12-07 22:05:36 +00:00
Neil Alexander
d0c2ce90bb Fix semver when git history is not present 2018-12-07 22:03:57 +00:00
Neil Alexander
4532d0e0c8 Merge pull request #219 from neilalexander/semver
Tag releases in master using CI
2018-12-07 19:38:01 +00:00
Neil Alexander
5106c217c7 Merge pull request #225 from Arceliar/switch
switch bugfixes
2018-12-06 07:09:49 +00:00
Arceliar
fe772dd38e switch bugfixes 2018-12-05 18:22:39 -06:00
Arceliar
dd6a1dfccc Merge pull request #224 from neilalexander/changelog
Update changelog
2018-12-05 18:17:26 -06:00
Arceliar
09228554cb Merge pull request #223 from neilalexander/reusemulticast
Try to SO_REUSEPORT on multicast socket
2018-12-05 18:15:21 -06:00
Neil Alexander
2eedcce3fd Update changelog 2018-12-05 23:39:28 +00:00
Neil Alexander
ae48a1721e Try to SO_REUSEADDR on Windows 2018-12-05 23:10:50 +00:00
Neil Alexander
eae8f9a666 Try to SO_REUSEPORT on UNIX platforms 2018-12-05 22:39:04 +00:00
Arceliar
cc066d7aeb Merge pull request #221 from jcgruenhage/reduce-container-image-size
Reduce container image size
2018-12-04 18:54:58 -06:00
Jan Christian Grünhage
9f4fc3669b Reduce container image size 2018-12-04 13:04:42 +01:00
Neil Alexander
58b60af208 Merge pull request #220 from Arceliar/switch
More latency-based switch optimizations
2018-12-04 09:11:55 +00:00
Arceliar
3d4b49b693 reset the switch speed info for a peer whenever it changes coords, instead of only if they're a parent and change coords. Also, make sure packets in the sim preserve order when sending, to avoid races when testing 2018-12-03 19:21:23 -06:00
Arceliar
9e17e41b79 Merge pull request #218 from cwinfo/develop
Docker Support & Updated License
2018-12-03 18:06:46 -06:00
Neil Alexander
8a04cbe3c8 Try to fix CircleCI shell error 2018-12-03 17:49:03 +00:00
Neil Alexander
a7f5c427d4 Tag releases in master using CI (also checks for v0.x.0 instead of v0.x when deciding version numbers) 2018-12-03 17:44:26 +00:00
Christer Warén
6170f7268f Rename LICENSE.md to LICENSE 2018-12-03 06:46:05 +02:00
Christer Warén
ecc0cd4992 Update and rename LICENSE to LICENSE.md 2018-12-03 06:39:28 +02:00
Christer Warén
4fc0117e08 Creating Dockerfile to /
Hint for support of docker, same in cjdns repository
2018-12-03 06:06:58 +02:00
Christer Warén
80b876d21d Creating entrypoint.sh to /contrib/docker/ 2018-12-03 05:58:24 +02:00
Christer Warén
8b7b3452cf Creating Dockerfile to /contrib/docker/
- Multiple architectures supported by using Golang's official Debian Stretch image.
- Upgrading os to latest updates
- Adding all files to image
- Creating user for yggdrasil (kinda unused)
- Building from source code
2018-12-03 05:57:00 +02:00
Neil Alexander
8ade7aed62 Merge pull request #217 from neilalexander/json
Add -json flag for -genconf and -normaliseconf
2018-12-02 23:53:29 +00:00
Neil Alexander
150cf810dd Update comments for -useconf and -useconffile 2018-12-02 23:52:57 +00:00
Neil Alexander
ad30e36881 Add -json flag for -genconf and -normaliseconf 2018-12-02 23:49:48 +00:00
Arceliar
684632eb3d Merge pull request #215 from Arceliar/switch
Latency-based parent selection for the switch
2018-12-02 17:26:26 -06:00
Neil Alexander
b7ccdaf423 Merge pull request #216 from neilalexander/switchoptions
Add SwitchOptions and MaxTotalQueueSize
2018-12-02 23:25:44 +00:00
Neil Alexander
5a89a869be Set queueTotalMaxSize before switch worker starts 2018-12-02 23:24:54 +00:00
Neil Alexander
b5f4637b5c Enforce min 4MB switch queue total size 2018-12-02 23:20:11 +00:00
Neil Alexander
319457ae27 Update comment for MaxTotalQueueSize 2018-12-02 23:03:10 +00:00
Neil Alexander
86da073226 Add SwitchOptions and MaxTotalQueueSize 2018-12-02 22:49:27 +00:00
Arceliar
dcfe55dae8 store 'faster' relationships between all pairs of peers, to make fallback easier when a parent goes offline 2018-12-02 16:36:25 -06:00
Arceliar
38093219fd dimensionless way to track how often nodes are faster than the current parent 2018-12-02 14:46:58 -06:00
Arceliar
05b07adba2 Merge pull request #213 from neilalexander/admincleanup
Admin socket clean-up
2018-11-26 19:18:24 -06:00
Arceliar
b3e2b8e6a5 Update admin.go
Replace `nil` with `[]string{}` for `list`'s argument list.
2018-11-26 19:15:27 -06:00
Neil Alexander
5912dcc72c Fix typo 2018-11-26 18:34:17 +00:00
Neil Alexander
099fee9cae Rename destPubKey to box_pub_key in addRoute etc 2018-11-26 17:58:54 +00:00
Neil Alexander
498d664f51 Add -v for verbose output from yggdrasilctl 2018-11-26 17:55:34 +00:00
Neil Alexander
315aadae06 Rename help to list 2018-11-26 17:51:30 +00:00
Neil Alexander
a6be4bacbc Don't show box_pub_key in tables 2018-11-26 17:50:31 +00:00
Neil Alexander
8239989c36 Send box_pub_key with getSessions, getDHT, getSwitchPeers and getPeers 2018-11-26 17:38:02 +00:00
Neil Alexander
5b10af7399 Rename key to box_pub_key in admin socket for consistency 2018-11-26 17:34:26 +00:00
Neil Alexander
bd9055ddd7 Merge pull request #212 from Arceliar/admin
Add dhtPing to the admin interface
2018-11-26 17:28:33 +00:00
Arceliar
d8d1e63c36 fix infinite loop from interaction between dht.isImportant and dht.insert 2018-11-25 20:33:33 -06:00
Arceliar
0ec6207e05 better response format and yggdrasilctl printing 2018-11-25 18:25:31 -06:00
Arceliar
a34ca40594 use a buffered channel to avoid races, and run gofmt 2018-11-25 17:59:36 -06:00
Arceliar
d253bb750c yggdrasilctl support 2018-11-25 17:50:56 -06:00
Arceliar
7954fa3c33 store one callback instead of many, needed to prevent search failures if there are multiple outstanding packets 2018-11-25 17:08:45 -06:00
Arceliar
9937a6102e add callbacks to maintenance map cleanup 2018-11-25 16:29:47 -06:00
Arceliar
12e635f946 adjust dhtPing response so 'nodes' defaults to an empty list instead of null 2018-11-25 16:16:06 -06:00
Arceliar
d520a8a1d5 refactor dht code to call arbitrary callbacks instead of only searches.checkDHTRes, and add admin API fuction to dhtPing a node (with an optional target NodeID) 2018-11-25 16:10:32 -06:00
Neil Alexander
9f16d0ed1f Merge pull request #211 from Arceliar/memleaks
Memleaks
2018-11-25 19:27:45 +00:00
Arceliar
e17efb6e91 don't penalize dht timeouts a second time 2018-11-25 13:21:13 -06:00
Arceliar
9046dbde4f remove sigManager, it seems safer to just burn the CPU than to store a map of strings of potentially arbitrary length 2018-11-25 13:06:54 -06:00
Arceliar
4e156bd4f7 better cleanup of maps 2018-11-25 12:25:38 -06:00
Arceliar
8d6beebac4 clean up old requests during dht maintenance 2018-11-24 20:04:14 -06:00
Neil Alexander
5a7c2b250c Merge pull request #209 from Arceliar/switch-tune
Adjust switch parent selection behavior
2018-11-24 23:21:45 +00:00
Neil Alexander
3efc9bfa22 Merge pull request #210 from Arceliar/admin
Admin functions for cryptokey routing
2018-11-24 23:21:08 +00:00
Arceliar
6d0e40045a cleanup/fixes from go vet 2018-11-22 21:41:16 -06:00
Arceliar
12cc7fc639 add yggdrasilctl support for getSourceSubnets and getRoutes 2018-11-22 21:37:57 -06:00
Arceliar
4870a2e149 removeSourceSubnet and removeRoute via the admin api 2018-11-22 21:30:56 -06:00
Arceliar
5953027411 switch from []byte to boxPubKey in ckr code, and start adding admin functions for existing code (yggdrasilctl.go still needs pretty printing support for the responses to the new get functions) 2018-11-21 00:10:20 -06:00
Arceliar
5fa23b1e38 move router.recvPacket calls into the main router goroutine, to make the ckr checks threadsafe 2018-11-20 22:04:18 -06:00
Arceliar
e9cff0506c comment the switch a little better and limit how much uptime can affect which peer is used as a parent 2018-11-19 21:30:52 -06:00
Arceliar
ae4107a3b2 Merge pull request #207 from Arceliar/chord
Tune DHT a little better
2018-11-16 20:35:11 -06:00
Arceliar
ef6cece720 fix sim and tune dht to bootstrap a little faster 2018-11-16 19:32:12 -06:00
Arceliar
289f1ce7c2 set packet version in sim, so it plays nice with new parsing from the new ckr code 2018-11-14 21:58:48 -06:00
Neil Alexander
fc5a5830aa Merge pull request #203 from Arceliar/chord
Chord
2018-11-14 20:13:42 +00:00
Arceliar
be3a7b3e68 Merge pull request #206 from Arceliar/ckr
fix bug in recvPacket for packets coming from a subnet
2018-11-11 00:04:47 -06:00
Arceliar
8cf8b0ec41 fix bug in recvPacket for packets coming from a subnet 2018-11-11 00:00:47 -06:00
Arceliar
3b8cd0a8d6 Merge pull request #205 from Arceliar/tunfix
fix crash when starting in tun mode
2018-11-10 22:44:02 -06:00
Arceliar
1b1b776097 fix crash when starting in tun mode 2018-11-10 22:39:15 -06:00
Neil Alexander
b3887e554c Merge pull request #204 from neilalexander/tapmac
Neighbor discovery changes for TAP mode
2018-11-10 19:41:50 +00:00
Neil Alexander
6fab0e9507 Fix CKR (IPv4/IPv6) in TAP mode so frames sent to node MAC, base MAC/LL from node IPv6 address 2018-11-10 18:33:52 +00:00
Neil Alexander
adc32fe92f Track further neighbor state, don't send more NDPs than needed 2018-11-10 17:32:03 +00:00
Neil Alexander
d50e1bc803 More complete NDP implementation for TAP mode, which tracks individual MAC addresses for neighbors 2018-11-10 15:46:10 +00:00
Arceliar
15d5b3f82c comments and minor cleanup 2018-11-09 23:02:38 -06:00
Arceliar
7af85c7d70 Merge pull request #201 from neilalexander/ckr
Tunnel traffic using crypto-key routing
2018-11-09 18:37:39 -06:00
Neil Alexander
685b565512 Check IP header lengths correctly per protocol 2018-11-07 10:29:08 +00:00
Neil Alexander
9542bfa902 Check the session perm pub key against the CKR key 2018-11-07 10:16:46 +00:00
Neil Alexander
fbfae473d4 Use full node ID for CKR routes instead of truncated node IDs from the address/subnet 2018-11-07 10:04:31 +00:00
Neil Alexander
39dab53ac7 Update comments in configuration and some godoc descriptions 2018-11-06 22:57:53 +00:00
Neil Alexander
a3a53f92c3 Reinstate length/bounds check in tun.go 2018-11-06 22:35:28 +00:00
Neil Alexander
0240375417 IPv4 CKR support in router 2018-11-06 20:49:19 +00:00
Neil Alexander
424faa1c51 Support IPv4 in ckr.go 2018-11-06 20:04:49 +00:00
Neil Alexander
cb7a5f17d9 Check destination address upon receive in router 2018-11-06 19:23:20 +00:00
Neil Alexander
2f75075da3 Fix Yggdrasil subnet routing 2018-11-06 14:28:57 +00:00
Neil Alexander
bc62af7f7d Enable CKR properly from config 2018-11-06 12:32:16 +00:00
Neil Alexander
bc578f571c Some output at startup 2018-11-06 11:56:32 +00:00
Neil Alexander
f0947223bb Only validate CKR routes if CKR enabled 2018-11-06 11:11:57 +00:00
Neil Alexander
19e6aaf9f5 Remove sourceSubnet from router 2018-11-06 00:06:37 +00:00
Neil Alexander
e3d4aed44a Configure IPv6Sources 2018-11-06 00:05:01 +00:00
Neil Alexander
8c2327a2bf Add source addresses option and more intelligent source checking 2018-11-05 23:59:41 +00:00
Neil Alexander
cfdbc481a5 Modify source address check for CKR 2018-11-05 23:22:45 +00:00
Neil Alexander
7218b5a56c Don't look up public keys for Yggdrasil native addresses 2018-11-05 23:12:26 +00:00
Neil Alexander
c7f2427de1 Check CKR routes when receiving packets in router 2018-11-05 22:58:58 +00:00
Neil Alexander
87b0f5fe24 Use CKR in router when sending packets 2018-11-05 22:39:30 +00:00
Neil Alexander
295e9c9a10 Cache crypto-key routes (until routing table changes) 2018-11-05 17:31:10 +00:00
Neil Alexander
ec751e8cc7 Don't allow Yggdrasil ranges as crypto-key routes 2018-11-05 17:03:58 +00:00
Neil Alexander
52206dc381 Add initial crypto-key routing handlers 2018-11-05 16:40:47 +00:00
Arceliar
a008b42f99 cleanup and some bugfixes, cache important dht nodes until something gets added/removed 2018-10-29 22:24:18 -05:00
Arceliar
671c7f2a47 don't update recv time for known nodes that ping us or known peers 2018-10-28 15:04:44 -05:00
Arceliar
c0531627bc fix some chord dht bootstrapping bugs, no known cases where it now fails 2018-10-24 22:03:27 -05:00
Neil Alexander
f088a244da Merge pull request #198 from neilalexander/endpoints
Show real endpoints in getPeers etc
2018-10-24 18:28:51 +01:00
Arceliar
253861ebd3 reverse dht ownership order from predecessor to successor, this plays nicer with the default 0 bits in unknown node IDs 2018-10-21 18:15:04 -05:00
Arceliar
5e3959f1d0 yet more debugging 2018-10-21 17:40:43 -05:00
Neil Alexander
aab0502a4a Remove friendlyname traces, preserve endpoints 2018-10-21 23:20:14 +01:00
Arceliar
f0bd40ff68 more testing 2018-10-21 15:10:18 -05:00
Arceliar
bcbd24120d keep track of all keys we're supposed to care about in the dht, don't give special treatment to successors/predecessors 2018-10-21 14:57:04 -05:00
Arceliar
efe6cec11a more debugging, trying to understand bootstrap issues 2018-10-21 12:28:21 -05:00
Neil Alexander
b809adf981 Add FriendlyName option, show friendly name and real endpoint in admin socket/yggdrasilctl 2018-10-21 17:57:48 +01:00
Arceliar
6c59ae862a more debugging 2018-10-21 00:05:04 -05:00
Arceliar
95201669fe reintroduce (better) dht throttling 2018-10-20 22:06:36 -05:00
Arceliar
8825494d59 remove maintenance searches and throttle logic, to focus on debugging in this simpler case first 2018-10-20 20:11:32 -05:00
Arceliar
3dbffae99f add search for successor, via parent, to the dht maintenance cycle 2018-10-20 19:09:25 -05:00
Arceliar
d851d9afe7 add max pings before timing out a successor 2018-10-20 18:31:11 -05:00
Arceliar
63d6ab4251 more cleanup, comments, and dht reset() changes 2018-10-20 18:12:34 -05:00
Arceliar
f3ec8c5b37 fix admin dht function, more cleanup, and slowly throttle back dht traffic when idle 2018-10-20 17:58:54 -05:00
Arceliar
5a85d3515d cleanup 2018-10-20 17:32:54 -05:00
Arceliar
02f0611dde more debugging 2018-10-20 16:27:01 -05:00
Arceliar
1720dff476 add some debug output and get things to start working in the sim 2018-10-20 15:21:40 -05:00
Arceliar
03a88fe304 Try using a chord-like DHT instead of a kad-like one, work in progress, but it compiles at least 2018-10-20 14:48:07 -05:00
Neil Alexander
a9f72a6ee1 Merge pull request #195 from neilalexander/macospkg
Add create-pkg.sh for creating macOS installers
2018-10-17 13:14:03 +01:00
Neil Alexander
9f129bc7b0 Backup and normalise config if needed 2018-10-17 12:48:54 +01:00
Neil Alexander
8844dedb8a Add create-pkg.sh for creating macOS installers 2018-10-17 11:55:01 +01:00
Arceliar
b087e955fb Merge pull request #192 from yggdrasil-network/develop
Version 0.2.7
2018-10-13 13:41:43 -05:00
Arceliar
bb975d2edd Merge pull request #191 from neilalexander/changelog
Update changelog for v0.2.7
2018-10-13 13:39:04 -05:00
Neil Alexander
fde5b18be4 Update changelog for v0.2.7 2018-10-13 19:37:07 +01:00
Neil Alexander
18428b0f93 Merge pull request #190 from neilalexander/sessionfirewall
Fix incorrect comment in config for SessionFirewall
2018-10-09 07:24:22 +01:00
Neil Alexander
eb42fd4973 Fix incorrect comment in config for SessionFirewall 2018-10-09 07:18:43 +01:00
Arceliar
ba8af20817 Merge pull request #187 from neilalexander/sessionfirewall
Add session firewall
2018-10-08 18:41:15 -05:00
Neil Alexander
1233371962 Merge pull request #189 from neilalexander/threadsafe
Fix a concurrent map read-write when removing peers
2018-10-08 23:00:15 +01:00
Neil Alexander
1d00131416 Hopefully fix a concurrent map read-write when removing peers 2018-10-08 22:09:55 +01:00
Neil Alexander
1e6667567a Update comments for session firewall in config 2018-10-08 19:57:14 +01:00
Neil Alexander
3ed63ede1e Add AlwaysAllowOutbound to session firewall 2018-10-08 19:51:51 +01:00
Neil Alexander
3f237372c9 Only apply session firewall to pings for sessions we don't already have 2018-10-08 19:05:50 +01:00
Neil Alexander
2e2c58bfef Add session firewall (extra security for controlling traffic flow to/from a given node) 2018-10-07 17:13:41 +01:00
Neil Alexander
401960e17e Merge pull request #183 from neilalexander/getswitchqueues
Add getSwitchQueues
2018-10-07 10:31:46 +01:00
Neil Alexander
85e8968a4d Merge pull request #186 from neilalexander/freebsd-service
Add freebsd service script into contrib
2018-10-06 00:26:11 +01:00
Neil Alexander
980f18b266 Add freebsd service script into contrib 2018-10-06 00:24:07 +01:00
Neil Alexander
81eea137d4 Merge branch 'develop' into getswitchqueues 2018-10-05 19:51:45 +01:00
Neil Alexander
ffa8580d30 Merge pull request #185 from neilalexander/debug
Fix debug builds
2018-10-05 19:45:58 +01:00
Neil Alexander
501dc2bb3d Test debug builds in CircleCI (also use Go 1.11 for CI builds) 2018-10-05 19:42:21 +01:00
Neil Alexander
605b6829db Fix debug builds (broken by #182) 2018-10-05 19:28:46 +01:00
Neil Alexander
8eed15b813 Fix merge conflict in tcp.go from d027a9ba75 2018-10-04 12:26:08 +01:00
Neil Alexander
b6ff6e96cd Merge pull request #184 from yggdrasil-network/source-interface
Ignore peer in InterfacePeers when source interface not found
2018-09-28 15:16:34 +01:00
Neil Alexander
d027a9ba75 Don't peer when source interface not found 2018-09-28 14:59:10 +01:00
Neil Alexander
69cd736112 Show queue capacity in getSwitchQueues 2018-09-27 16:19:47 +01:00
Neil Alexander
7d8a1859f0 Aggregate queue info by switchport 2018-09-27 15:51:17 +01:00
Neil Alexander
0b1a6611fd Identify switchport for queue based on coords in stream ID 2018-09-27 15:05:45 +01:00
Neil Alexander
8113b4cc22 Update comments 2018-09-27 14:20:52 +01:00
Neil Alexander
95c551d011 Fix showing active queues in yggdrasilctl 2018-09-27 12:55:41 +01:00
Neil Alexander
b530916044 Show information about individual active queues 2018-09-27 12:14:55 +01:00
Neil Alexander
2674e1cb8b Merge remote-tracking branch 'origin/develop' into getswitchqueues 2018-09-27 11:03:59 +01:00
Neil Alexander
f57567ea56 Add getSwitchQueues 2018-09-27 10:53:19 +01:00
Arceliar
7e3426ba93 Merge pull request #182 from yggdrasil-network/source-interface
Support adding peers on specific interfaces
2018-09-26 07:48:55 -05:00
Neil Alexander
b7f2f8b55c Ignore interfaces that are not up 2018-09-25 19:46:06 +01:00
Neil Alexander
6844b9df51 Update comments in default config 2018-09-25 18:17:00 +01:00
Neil Alexander
387ae9ea6c Only replace call name with interface prefix when interface is set 2018-09-25 18:05:57 +01:00
Neil Alexander
3f8a4ab17d Add bytes_sent and bytes_recvd to getSwitchPorts 2018-09-25 17:59:01 +01:00
Neil Alexander
b368421dbd Fix addPeer to make interface= optional 2018-09-25 17:13:35 +01:00
Neil Alexander
1796000b05 Change to InterfacePeers instead of modifying the tcp:// URI format 2018-09-25 16:55:57 +01:00
Neil Alexander
aecc151baf Add support for specifying TCP source interface, i.e. tcp://a.b.c.d:e/eth0, for multiple simultaneous peerings to the same node over different interfaces 2018-09-25 15:32:45 +01:00
Neil Alexander
81ca5d8ede Merge pull request #181 from Arceliar/admin-dot
Fix bug in admin dot graph
2018-09-05 09:40:15 +01:00
Arceliar
25661ebcad fix 0 port number on links to unknown nodes in the admin dot graph 2018-09-04 19:30:07 -05:00
Neil Alexander
f2345a9a63 Merge pull request #178 from Arceliar/whitepaper
Whitepaper
2018-09-01 00:05:36 +01:00
Arceliar
e833cdfb98 fix debug builds and update whitepaper with old info from the .io site's about page 2018-08-18 13:24:02 -05:00
Neil Alexander
4666b8f6cd Merge pull request #175 from yggdrasil-network/develop
Version 0.2.6
2018-07-31 10:29:30 +01:00
Neil Alexander
ff83527ac7 Merge pull request #177 from neilalexander/changelog
Update changelog for 0.2.6
2018-07-31 10:10:48 +01:00
Neil Alexander
c6dbc307ae Update changelog for 0.2.6 2018-07-31 10:04:22 +01:00
Arceliar
514de5434f Merge pull request #176 from cathugger/develop
Simpler flowlabel parsing; avoid using 0 flowlabel.
2018-07-30 18:25:31 -05:00
cathugger
b4db89ea9d Avoid unnecessarily allocating coords slice if it's unchanged. 2018-07-30 13:44:46 +00:00
cathugger
67b8a7a53d Ensure no memory allocations happen at hot path 2018-07-30 12:43:34 +00:00
Neil Alexander
c4e6894d6a Copy sinfo.coords for safety 2018-07-30 13:34:32 +01:00
Neil Alexander
ebb4ec7c33 Clean up the flow a bit (partly because I am allergic to huge compounded if statements) 2018-07-30 11:46:44 +01:00
cathugger
68a482ed92 Simplify flowkey stuff further. 2018-07-30 02:15:57 +00:00
cathugger
36dcab9300 optimize wire_put_uint64; use protokey for flowlabel fallback. 2018-07-30 01:58:52 +00:00
cathugger
fec7100898 Clean up / clarify coords sending code. 2018-07-30 00:01:37 +00:00
cathugger
11b0a82c4a Simpler flowlabel parsing; avoid using 0 flowlabel. 2018-07-29 22:09:16 +00:00
Arceliar
ddab8ecf33 Merge pull request #174 from cathugger/develop
Make TCP read timeouts configurable.
2018-07-29 10:39:43 -05:00
cathugger
d171552577 Make TCP read timeouts configurable.
This should be helpful on high-latency networks, like Tor or I2P.
Also gofmt.
2018-07-29 14:30:13 +00:00
Arceliar
e00ed4c95d Merge pull request #171 from neilalexander/backpressure
Use flow label instead of TCP/UDP/SCTP source/destination
2018-07-22 12:40:31 -05:00
Neil Alexander
38e8b036d2 Use addUint64 instead to not interfere with coordinate parsing 2018-07-22 18:33:53 +01:00
Neil Alexander
81fde1a805 Use flow label instead of TCP/UDP/SCTP source/destination ports 2018-07-22 18:16:03 +01:00
Arceliar
98f1dd1624 Merge pull request #170 from neilalexander/backpressure
Only split queues based on port number for TCP/UDP/SCTP
2018-07-22 10:43:26 -05:00
Neil Alexander
3f4295f8cd Only split queues based on port number for TCP/UDP/SCTP, rely only on protocol number for other protos to prevent issues with IPIP, GRE, etc 2018-07-22 12:00:40 +01:00
Neil Alexander
f53699367b Merge pull request #169 from Arceliar/backpressure
StreamID changes
2018-07-22 10:14:27 +01:00
Arceliar
9cbcaf39ac Use coords for queue stream IDs in the switch, and append protocol/port information to coords when sending, to designate different streams 2018-07-21 18:59:29 -05:00
Neil Alexander
388683e3f2 Merge pull request #168 from Arceliar/dotlinksort
Sort dotgraph links by integer value
2018-07-21 09:57:27 +01:00
Arceliar
996a593fa2 Sort dotgraph links by integer value 2018-07-20 23:02:25 -05:00
Neil Alexander
ab73e3cb90 Merge pull request #167 from neilalexander/arm64
Create arm64 builds for Linux
2018-07-20 10:09:30 +01:00
Neil Alexander
438fcdfc5f Build for arm64 2018-07-20 10:04:04 +01:00
Arceliar
dc0c3f9f8b Merge pull request #164 from yggdrasil-network/develop
Version 0.2.5
2018-07-19 18:47:40 -05:00
Arceliar
6d1e705684 Merge pull request #166 from yggdrasil-network/changelog-0.2.5
Update CHANGELOG.md for v0.2.5
2018-07-19 18:43:11 -05:00
Arceliar
2b7b32ff3a Update CHANGELOG.md 2018-07-19 18:38:48 -05:00
Arceliar
549d6f9dd2 Merge pull request #163 from neilalexander/tapmtu
Cap MTU on Linux in TAP mode
2018-07-19 18:25:03 -05:00
Arceliar
9ff08c1b34 Merge pull request #165 from cathugger/master
rearrange tcp reading loop
2018-07-19 18:18:51 -05:00
cathugger
91a374d698 rearrange tcp reading loop
according to documentation of io.Reader interface,
"Callers should always process the n > 0 bytes returned before considering the error err. Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF behaviors."
2018-07-19 21:58:53 +00:00
Neil Alexander
55b56e8686 Normalise startup output for TUN/TAP on Linux and Windows 2018-07-19 10:15:26 +01:00
Neil Alexander
df9cadd938 Cap MTU on Linux in TAP mode 2018-07-19 10:01:12 +01:00
Arceliar
1baafdd17d Merge pull request #162 from cathugger/master
More verbose disconnect messages
2018-07-18 22:55:45 -05:00
cathugger
f4bb2aaaeb More verbose disconnect messages 2018-07-19 01:03:24 +00:00
Neil Alexander
d12e321584 Merge pull request #161 from neilalexander/casesensitive
Make yggdrasilctl less case-sensitive
2018-07-09 19:40:03 +01:00
Neil Alexander
cff7ef026f Make yggdrasilctl less case-sensitive 2018-07-09 19:30:41 +01:00
Arceliar
5cff8428c3 Merge pull request #160 from neilalexander/fixdebug
Fix debug builds after changes in #155
2018-07-09 13:20:10 -05:00
Neil Alexander
f21cbaef9c Fix debug builds after changes in #155 2018-07-09 09:38:48 +01:00
Neil Alexander
059fe24526 Merge pull request #156 from yggdrasil-network/develop
Version 0.2.4
2018-07-08 18:46:48 +01:00
Neil Alexander
8bcb761cef Merge pull request #159 from yggdrasil-network/changelog
Update CHANGELOG.md
2018-07-08 11:13:36 +01:00
Neil Alexander
69cf64dce5 Update CHANGELOG.md 2018-07-08 11:09:49 +01:00
Neil Alexander
0d9a6d7a49 Merge pull request #158 from neilalexander/unixsockets
Fallback to TCP when parsing AdminListen
2018-07-08 10:43:40 +01:00
Neil Alexander
d59bdfeb99 Fail to TCP when parsing AdminListen 2018-07-08 10:37:20 +01:00
Arceliar
70e755fdd3 Merge pull request #157 from Arceliar/develop
Update admin socket error message and run gofmt
2018-07-07 19:43:53 -05:00
Arceliar
bf90447cc4 update admin socket error message and run gofmt 2018-07-07 19:37:36 -05:00
Arceliar
face270298 Merge pull request #154 from neilalexander/unixsockets
Add support for UNIX domain admin sockets
2018-07-07 14:28:40 -05:00
Neil Alexander
b24c7ffa6b Resolve merge conflict with platformdefaults 2018-07-07 20:08:23 +01:00
Neil Alexander
0e9a9f97ba Merge pull request #155 from neilalexander/platformdefaults
Centralise platform defaults
2018-07-07 20:05:20 +01:00
Neil Alexander
a5af69df8a Use Close() for admin socket 2018-07-07 20:04:11 +01:00
Neil Alexander
2a931df07a Try to clean up UNIX domain admin sockets if shutdown properly 2018-07-07 12:34:10 +01:00
Neil Alexander
14d48597da Fix openbsd and netbsd 2018-07-07 12:18:03 +01:00
Neil Alexander
36c89da848 Stick with tcp://localhost:9001 as default for now 2018-07-07 12:16:55 +01:00
Neil Alexander
1692bd98fd Centralise platform defaults into the 'defaults' package 2018-07-07 12:08:52 +01:00
Neil Alexander
171e1e7823 Update default AdminListen to URI format 2018-07-07 11:28:50 +01:00
Neil Alexander
047b7d95a1 Add support for UNIX domain admin sockets to yggdrasilctl 2018-07-07 11:25:01 +01:00
Neil Alexander
cd6030ec8f Add support for UNIX domain admin sockets and specifying URIs in AdminListen 2018-07-07 11:22:49 +01:00
Arceliar
adc21baa28 Merge pull request #152 from Arceliar/backpressure
Limit backpressure resource consumption
2018-07-06 17:38:28 -05:00
Arceliar
ba4047b51a correctly update buffer sizs when buffers overflow, and returned freed packets to the byte store 2018-07-06 17:27:04 -05:00
Arceliar
ad5dc9ea87 Drop 1 packet instead of a whole queue when overflowing 2018-07-06 00:55:00 -05:00
Arceliar
e6a47f705d when dropping a queue, select one at random based on queue size in bytes 2018-07-06 00:11:36 -05:00
Arceliar
1a65c065d0 prioritize sending from small queues that have been blocked for a long time 2018-07-05 23:56:37 -05:00
Arceliar
7da4967f5e Limit maximum queue size to 4 MB 2018-07-05 23:39:41 -05:00
Arceliar
a7c8be4d69 base backpressure decisions on queue size in bytes, instead of packet counts 2018-07-05 23:07:01 -05:00
Neil Alexander
885ba4452d Merge pull request #149 from yggdrasil-network/develop
Version 0.2.3
2018-06-29 23:28:50 +01:00
Neil Alexander
d0e6a9ad41 Merge pull request #150 from neilalexander/version
Update changelog version for v0.2.3
2018-06-29 23:25:00 +01:00
Neil Alexander
af99cebf11 Update changelog version 2018-06-29 23:20:58 +01:00
Neil Alexander
1d05e511b3 Merge pull request #148 from Arceliar/changelog
Add changelog
2018-06-29 08:40:13 +01:00
Neil Alexander
1fced2bdf0 Update changelog 2018-06-29 08:35:39 +01:00
Arceliar
dd6ca6e4b6 Add changelog 2018-06-28 18:47:10 -05:00
Neil Alexander
1a0771b016 Merge pull request #146 from Arceliar/backpressure
Local backpressure improvements
2018-06-28 23:04:03 +01:00
Arceliar
b63b534fa7 drop packets that have been queued for longer than some timeout (currently 25ms) instead of using fixed length queues 2018-06-25 18:12:18 -05:00
Neil Alexander
01f0ec34f4 Merge pull request #147 from neilalexander/alien
Build RPMs in CircleCI using alien
2018-06-25 14:25:48 +01:00
Neil Alexander
3d0b39f05a Keep version number 2018-06-25 14:21:31 +01:00
Neil Alexander
a7d1f21271 Run alien as root so package permissions are right 2018-06-25 14:19:22 +01:00
Neil Alexander
11acb0129d Use alien to generate RPMs 2018-06-25 14:17:07 +01:00
Arceliar
7695a3fcbf try using a simpler FIFO order for each backpressure buffer, since there are other mechanisms to penalize the flooding node, leads to better TCP throughput without affecting traffic between other nodes (does affect traffic in the same session, but there's hypothetically workarounds to that) 2018-06-24 20:20:07 -05:00
Arceliar
4ad2446557 cleanup 2018-06-24 18:21:00 -05:00
Arceliar
03949dcf3f fix my terrible bug, I have no idea why the old one even worked 2018-06-24 18:05:00 -05:00
Arceliar
9c028e1d0d switch to a separate queue per stream of traffic, FIXME for some reason this makes distance calculations more expensive in handleIdle? 2018-06-24 17:39:43 -05:00
Arceliar
189628b381 cleanup 2018-06-23 23:55:27 -05:00
Arceliar
0ad801bcfe more work on backpressure, but still needs more testing 2018-06-23 23:33:03 -05:00
Arceliar
4b83efa218 more backpressure work, still needs testing 2018-06-23 21:51:32 -05:00
Arceliar
52a0027aea switch refactoring, setup for a better approximation of local backpressure 2018-06-23 20:59:26 -05:00
Arceliar
988f4ad265 add a dedicated switch worker and start using it for lookups 2018-06-23 19:08:32 -05:00
Arceliar
2ae213c255 I'll try sorting, that's a good trick 2018-06-23 01:10:18 -05:00
Arceliar
cceecf4b1a larger out queue size, make sure linkOut packets always get sent first 2018-06-22 23:46:42 -05:00
Arceliar
0021f3463f slightly better way for the tcp sender goroutine(s) to block waiting for work 2018-06-22 20:39:57 -05:00
Arceliar
fd074a4364 Merge pull request #143 from neilalexander/admin
Change box_pub_key to key in admin API
2018-06-22 16:35:48 -05:00
Neil Alexander
f68f779bee Change box_pub_key to key in admin API 2018-06-22 22:26:17 +01:00
Neil Alexander
79a35caf24 Merge pull request #142 from Arceliar/sessioncleanup
Sesson cleanup
2018-06-22 08:48:22 +01:00
Arceliar
5dfa01a0e8 periodically clean up timed-out sessions and old signatures, instead of trying to do it when creating new sessions or adding new signatures 2018-06-21 20:31:30 -05:00
Arceliar
e2d739f646 Merge pull request #141 from yggdrasil-network/develop
Version 0.2.2
2018-06-21 12:04:01 -05:00
Arceliar
8e7edf566c Merge pull request #140 from Arceliar/misc
Misc cleanup
2018-06-21 10:50:18 -05:00
Arceliar
254be42614 gofmt 2018-06-21 10:39:43 -05:00
Arceliar
19014a198e randomize the delay after tcp disconnects, to prevent synchronization issues 2018-06-21 10:38:31 -05:00
Arceliar
f599a1a2c1 start the pprof based on an env variable in debug builds, otherwise remove the flag/release.go stuff 2018-06-21 10:32:16 -05:00
Neil Alexander
7a19507665 Merge pull request #139 from neilalexander/yggdrasilconf
Add yggdrasilconf for testing with vyatta-yggdrasil
2018-06-21 16:07:25 +01:00
Neil Alexander
1a60e89ada Add yggdrasilconf for testing with vyatta-yggdrasil 2018-06-21 16:03:46 +01:00
Arceliar
12bcb6cc1f Merge pull request #138 from neilalexander/dedwarf
Update build script
2018-06-21 09:33:28 -05:00
Arceliar
c4d28c4f65 Update build
More portable way to strip the `.go` extension from files when packing with upx.
2018-06-21 09:29:05 -05:00
Neil Alexander
2c3074a979 Update build script to strip by default, allow debug -d and UPX -u flags 2018-06-21 09:53:35 +01:00
Neil Alexander
b415adee6d Merge pull request #137 from yggdrasil-network/develop
Integrate history from develop for v0.2.1
2018-06-17 18:27:37 +01:00
Neil Alexander
10a66a4edc Update semver to hopefully avoid squash merges 2018-06-17 18:17:21 +01:00
Neil Alexander
e8e7e6bcf5 Bring squash merge into develop 2018-06-17 18:11:18 +01:00
Arceliar
f0fd19b5e5 Merge pull request #136 from Arceliar/dcfix
Mitigate connection cycling issue
2018-06-16 16:12:29 -05:00
Arceliar
496dc94f02 possibly mitigate livelock bug where lossy links lead to constant connect/disconnect cycles due to disagreement about which of the two duplicate autoconnection attempts to use 2018-06-16 15:31:25 -05:00
Neil Alexander
0ca2cda49b Merge pull request #135 from neilalexander/utf16
Fix typo for big-endian BOM
2018-06-16 17:05:43 +01:00
Neil Alexander
9ac7d4e0df Fix typo for big-endian BOM 2018-06-16 17:01:47 +01:00
Neil Alexander
0ec5f1c02c Version 0.2.1 (#130)
* switch address range from fd00::/8 to the deprecated 0200::/7 range

* Fix launchd script path and amend debian control file

* fix address/prefix code, platform specific parts still need testing

* macos

* cleanup old ugly session MTU code that only mattered with lossy UDP fragments

* Fix debian control file

* Let's try this again

* tcp/socks cleanup

* comment

* avoid the proxy.SOCK5 connection attempt unless we're actually going to use the dialer

* Update generate.sh

* prevent parent nodes from forcing coord oscillation, have dht.handleRes clean up the old request info immediately

* address range changes

* Update README.md

Consistently remove leading zeros from addresses in the readme.

* Update yggdrasil.go

* Collect yggdrasilctl during CI build

* Fix CircleCI after fat-fingered copypasta

* Fix for Windows

* clean up main yggdrasil.go imports and run gofmt
2018-06-15 11:02:45 +01:00
Arceliar
21b15c97a9 Merge pull request #134 from Arceliar/develop
clean up main yggdrasil.go imports and run gofmt
2018-06-15 04:42:32 -05:00
Arceliar
e65a66b181 clean up main yggdrasil.go imports and run gofmt 2018-06-15 04:30:09 -05:00
Arceliar
5fc4dddf83 Merge pull request #129 from neilalexander/utf16
Convert config from UTF-16 if a BOM is found
2018-06-15 04:20:59 -05:00
Neil Alexander
2fe493ba6c Merge pull request #133 from neilalexander/circleci
Collect yggdrasilctl during CI builds
2018-06-15 10:07:16 +01:00
Neil Alexander
17146ee5bb Fix for Windows 2018-06-15 10:00:58 +01:00
Neil Alexander
3be8d97cc3 Fix CircleCI after fat-fingered copypasta 2018-06-15 09:58:02 +01:00
Neil Alexander
20fc551a67 Collect yggdrasilctl during CI build 2018-06-15 09:54:29 +01:00
Neil Alexander
0aea4bd395 Update yggdrasil.go 2018-06-15 09:20:41 +01:00
Arceliar
3d9ab25930 Merge pull request #127 from Arceliar/bugfixes
Bugfixes
2018-06-14 13:46:29 -05:00
Arceliar
28d187d5a0 Merge pull request #128 from Arceliar/doc
address range changes
2018-06-14 13:36:49 -05:00
Arceliar
0c74c74879 Update README.md
Consistently remove leading zeros from addresses in the readme.
2018-06-14 13:33:36 -05:00
Arceliar
8025e51299 address range changes 2018-06-14 13:26:46 -05:00
Arceliar
7fe038f87e prevent parent nodes from forcing coord oscillation, have dht.handleRes clean up the old request info immediately 2018-06-14 12:32:18 -05:00
Neil Alexander
6c556da05e Merge pull request #125 from Arceliar/cleanup
Cleanup
2018-06-14 15:26:34 +01:00
Neil Alexander
6a1927a09e Merge pull request #126 from neilalexander/contrib
Set Conflicts as well as Replaces on the Debian package
2018-06-14 15:25:54 +01:00
Neil Alexander
415748d381 Update generate.sh 2018-06-14 15:22:13 +01:00
Arceliar
d9c9787611 avoid the proxy.SOCK5 connection attempt unless we're actually going to use the dialer 2018-06-14 09:21:35 -05:00
Arceliar
e8eaabf0c8 comment 2018-06-14 09:12:58 -05:00
Neil Alexander
aa46f67d08 Merge pull request #124 from neilalexander/contrib
Fix debian control file again
2018-06-14 15:12:52 +01:00
Arceliar
57837057b7 tcp/socks cleanup 2018-06-14 09:11:34 -05:00
Neil Alexander
7cc067e3a5 Let's try this again 2018-06-14 15:07:39 +01:00
Neil Alexander
dde0486f03 Merge pull request #123 from neilalexander/contrib
Fix debian control file
2018-06-14 14:51:45 +01:00
Neil Alexander
2ab5a1f1c2 Fix debian control file 2018-06-14 14:47:55 +01:00
Arceliar
f7a7f601a0 cleanup old ugly session MTU code that only mattered with lossy UDP fragments 2018-06-14 08:38:43 -05:00
Neil Alexander
36f80cb12c Merge pull request #122 from Arceliar/addressPrefix
Address prefix change
2018-06-14 14:37:22 +01:00
Arceliar
695610c305 Merge pull request #121 from neilalexander/contrib
Fix launchd script path and amend debian control file
2018-06-14 08:09:48 -05:00
Arceliar
93ffc0b876 macos 2018-06-14 08:00:57 -05:00
Arceliar
e7fca66655 fix address/prefix code, platform specific parts still need testing 2018-06-14 07:58:07 -05:00
Neil Alexander
474fdda8ca Fix launchd script path and amend debian control file 2018-06-14 13:24:37 +01:00
Arceliar
330175889e switch address range from fd00::/8 to the deprecated 0200::/7 range 2018-06-14 07:08:48 -05:00
68 changed files with 3881 additions and 1832 deletions

View File

@@ -5,62 +5,90 @@ version: 2
jobs:
build:
docker:
- image: circleci/golang:1.9
working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}}
- image: circleci/golang:1.11
steps:
- checkout
- run:
name: Create artifact upload directory and set variables
command: |
mkdir /tmp/upload
echo 'export CINAME=$(sh contrib/semver/name.sh)' >> $BASH_ENV
echo 'export CIVERSION=$(sh contrib/semver/version.sh | cut -c 2-)' >> $BASH_ENV
echo 'export CIVERSION=$(sh contrib/semver/version.sh --bare)' >> $BASH_ENV
git config --global user.email "$(git log --format='%ae' HEAD -1)";
git config --global user.name "$(git log --format='%an' HEAD -1)";
- run:
name: Build for Linux (including Debian packages)
name: Install alien
command: |
PKGARCH=amd64 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-amd64;
PKGARCH=i386 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-i386;
PKGARCH=mipsel sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mipsel;
PKGARCH=mips sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mips;
PKGARCH=armhf sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-armhf;
sudo apt-get install -y alien
- run:
name: Test debug builds
command: |
./build -d
test -f yggdrasil && test -f yggdrasilctl
- run:
name: Build for Linux (including Debian packages and RPMs)
command: |
rm -f {yggdrasil,yggdrasilctl}
PKGARCH=amd64 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-amd64;
PKGARCH=i386 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-i386;
PKGARCH=mipsel sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mipsel && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-mipsel;
PKGARCH=mips sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-mips && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-mips;
PKGARCH=armhf sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-armhf && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-armhf;
PKGARCH=arm64 sh contrib/deb/generate.sh && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-linux-arm64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-linux-arm64;
sudo alien --to-rpm yggdrasil*.deb --scripts --keep-version && mv *.rpm /tmp/upload/;
mv *.deb /tmp/upload/
- run:
name: Build for macOS
command: |
GOOS=darwin GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-amd64;
GOOS=darwin GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-i386;
rm -f {yggdrasil,yggdrasilctl}
GOOS=darwin GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-darwin-amd64;
GOOS=darwin GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-darwin-i386;
- run:
name: Build for macOS (.pkg format)
command: |
rm -rf {yggdrasil,yggdrasilctl}
GOOS=darwin GOARCH=amd64 ./build && PKGARCH=amd64 sh contrib/macos/create-pkg.sh && mv *.pkg /tmp/upload/
GOOS=darwin GOARCH=386 ./build && PKGARCH=i386 sh contrib/macos/create-pkg.sh && mv *.pkg /tmp/upload/
- run:
name: Build for OpenBSD
command: |
GOOS=openbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-amd64;
GOOS=openbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-i386;
rm -f {yggdrasil,yggdrasilctl}
GOOS=openbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-openbsd-amd64;
GOOS=openbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-openbsd-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-openbsd-i386;
- run:
name: Build for FreeBSD
command: |
GOOS=freebsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-amd64;
GOOS=freebsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-i386;
rm -f {yggdrasil,yggdrasilctl}
GOOS=freebsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-freebsd-amd64;
GOOS=freebsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-freebsd-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-freebsd-i386;
- run:
name: Build for NetBSD
command: |
GOOS=netbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-amd64;
GOOS=netbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-i386;
rm -f {yggdrasil,yggdrasilctl}
GOOS=netbsd GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-netbsd-amd64;
GOOS=netbsd GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-netbsd-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-netbsd-i386;
- run:
name: Build for Windows
command: |
GOOS=windows GOARCH=amd64 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-amd64.exe;
GOOS=windows GOARCH=386 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-i386.exe;
rm -f {yggdrasil,yggdrasilctl}
GOOS=windows GOARCH=amd64 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-amd64.exe && mv yggdrasilctl.exe /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-windows-amd64.exe;
GOOS=windows GOARCH=386 ./build && mv yggdrasil.exe /tmp/upload/$CINAME-$CIVERSION-windows-i386.exe && mv yggdrasilctl.exe /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-windows-i386.exe;
- run:
name: Build for EdgeRouter
command: |
rm -f {yggdrasil,yggdrasilctl}
git clone https://github.com/neilalexander/vyatta-yggdrasil /tmp/vyatta-yggdrasil;
cd /tmp/vyatta-yggdrasil;
BUILDDIR_YGG=$CIRCLE_WORKING_DIRECTORY ./build-edgerouter-x $CIRCLE_BRANCH;
@@ -70,3 +98,13 @@ jobs:
- store_artifacts:
path: /tmp/upload
destination: /
- run:
name: Create tags (master branch only)
command: >
if [ "${CIRCLE_BRANCH}" == "master" ]; then
git tag -f -a $(sh contrib/semver/version.sh) -m "Created by CircleCI" && git push -f --tags;
else
echo "Only runs for master branch (this is ${CIRCLE_BRANCH})";
fi;
when: on_success

160
CHANGELOG.md Normal file
View File

@@ -0,0 +1,160 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
<!-- Use this as a template
## [X.Y.Z] - YYYY-MM-DD
### Added
- for new features.
### Changed
- for changes in existing functionality.
### Deprecated
- for soon-to-be removed features.
### Removed
- for now removed features.
### Fixed
- for any bug fixes.
### Security
- in case of vulnerabilities.
-->
## [0.3.0] - 2018-12-12
### Added
- Crypto-key routing support for tunnelling both IPv4 and IPv6 over Yggdrasil
- Add advanced `SwitchOptions` in configuration file for tuning the switch
- Add `dhtPing` to the admin socket to aid in crawling the network
- New macOS .pkgs built automatically by CircleCI
- Add Dockerfile to repository for Docker support
- Add `-json` command line flag for generating and normalising configuration in plain JSON instead of HJSON
- Build name and version numbers are now imprinted onto the build, accessible through `yggdrasil -version` and `yggdrasilctl getSelf`
- Add ability to disable admin socket by setting `AdminListen` to `"none"`
- `yggdrasilctl` now tries to look for the default configuration file to find `AdminListen` if `-endpoint` is not specified
- `yggdrasilctl` now returns more useful logging in the event of a fatal error
### Changed
- Switched to Chord DHT (instead of Kademlia, although still compatible at the protocol level)
- The `AdminListen` option and `yggdrasilctl` now default to `unix:///var/run/yggdrasil.sock` on BSDs, macOS and Linux
- Cleaned up some of the parameter naming in the admin socket
- Latency-based parent selection for the switch instead of uptime-based (should help to avoid high latency links somewhat)
- Real peering endpoints now shown in the admin socket `getPeers` call to help identify peerings
- Reuse the multicast port on supported platforms so that multiple Yggdrasil processes can run
- `yggdrasilctl` now has more useful help text (with `-help` or when no arguments passed)
### Fixed
- Memory leaks in the DHT fixed
- Crash fixed where the ICMPv6 NDP goroutine would incorrectly start in TUN mode
- Removing peers from the switch table if they stop sending switch messages but keep the TCP connection alive
## [0.2.7] - 2018-10-13
### Added
- Session firewall, which makes it possible to control who can open sessions with your node
- Add `getSwitchQueues` to admin socket
- Add `InterfacePeers` for configuring static peerings via specific network interfaces
- More output shown in `getSwitchPeers`
- FreeBSD service script in `contrib`
## Changed
- CircleCI builds are now built with Go 1.11 instead of Go 1.9
## Fixed
- Race condition in the switch table, reported by trn
- Debug builds are now tested by CircleCI as well as platform release builds
- Port number fixed on admin graph from unknown nodes
## [0.2.6] - 2018-07-31
### Added
- Configurable TCP timeouts to assist in peering over Tor/I2P
- Prefer IPv6 flow label when extending coordinates to sort backpressure queues
- `arm64` builds through CircleCI
### Changed
- Sort dot graph links by integer value
## [0.2.5] - 2018-07-19
### Changed
- Make `yggdrasilctl` less case sensitive
- More verbose TCP disconnect messages
### Fixed
- Fixed debug builds
- Cap maximum MTU on Linux in TAP mode
- Process successfully-read TCP traffic before checking for / handling errors (fixes EOF behavior)
## [0.2.4] - 2018-07-08
### Added
- Support for UNIX domain sockets for the admin socket using `unix:///path/to/file.sock`
- Centralised platform-specific defaults
### Changed
- Backpressure tuning, including reducing resource consumption
### Fixed
- macOS local ping bug, which previously prevented you from pinging your own `utun` adapter's IPv6 address
## [0.2.3] - 2018-06-29
### Added
- Begin keeping changelog (incomplete and possibly inaccurate information before this point).
- Build RPMs in CircleCI using alien. This provides package support for Fedora, Red Hat Enterprise Linux, CentOS and other RPM-based distributions.
### Changed
- Local backpressure improvements.
- Change `box_pub_key` to `key` in admin API for simplicity.
- Session cleanup.
## [0.2.2] - 2018-06-21
### Added
- Add `yggdrasilconf` utility for testing with the `vyatta-yggdrasil` package.
- Add a randomized retry delay after TCP disconnects, to prevent synchronization livelocks.
### Changed
- Update build script to strip by default, which significantly reduces the size of the binary.
- Add debug `-d` and UPX `-u` flags to the `build` script.
- Start pprof in debug builds based on an environment variable (e.g. `PPROFLISTEN=localhost:6060`), instead of a flag.
### Fixed
- Fix typo in big-endian BOM so that both little-endian and big-endian UTF-16 files are detected correctly.
## [0.2.1] - 2018-06-15
### Changed
- The address range was moved from `fd00::/8` to `200::/7`. This range was chosen as it is marked as deprecated. The change prevents overlap with other ULA privately assigned ranges.
### Fixed
- UTF-16 detection conversion for configuration files, which can particularly be a problem on Windows 10 if a configuration file is generated from within PowerShell.
- Fixes to the Debian package control file.
- Fixes to the launchd service for macOS.
- Fixes to the DHT and switch.
## [0.2.0] - 2018-06-13
### Added
- Exchange version information during connection setup, to prevent connections with incompatible versions.
### Changed
- Wire format changes (backwards incompatible).
- Less maintenance traffic per peer.
- Exponential back-off for DHT maintenance traffic (less maintenance traffic for known good peers).
- Iterative DHT (added some time between v0.1.0 and here).
- Use local queue sizes for a sort of local-only backpressure routing, instead of the removed bandwidth estimates, when deciding where to send a packet.
### Removed
- UDP peering, this may be added again if/when a better implementation appears.
- Per peer bandwidth estimation, as this has been replaced with an early local backpressure implementation.
## [0.1.0] - 2018-02-01
### Added
- Adopt semantic versioning.
### Changed
- Wire format changes (backwards incompatible).
- Many other undocumented changes leading up to this release and before the next one.
## [0.0.1] - 2017-12-28
### Added
- First commit.
- Initial public release.

1
Dockerfile Normal file
View File

@@ -0,0 +1 @@
contrib/docker/Dockerfile

View File

@@ -17,11 +17,10 @@ statement from your version. This exception does not (and cannot) modify any
license terms which apply to the Application, with which you must still
comply.
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.

View File

@@ -15,7 +15,7 @@ You're encouraged to play with it, but it is strongly advised not to use it for
## Building
1. Install Go (tested on 1.9+, [godeb](https://github.com/niemeyer/godeb) is recommended for debian-based linux distributions).
1. Install Go (requires 1.11 or later, [godeb](https://github.com/niemeyer/godeb) is recommended for Debian-based Linux distributions).
2. Clone this repository.
2. `./build`
@@ -85,7 +85,7 @@ journalctl -u yggdrasil
- Tested and working on Windows 7 and Windows 10, and should work on any recent versions of Windows, but it depends on the [OpenVPN TAP driver](https://openvpn.net/index.php/open-source/downloads.html) being installed first.
- Has been proven to work with both the [NDIS 5](https://swupdate.openvpn.org/community/releases/tap-windows-9.9.2_3.exe) (`tap-windows-9.9.2_3`) driver and the [NDIS 6](https://swupdate.openvpn.org/community/releases/tap-windows-9.21.2.exe) (`tap-windows-9.21.2`) driver, however there are substantial performance issues with the NDIS 6 driver therefore it is recommended to use the NDIS 5 driver instead.
- Be aware that connectivity issues can occur on Windows if multiple IPv6 addresses from the `fd00::/8` prefix are assigned to the TAP interface. If this happens, then you may need to manually remove the old/unused addresses from the interface (though the code has a workaround in place to do this automatically in some cases).
- Be aware that connectivity issues can occur on Windows if multiple IPv6 addresses from the `200::/7` prefix are assigned to the TAP interface. If this happens, then you may need to manually remove the old/unused addresses from the interface (though the code has a workaround in place to do this automatically in some cases).
- TUN mode is not supported on Windows.
- Yggdrasil can be installed as a Windows service so that it runs automatically in the background. From an Administrator Command Prompt:
```
@@ -105,26 +105,26 @@ sc create yggdrasil binpath= "\"C:\path\to\yggdrasil.exe\" -autoconf"
## Optional: advertise a prefix locally
Suppose a node has generated the address: `fd00:1111:2222:3333:4444:5555:6666:7777`
Suppose a node has generated the address: `200:1111:2222:3333:4444:5555:6666:7777`
Then the node may also use addresses from the prefix: `fd80:1111:2222:3333::/64` (note the `fd00` changed to `fd80`, a separate `/9` is used for prefixes, but the rest of the first 64 bits are the same).
Then the node may also use addresses from the prefix: `300:1111:2222:3333::/64` (note the `200` changed to `300`, a separate `/8` is used for prefixes, but the rest of the first 64 bits are the same).
To advertise this prefix and a route to `fd00::/8`, the following seems to work on the developers' networks:
To advertise this prefix and a route to `200::/7`, the following seems to work on the developers' networks:
1. Enable IPv6 forwarding (e.g. `sysctl -w net.ipv6.conf.all.forwarding=1` or add it to sysctl.conf).
2. `ip addr add fd80:1111:2222:3333::1/64 dev eth0` or similar, to assign an address for the router to use in that prefix, where the LAN is reachable through `eth0`.
2. `ip addr add 300:1111:2222:3333::1/64 dev eth0` or similar, to assign an address for the router to use in that prefix, where the LAN is reachable through `eth0`.
3. Install/run `radvd` with something like the following in `/etc/radvd.conf`:
```
interface eth0
{
AdvSendAdvert on;
prefix fd80:1111:2222:3333::/64 {
prefix 300:1111:2222:3333::/64 {
AdvOnLink on;
AdvAutonomous on;
};
route fd00::/8 {};
route 200::/7 {};
};
```

View File

@@ -1 +0,0 @@
0.2

43
build
View File

@@ -1,11 +1,36 @@
#!/bin/sh
export GOPATH=$PWD
echo "Downloading..."
go get -d -v
go get -d -v yggdrasil
for file in *.go ; do
echo "Building: $file"
go build $@ $file
#go build -ldflags="-s -w" -v $file
#upx --brute ${file/.go/}
PKGSRC=${PKGSRC:-github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil}
PKGNAME=${PKGNAME:-$(sh contrib/semver/name.sh)}
PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)}
LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER"
while getopts "udtc:l:" option
do
case "${option}"
in
u) UPX=true;;
d) DEBUG=true;;
t) TABLES=true;;
c) GCFLAGS="$GCFLAGS $OPTARG";;
l) LDFLAGS="$LDFLAGS $OPTARG";;
esac
done
if [ -z $TABLES ]; then
STRIP="-s -w"
fi
for CMD in `ls cmd/` ; do
echo "Building: $CMD"
if [ $DEBUG ]; then
go build -ldflags="$LDFLAGS" -gcflags="$GCFLAGS" -tags debug -v ./cmd/$CMD
else
go build -ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" -v ./cmd/$CMD
fi
if [ $UPX ]; then
upx --brute $CMD
fi
done

View File

@@ -1,24 +1,30 @@
package main
import "encoding/json"
import "encoding/hex"
import "flag"
import "fmt"
import "io/ioutil"
import "os"
import "os/signal"
import "syscall"
import "time"
import "regexp"
import "math/rand"
import "log"
import (
"bytes"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"os/signal"
"regexp"
"syscall"
"time"
import "yggdrasil"
import "yggdrasil/config"
"golang.org/x/text/encoding/unicode"
import "github.com/kardianos/minwinsvc"
import "github.com/neilalexander/hjson-go"
import "github.com/mitchellh/mapstructure"
"github.com/kardianos/minwinsvc"
"github.com/mitchellh/mapstructure"
"github.com/neilalexander/hjson-go"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
)
type nodeConfig = config.NodeConfig
type Core = yggdrasil.Core
@@ -48,26 +54,37 @@ func generateConfig(isAutoconf bool) *nodeConfig {
r1 := rand.New(rand.NewSource(time.Now().UnixNano()))
cfg.Listen = fmt.Sprintf("[::]:%d", r1.Intn(65534-32768)+32768)
}
cfg.AdminListen = "localhost:9001"
cfg.AdminListen = defaults.GetDefaults().DefaultAdminListen
cfg.EncryptionPublicKey = hex.EncodeToString(bpub[:])
cfg.EncryptionPrivateKey = hex.EncodeToString(bpriv[:])
cfg.SigningPublicKey = hex.EncodeToString(spub[:])
cfg.SigningPrivateKey = hex.EncodeToString(spriv[:])
cfg.Peers = []string{}
cfg.InterfacePeers = map[string][]string{}
cfg.AllowedEncryptionPublicKeys = []string{}
cfg.MulticastInterfaces = []string{".*"}
cfg.IfName = core.GetTUNDefaultIfName()
cfg.IfMTU = core.GetTUNDefaultIfMTU()
cfg.IfTAPMode = core.GetTUNDefaultIfTAPMode()
cfg.IfName = defaults.GetDefaults().DefaultIfName
cfg.IfMTU = defaults.GetDefaults().DefaultIfMTU
cfg.IfTAPMode = defaults.GetDefaults().DefaultIfTAPMode
cfg.SessionFirewall.Enable = false
cfg.SessionFirewall.AllowFromDirect = true
cfg.SessionFirewall.AllowFromRemote = true
cfg.SwitchOptions.MaxTotalQueueSize = yggdrasil.SwitchQueueTotalMinSize
return &cfg
}
// Generates a new configuration and returns it in HJSON format. This is used
// with -genconf.
func doGenconf() string {
func doGenconf(isjson bool) string {
cfg := generateConfig(false)
bs, err := hjson.Marshal(cfg)
var bs []byte
var err error
if isjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
@@ -77,16 +94,21 @@ func doGenconf() string {
// The main function is responsible for configuring and starting Yggdrasil.
func main() {
// Configure the command line parameters.
pprof := flag.Bool("pprof", false, "Run pprof, see http://localhost:6060/debug/pprof/")
genconf := flag.Bool("genconf", false, "print a new config to stdout")
useconf := flag.Bool("useconf", false, "read config from stdin")
useconffile := flag.String("useconffile", "", "read config from specified file path")
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
version := flag.Bool("version", false, "prints the version of this build")
flag.Parse()
var cfg *nodeConfig
switch {
case *version:
fmt.Println("Build name:", yggdrasil.GetBuildName())
fmt.Println("Build version:", yggdrasil.GetBuildVersion())
os.Exit(0)
case *autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN/TAP interface.
@@ -107,6 +129,19 @@ func main() {
if err != nil {
panic(err)
}
// If there's a byte order mark - which Windows 10 is now incredibly fond of
// throwing everywhere when it's converting things into UTF-16 for the hell
// of it - remove it and decode back down into UTF-8. This is necessary
// because hjson doesn't know what to do with UTF-16 and will panic
if bytes.Compare(config[0:2], []byte{0xFF, 0xFE}) == 0 ||
bytes.Compare(config[0:2], []byte{0xFE, 0xFF}) == 0 {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
config, err = decoder.Bytes(config)
if err != nil {
panic(err)
}
}
// Generate a new configuration - this gives us a set of sane defaults -
// then parse the configuration we loaded above on top of it. The effect
// of this is that any configuration item that is missing from the provided
@@ -163,7 +198,12 @@ func main() {
// their configuration file with newly mapped names (like above) or to
// convert from plain JSON to commented HJSON.
if *normaliseconf {
bs, err := hjson.Marshal(cfg)
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
@@ -172,7 +212,7 @@ func main() {
}
case *genconf:
// Generate a new configuration and print it to stdout.
fmt.Println(doGenconf())
fmt.Println(doGenconf(*confjson))
default:
// No flags were provided, therefore print the list of flags to stdout.
flag.PrintDefaults()
@@ -185,12 +225,6 @@ func main() {
}
// Create a new logger that logs output to stdout.
logger := log.New(os.Stdout, "", log.Flags())
// If the -pprof flag was provided then start the pprof service on port 6060.
if *pprof {
if err := yggdrasil.StartProfiler(logger); err != nil {
logger.Println(err)
}
}
// Setup the Yggdrasil node itself. The node{} type includes a Core, so we
// don't need to create this manually.
n := node{}
@@ -219,14 +253,20 @@ func main() {
// configure them. The loop ensures that disconnected peers will eventually
// be reconnected with.
go func() {
if len(cfg.Peers) == 0 {
if len(cfg.Peers) == 0 && len(cfg.InterfacePeers) == 0 {
return
}
for {
for _, p := range cfg.Peers {
n.core.AddPeer(p)
for _, peer := range cfg.Peers {
n.core.AddPeer(peer, "")
time.Sleep(time.Second)
}
for intf, intfpeers := range cfg.InterfacePeers {
for _, peer := range intfpeers {
n.core.AddPeer(peer, intf)
time.Sleep(time.Second)
}
}
time.Sleep(time.Minute)
}
}()

401
cmd/yggdrasilctl/main.go Normal file
View File

@@ -0,0 +1,401 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/url"
"os"
"sort"
"strconv"
"strings"
"golang.org/x/text/encoding/unicode"
"github.com/neilalexander/hjson-go"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
)
type admin_info map[string]interface{}
func main() {
logbuffer := &bytes.Buffer{}
logger := log.New(logbuffer, "", log.Flags())
defer func() {
if r := recover(); r != nil {
logger.Println("Fatal error:", r)
fmt.Print(logbuffer)
os.Exit(1)
}
}()
endpoint := defaults.GetDefaults().DefaultAdminListen
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] command [key=value] [key=value] ...\n", os.Args[0])
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println("Commands:\n - Use \"list\" for a list of available commands")
fmt.Println("Examples:")
fmt.Println(" - ", os.Args[0], "list")
fmt.Println(" - ", os.Args[0], "getPeers")
fmt.Println(" - ", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false")
fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getDHT")
fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT")
}
server := flag.String("endpoint", endpoint, "Admin socket endpoint")
injson := flag.Bool("json", false, "Output in JSON format (as opposed to pretty-print)")
verbose := flag.Bool("v", false, "Verbose output (includes public keys)")
flag.Parse()
args := flag.Args()
if len(args) == 0 {
flag.Usage()
return
}
if *server == endpoint {
if config, err := ioutil.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil {
if bytes.Compare(config[0:2], []byte{0xFF, 0xFE}) == 0 ||
bytes.Compare(config[0:2], []byte{0xFE, 0xFF}) == 0 {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
config, err = decoder.Bytes(config)
if err != nil {
panic(err)
}
}
var dat map[string]interface{}
if err := hjson.Unmarshal(config, &dat); err != nil {
panic(err)
}
if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") {
endpoint = ep
logger.Println("Found platform default config file", defaults.GetDefaults().DefaultConfigFile)
logger.Println("Using endpoint", endpoint, "from AdminListen")
} else {
logger.Println("Configuration file doesn't contain appropriate AdminListen option")
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen)
}
} else {
logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile)
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen)
}
} else {
logger.Println("Using endpoint", endpoint, "from command line")
}
var conn net.Conn
u, err := url.Parse(endpoint)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
logger.Println("Connecting to UNIX socket", endpoint[7:])
conn, err = net.Dial("unix", endpoint[7:])
case "tcp":
logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", u.Host)
default:
logger.Println("Unknown protocol or malformed address - check your endpoint")
err = errors.New("protocol not supported")
}
} else {
logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", endpoint)
}
if err != nil {
panic(err)
}
logger.Println("Connected")
defer conn.Close()
decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn)
send := make(admin_info)
recv := make(admin_info)
for c, a := range args {
if c == 0 {
logger.Printf("Sending request: %v\n", a)
send["request"] = a
continue
}
tokens := strings.Split(a, "=")
if i, err := strconv.Atoi(tokens[1]); err == nil {
logger.Printf("Sending parameter %s: %d\n", tokens[0], i)
send[tokens[0]] = i
} else {
switch strings.ToLower(tokens[1]) {
case "true":
send[tokens[0]] = true
case "false":
send[tokens[0]] = false
default:
send[tokens[0]] = tokens[1]
}
logger.Printf("Sending parameter %s: %v\n", tokens[0], send[tokens[0]])
}
}
if err := encoder.Encode(&send); err != nil {
panic(err)
}
logger.Printf("Request sent")
if err := decoder.Decode(&recv); err == nil {
logger.Printf("Response received")
if recv["status"] == "error" {
if err, ok := recv["error"]; ok {
fmt.Println("Admin socket returned an error:", err)
} else {
fmt.Println("Admin socket returned an error but didn't specify any error text")
}
os.Exit(1)
}
if _, ok := recv["request"]; !ok {
fmt.Println("Missing request in response (malformed response?)")
os.Exit(1)
}
if _, ok := recv["response"]; !ok {
fmt.Println("Missing response body (malformed response?)")
os.Exit(1)
}
req := recv["request"].(map[string]interface{})
res := recv["response"].(map[string]interface{})
if *injson {
if json, err := json.MarshalIndent(res, "", " "); err == nil {
fmt.Println(string(json))
}
os.Exit(0)
}
switch strings.ToLower(req["request"].(string)) {
case "dot":
fmt.Println(res["dot"])
case "list", "getpeers", "getswitchpeers", "getdht", "getsessions", "dhtping":
maxWidths := make(map[string]int)
var keyOrder []string
keysOrdered := false
for _, tlv := range res {
for slk, slv := range tlv.(map[string]interface{}) {
if !keysOrdered {
for k := range slv.(map[string]interface{}) {
if !*verbose {
if k == "box_pub_key" || k == "box_sig_key" {
continue
}
}
keyOrder = append(keyOrder, fmt.Sprint(k))
}
sort.Strings(keyOrder)
keysOrdered = true
}
for k, v := range slv.(map[string]interface{}) {
if len(fmt.Sprint(slk)) > maxWidths["key"] {
maxWidths["key"] = len(fmt.Sprint(slk))
}
if len(fmt.Sprint(v)) > maxWidths[k] {
maxWidths[k] = len(fmt.Sprint(v))
if maxWidths[k] < len(k) {
maxWidths[k] = len(k)
}
}
}
}
if len(keyOrder) > 0 {
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", "")
for _, v := range keyOrder {
fmt.Printf("%-"+fmt.Sprint(maxWidths[v])+"s ", v)
}
fmt.Println()
}
for slk, slv := range tlv.(map[string]interface{}) {
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", slk)
for _, k := range keyOrder {
preformatted := slv.(map[string]interface{})[k]
var formatted string
switch k {
case "bytes_sent", "bytes_recvd":
formatted = fmt.Sprintf("%d", uint(preformatted.(float64)))
case "uptime", "last_seen":
seconds := uint(preformatted.(float64)) % 60
minutes := uint(preformatted.(float64)/60) % 60
hours := uint(preformatted.(float64) / 60 / 60)
formatted = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
default:
formatted = fmt.Sprint(preformatted)
}
fmt.Printf("%-"+fmt.Sprint(maxWidths[k])+"s ", formatted)
}
fmt.Println()
}
}
case "gettuntap", "settuntap":
for k, v := range res {
fmt.Println("Interface name:", k)
if mtu, ok := v.(map[string]interface{})["mtu"].(float64); ok {
fmt.Println("Interface MTU:", mtu)
}
if tap_mode, ok := v.(map[string]interface{})["tap_mode"].(bool); ok {
fmt.Println("TAP mode:", tap_mode)
}
}
case "getself":
for k, v := range res["self"].(map[string]interface{}) {
if buildname, ok := v.(map[string]interface{})["build_name"].(string); ok && buildname != "unknown" {
fmt.Println("Build name:", buildname)
}
if buildversion, ok := v.(map[string]interface{})["build_version"].(string); ok && buildversion != "unknown" {
fmt.Println("Build version:", buildversion)
}
fmt.Println("IPv6 address:", k)
if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok {
fmt.Println("IPv6 subnet:", subnet)
}
if coords, ok := v.(map[string]interface{})["coords"].(string); ok {
fmt.Println("Coords:", coords)
}
if *verbose {
if boxPubKey, ok := v.(map[string]interface{})["box_pub_key"].(string); ok {
fmt.Println("Public encryption key:", boxPubKey)
}
if boxSigKey, ok := v.(map[string]interface{})["box_sig_key"].(string); ok {
fmt.Println("Public signing key:", boxSigKey)
}
}
}
case "getswitchqueues":
maximumqueuesize := float64(4194304)
portqueues := make(map[float64]float64)
portqueuesize := make(map[float64]float64)
portqueuepackets := make(map[float64]float64)
v := res["switchqueues"].(map[string]interface{})
if queuecount, ok := v["queues_count"].(float64); ok {
fmt.Printf("Active queue count: %d queues\n", uint(queuecount))
}
if queuesize, ok := v["queues_size"].(float64); ok {
fmt.Printf("Active queue size: %d bytes\n", uint(queuesize))
}
if highestqueuecount, ok := v["highest_queues_count"].(float64); ok {
fmt.Printf("Highest queue count: %d queues\n", uint(highestqueuecount))
}
if highestqueuesize, ok := v["highest_queues_size"].(float64); ok {
fmt.Printf("Highest queue size: %d bytes\n", uint(highestqueuesize))
}
if m, ok := v["maximum_queues_size"].(float64); ok {
maximumqueuesize = m
fmt.Printf("Maximum queue size: %d bytes\n", uint(maximumqueuesize))
}
if queues, ok := v["queues"].([]interface{}); ok {
if len(queues) != 0 {
fmt.Println("Active queues:")
for _, v := range queues {
queueport := v.(map[string]interface{})["queue_port"].(float64)
queuesize := v.(map[string]interface{})["queue_size"].(float64)
queuepackets := v.(map[string]interface{})["queue_packets"].(float64)
queueid := v.(map[string]interface{})["queue_id"].(string)
portqueues[queueport]++
portqueuesize[queueport] += queuesize
portqueuepackets[queueport] += queuepackets
queuesizepercent := (100 / maximumqueuesize) * queuesize
fmt.Printf("- Switch port %d, Stream ID: %v, size: %d bytes (%d%% full), %d packets\n",
uint(queueport), []byte(queueid), uint(queuesize),
uint(queuesizepercent), uint(queuepackets))
}
}
}
if len(portqueuesize) > 0 && len(portqueuepackets) > 0 {
fmt.Println("Aggregated statistics by switchport:")
for k, v := range portqueuesize {
queuesizepercent := (100 / (portqueues[k] * maximumqueuesize)) * v
fmt.Printf("- Switch port %d, size: %d bytes (%d%% full), %d packets\n",
uint(k), uint(v), uint(queuesizepercent), uint(portqueuepackets[k]))
}
}
case "addpeer", "removepeer", "addallowedencryptionpublickey", "removeallowedencryptionpublickey", "addsourcesubnet", "addroute", "removesourcesubnet", "removeroute":
if _, ok := res["added"]; ok {
for _, v := range res["added"].([]interface{}) {
fmt.Println("Added:", fmt.Sprint(v))
}
}
if _, ok := res["not_added"]; ok {
for _, v := range res["not_added"].([]interface{}) {
fmt.Println("Not added:", fmt.Sprint(v))
}
}
if _, ok := res["removed"]; ok {
for _, v := range res["removed"].([]interface{}) {
fmt.Println("Removed:", fmt.Sprint(v))
}
}
if _, ok := res["not_removed"]; ok {
for _, v := range res["not_removed"].([]interface{}) {
fmt.Println("Not removed:", fmt.Sprint(v))
}
}
case "getallowedencryptionpublickeys":
if _, ok := res["allowed_box_pubs"]; !ok {
fmt.Println("All connections are allowed")
} else if res["allowed_box_pubs"] == nil {
fmt.Println("All connections are allowed")
} else {
fmt.Println("Connections are allowed only from the following public box keys:")
for _, v := range res["allowed_box_pubs"].([]interface{}) {
fmt.Println("-", v)
}
}
case "getmulticastinterfaces":
if _, ok := res["multicast_interfaces"]; !ok {
fmt.Println("No multicast interfaces found")
} else if res["multicast_interfaces"] == nil {
fmt.Println("No multicast interfaces found")
} else {
fmt.Println("Multicast peer discovery is active on:")
for _, v := range res["multicast_interfaces"].([]interface{}) {
fmt.Println("-", v)
}
}
case "getsourcesubnets":
if _, ok := res["source_subnets"]; !ok {
fmt.Println("No source subnets found")
} else if res["source_subnets"] == nil {
fmt.Println("No source subnets found")
} else {
fmt.Println("Source subnets:")
for _, v := range res["source_subnets"].([]interface{}) {
fmt.Println("-", v)
}
}
case "getroutes":
if _, ok := res["routes"]; !ok {
fmt.Println("No routes found")
} else if res["routes"] == nil {
fmt.Println("No routes found")
} else {
fmt.Println("Routes:")
for _, v := range res["routes"].([]interface{}) {
fmt.Println("-", v)
}
}
default:
if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil {
fmt.Println(string(json))
}
}
} else {
logger.Println("Error receiving response:", err)
}
if v, ok := recv["status"]; ok && v != "success" {
os.Exit(1)
}
os.Exit(0)
}

View File

@@ -0,0 +1,97 @@
package main
/*
This is a small utility that is designed to accompany the vyatta-yggdrasil
package. It takes a HJSON configuration file, makes changes to it based on
the command line arguments, and then spits out an updated file.
*/
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"strconv"
"github.com/neilalexander/hjson-go"
"golang.org/x/text/encoding/unicode"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
)
type nodeConfig = config.NodeConfig
func main() {
useconffile := flag.String("useconffile", "/etc/yggdrasil.conf", "update config at specified file path")
flag.Parse()
cfg := nodeConfig{}
var config []byte
var err error
config, err = ioutil.ReadFile(*useconffile)
if err != nil {
panic(err)
}
if bytes.Compare(config[0:2], []byte{0xFF, 0xFE}) == 0 ||
bytes.Compare(config[0:2], []byte{0xFE, 0xFF}) == 0 {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
config, err = decoder.Bytes(config)
if err != nil {
panic(err)
}
}
var dat map[string]interface{}
if err := hjson.Unmarshal(config, &dat); err != nil {
panic(err)
}
confJson, err := json.Marshal(dat)
if err != nil {
panic(err)
}
json.Unmarshal(confJson, &cfg)
switch flag.Arg(0) {
case "setMTU":
cfg.IfMTU, err = strconv.Atoi(flag.Arg(1))
if err != nil {
cfg.IfMTU = 1280
}
if mtu, _ := strconv.Atoi(flag.Arg(1)); mtu < 1280 {
cfg.IfMTU = 1280
}
case "setIfName":
cfg.IfName = flag.Arg(1)
case "setListen":
cfg.Listen = flag.Arg(1)
case "setAdminListen":
cfg.AdminListen = flag.Arg(1)
case "setIfTapMode":
if flag.Arg(1) == "true" {
cfg.IfTAPMode = true
} else {
cfg.IfTAPMode = false
}
case "addPeer":
found := false
for _, v := range cfg.Peers {
if v == flag.Arg(1) {
found = true
}
}
if !found {
cfg.Peers = append(cfg.Peers, flag.Arg(1))
}
case "removePeer":
for k, v := range cfg.Peers {
if v == flag.Arg(1) {
cfg.Peers = append(cfg.Peers[:k], cfg.Peers[k+1:]...)
}
}
}
bs, err := hjson.Marshal(cfg)
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
}

View File

@@ -7,23 +7,29 @@
if [ `pwd` != `git rev-parse --show-toplevel` ]
then
echo "You should run this script from the top-level directory of the git repo"
exit -1
exit 1
fi
PKGBRANCH=$(basename `git name-rev --name-only HEAD`)
PKGNAME=$(sh contrib/semver/name.sh)
PKGVERSION=$(sh contrib/semver/version.sh | cut -c 2-)
PKGVERSION=$(sh contrib/semver/version.sh --bare)
PKGARCH=${PKGARCH-amd64}
PKGFILE=$PKGNAME-$PKGVERSION-$PKGARCH.deb
PKGREPLACES=yggdrasil
if [ $PKGBRANCH = "master" ]; then
PKGREPLACES=yggdrasil-develop
fi
if [ $PKGARCH = "amd64" ]; then GOARCH=amd64 GOOS=linux ./build
elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build
elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build
elif [ $PKGARCH = "mips" ]; then GOARCH=mips64 GOOS=linux ./build
elif [ $PKGARCH = "armhf" ]; then GOARCH=arm GOOS=linux GOARM=7 ./build
elif [ $PKGARCH = "arm64" ]; then GOARCH=arm64 GOOS=linux ./build
else
echo "Specify PKGARCH=amd64,i386,mips,mipsel,armhf"
exit -1
echo "Specify PKGARCH=amd64,i386,mips,mipsel,armhf,arm64"
exit 1
fi
echo "Building $PKGFILE"
@@ -34,7 +40,7 @@ mkdir -p /tmp/$PKGNAME/usr/bin/
mkdir -p /tmp/$PKGNAME/etc/systemd/system/
cat > /tmp/$PKGNAME/debian/changelog << EOF
Please see https://github.com/Arceliar/yggdrasil-go/
Please see https://github.com/yggdrasil-network/yggdrasil-go/
EOF
echo 9 > /tmp/$PKGNAME/debian/compat
cat > /tmp/$PKGNAME/debian/control << EOF
@@ -43,15 +49,17 @@ Version: $PKGVERSION
Section: contrib/net
Priority: extra
Architecture: $PKGARCH
Replaces: $PKGREPLACES
Conflicts: $PKGREPLACES
Maintainer: Neil Alexander <neilalexander@users.noreply.github.com>
Description: Debian yggdrasil package
Binary yggdrasil package for Debian and Ubuntu
EOF
cat > /tmp/$PKGNAME/debian/copyright << EOF
Please see https://github.com/Arceliar/yggdrasil-go/
Please see https://github.com/yggdrasil-network/yggdrasil-go/
EOF
cat > /tmp/$PKGNAME/debian/docs << EOF
Please see https://github.com/Arceliar/yggdrasil-go/
Please see https://github.com/yggdrasil-network/yggdrasil-go/
EOF
cat > /tmp/$PKGNAME/debian/install << EOF
usr/bin/yggdrasil usr/bin

22
contrib/docker/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM docker.io/golang:alpine as builder
COPY . /src
WORKDIR /src
RUN apk add git && ./build
FROM docker.io/alpine
LABEL maintainer="Christer Waren/CWINFO <christer.waren@cwinfo.org>"
COPY --from=builder /src/yggdrasil /usr/bin/yggdrasil
COPY --from=builder /src/yggdrasilctl /usr/bin/yggdrasilctl
COPY contrib/docker/entrypoint.sh /usr/bin/entrypoint.sh
# RUN addgroup -g 1000 -S yggdrasil-network \
# && adduser -u 1000 -S -g 1000 --home /etc/yggdrasil-network yggdrasil-network
#
# USER yggdrasil-network
# TODO: Make running unprivileged work
VOLUME [ "/etc/yggdrasil-network" ]
ENTRYPOINT [ "/usr/bin/entrypoint.sh" ]

13
contrib/docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env sh
set -e
CONF_DIR="/etc/yggdrasil-network"
if [ ! -f "$CONF_DIR/config.conf" ]; then
echo "generate $CONF_DIR/config.conf"
yggdrasil --genconf > "$CONF_DIR/config.conf"
fi
yggdrasil --useconf < "$CONF_DIR/config.conf"
exit $?

72
contrib/freebsd/yggdrasil Normal file
View File

@@ -0,0 +1,72 @@
#!/bin/sh
#
# Put the yggdrasil and yggdrasilctl binaries into /usr/local/bin
# Then copy this script into /etc/rc.d/yggdrasil
# Finally, run:
# 1. chmod +x /etc/rc.d/yggdrasil /usr/local/bin/{yggdrasil,yggdrasilctl}
# 2. echo "yggdrasil_enable=yes" >> /etc/rc.d
# 3. service yggdrasil start
#
# PROVIDE: yggdrasil
# REQUIRE: networking
# KEYWORD:
. /etc/rc.subr
name="yggdrasil"
rcvar="yggdrasil_enable"
start_cmd="${name}_start"
stop_cmd="${name}_stop"
pidfile="/var/run/yggdrasil/${name}.pid"
command="/usr/sbin/daemon"
command_args="-P ${pidfile} -r -f ${yggdrasil_command}"
yggdrasil_start()
{
test ! -x /usr/local/bin/yggdrasil && (
logger -s -t yggdrasil "Warning: /usr/local/bin/yggdrasil is missing or not executable"
logger -s -t yggdrasil "Copy the yggdrasil binary into /usr/local/bin and then chmod +x /usr/local/bin/yggdrasil"
return 1
)
test ! -f /etc/yggdrasil.conf && (
logger -s -t yggdrasil "Generating new configuration file into /etc/yggdrasil.conf"
/usr/local/bin/yggdrasil -genconf > /etc/yggdrasil.conf
)
tap_path="$(cat /etc/yggdrasil.conf | egrep -o '/dev/tap[0-9]{1,2}$')"
tap_name="$(echo -n ${tap_path} | tr -d '/dev/')"
/sbin/ifconfig ${tap_name} >/dev/null 2>&1 || (
logger -s -t yggdrasil "Creating ${tap_name} adapter"
/sbin/ifconfig ${tap_name} create || logger -s -t yggdrasil "Failed to create ${tap_name} adapter"
)
test ! -d /var/run/yggdrasil && mkdir -p /var/run/yggdrasil
logger -s -t yggdrasil "Starting yggdrasil"
${command} ${command_args} /usr/local/bin/yggdrasil -useconffile /etc/yggdrasil.conf \
1>/var/log/yggdrasil.stdout.log \
2>/var/log/yggdrasil.stderr.log &
}
yggdrasil_stop()
{
logger -s -t yggdrasil "Stopping yggdrasil"
test -f /var/run/yggdrasil/${name}.pid && kill -TERM $(cat /var/run/yggdrasil/${name}.pid)
tap_path="$(cat /etc/yggdrasil.conf | grep /dev/tap | egrep -o '/dev/.*$')"
tap_name="$(echo -n ${tap_path} | tr -d '/dev/')"
/sbin/ifconfig ${tap_name} >/dev/null 2>&1 && (
logger -s -t yggdrasil "Destroying ${tap_name} adapter"
/sbin/ifconfig ${tap_name} destroy || logger -s -t yggdrasil "Failed to destroy ${tap_name} adapter"
)
}
load_rc_config $name
: ${yggdrasil_enable:=no}
run_rc_command "$1"

122
contrib/macos/create-pkg.sh Executable file
View File

@@ -0,0 +1,122 @@
#!/bin/sh
# Check if xar and mkbom are available
command -v xar >/dev/null 2>&1 || (
echo "Building xar"
sudo apt-get install libxml2-dev libssl1.0-dev zlib1g-dev -y
mkdir -p /tmp/xar && cd /tmp/xar
git clone https://github.com/mackyle/xar && cd xar/xar
(sh autogen.sh && make && sudo make install) || (echo "Failed to build xar"; exit 1)
)
command -v mkbom >/dev/null 2>&1 || (
echo "Building mkbom"
mkdir -p /tmp/mkbom && cd /tmp/mkbom
git clone https://github.com/hogliux/bomutils && cd bomutils
sudo make install || (echo "Failed to build mkbom"; exit 1)
)
# Check if we can find the files we need - they should
# exist if you are running this script from the root of
# the yggdrasil-go repo and you have ran ./build
test -f yggdrasil || (echo "yggdrasil binary not found"; exit 1)
test -f yggdrasilctl || (echo "yggdrasilctl binary not found"; exit 1)
test -f contrib/macos/yggdrasil.plist || (echo "contrib/macos/yggdrasil.plist not found"; exit 1)
test -f contrib/semver/version.sh || (echo "contrib/semver/version.sh not found"; exit 1)
# Delete the pkgbuild folder if it already exists
test -d pkgbuild && rm -rf pkgbuild
# Create our folder structure
mkdir -p pkgbuild/scripts
mkdir -p pkgbuild/flat/base.pkg
mkdir -p pkgbuild/flat/Resources/en.lproj
mkdir -p pkgbuild/root/usr/local/bin
mkdir -p pkgbuild/root/Library/LaunchDaemons
# Copy package contents into the pkgbuild root
cp yggdrasil pkgbuild/root/usr/local/bin
cp yggdrasilctl pkgbuild/root/usr/local/bin
cp contrib/macos/yggdrasil.plist pkgbuild/root/Library/LaunchDaemons
# Create the postinstall script
cat > pkgbuild/scripts/postinstall << EOF
#!/bin/sh
# Normalise the config if it exists, generate it if it doesn't
if [ -f /etc/yggdrasil.conf ];
then
mkdir -p /Library/Preferences/Yggdrasil
echo "Backing up configuration file to /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d`"
cp /etc/yggdrasil.conf /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d`
echo "Normalising /etc/yggdrasil.conf"
/usr/local/bin/yggdrasil -useconffile /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d` -normaliseconf > /etc/yggdrasil.conf
else
/usr/local/bin/yggdrasil -genconf > /etc/yggdrasil.conf
fi
# Unload existing Yggdrasil launchd service, if possible
test -f /Library/LaunchDaemons/yggdrasil.plist && (launchctl unload /Library/LaunchDaemons/yggdrasil.plist || true)
# Load Yggdrasil launchd service and start Yggdrasil
launchctl load /Library/LaunchDaemons/yggdrasil.plist
EOF
# Set execution permissions
chmod +x pkgbuild/scripts/postinstall
chmod +x pkgbuild/root/usr/local/bin/yggdrasil
chmod +x pkgbuild/root/usr/local/bin/yggdrasilctl
# Pack payload and scripts
( cd pkgbuild/scripts && find . | cpio -o --format odc --owner 0:80 | gzip -c ) > pkgbuild/flat/base.pkg/Scripts
( cd pkgbuild/root && find . | cpio -o --format odc --owner 0:80 | gzip -c ) > pkgbuild/flat/base.pkg/Payload
# Work out metadata for the package info
PKGNAME=$(sh contrib/semver/name.sh)
PKGVERSION=$(sh contrib/semver/version.sh --bare)
PKGARCH=${PKGARCH-amd64}
PAYLOADSIZE=$(( $(wc -c pkgbuild/flat/base.pkg/Payload | awk '{ print $1 }') / 1024 ))
# Create the PackageInfo file
cat > pkgbuild/flat/base.pkg/PackageInfo << EOF
<pkg-info format-version="2" identifier="io.github.yggdrasil-network.pkg" version="${PKGVERSION}" install-location="/" auth="root">
<payload installKBytes="${PAYLOADSIZE}" numberOfFiles="3"/>
<scripts>
<postinstall file="./postinstall"/>
</scripts>
</pkg-info>
EOF
# Create the BOM
( cd pkgbuild && mkbom root flat/base.pkg/Bom )
# Create the Distribution file
cat > pkgbuild/flat/Distribution << EOF
<?xml version="1.0" encoding="utf-8"?>
<installer-script minSpecVersion="1.000000" authoringTool="com.apple.PackageMaker" authoringToolVersion="3.0.3" authoringToolBuild="174">
<title>Yggdrasil (${PKGNAME}-${PKGVERSION})</title>
<options customize="never" allow-external-scripts="no"/>
<domains enable_anywhere="true"/>
<installation-check script="pm_install_check();"/>
<script>
function pm_install_check() {
if(!(system.compareVersions(system.version.ProductVersion,'10.10') >= 0)) {
my.result.title = 'Failure';
my.result.message = 'You need at least Mac OS X 10.10 to install Yggdrasil.';
my.result.type = 'Fatal';
return false;
}
return true;
}
</script>
<choices-outline>
<line choice="choice1"/>
</choices-outline>
<choice id="choice1" title="base">
<pkg-ref id="io.github.yggdrasil-network.pkg"/>
</choice>
<pkg-ref id="io.github.yggdrasil-network.pkg" installKBytes="${PAYLOADSIZE}" version="${VERSION}" auth="Root">#base.pkg</pkg-ref>
</installer-script>
EOF
# Finally pack the .pkg
( cd pkgbuild/flat && xar --compression none -cf "../../${PKGNAME}-${PKGVERSION}-macos-${PKGARCH}.pkg" * )

View File

@@ -8,7 +8,7 @@
<array>
<string>sh</string>
<string>-c</string>
<string>/usr/bin/yggdrasil -useconf &lt; /etc/yggdrasil.conf</string>
<string>/usr/local/bin/yggdrasil -useconf &lt; /etc/yggdrasil.conf</string>
</array>
<key>KeepAlive</key>
<true/>

View File

@@ -0,0 +1,47 @@
Name: yggdrasil
Version: 0.3.0
Release: 1%{?dist}
Summary: End-to-end encrypted IPv6 networking
License: GPLv3
URL: https://yggdrasil-network.github.io
Source0: https://codeload.github.com/yggdrasil-network/yggdrasil-go/tar.gz/v0.3.0
%{?systemd_requires}
BuildRequires: systemd golang >= 1.11
%description
Yggdrasil is a proof-of-concept to explore a wholly different approach to
network routing. Whereas current computer networks depend heavily on very
centralised design and configuration, Yggdrasil breaks this mould by making
use of a global spanning tree to form a scalable IPv6 encrypted mesh network.
%prep
%setup -qn yggdrasil-go-%{version}
%build
./build -t -l "-linkmode=external"
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/%{_bindir}
mkdir -p %{buildroot}/%{_sysconfdir}/systemd/system
install -m 0755 yggdrasil %{buildroot}/%{_bindir}/yggdrasil
install -m 0755 yggdrasilctl %{buildroot}/%{_bindir}/yggdrasilctl
install -m 0755 contrib/systemd/yggdrasil.service %{buildroot}/%{_sysconfdir}/systemd/system/yggdrasil.service
install -m 0755 contrib/systemd/yggdrasil-resume.service %{buildroot}/%{_sysconfdir}/systemd/system/yggdrasil-resume.service
%files
%{_bindir}/yggdrasil
%{_bindir}/yggdrasilctl
%{_sysconfdir}/systemd/system/yggdrasil.service
%{_sysconfdir}/systemd/system/yggdrasil-resume.service
%post
%systemd_post yggdrasil.service
%preun
%systemd_preun yggdrasil.service
%postun
%systemd_postun_with_restart yggdrasil.service

View File

@@ -1,7 +1,16 @@
#!/bin/sh
# Get the branch name, removing any "/" characters from pull requests
BRANCH=$(git symbolic-ref --short HEAD | tr -d "/" 2>/dev/null)
# Get the current branch name
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
# Complain if the git history is not available
if [ $? != 0 ]; then
printf "unknown"
exit 1
fi
# Remove "/" characters from the branch name if present
BRANCH=$(echo $BRANCH | tr -d "/")
# Check if the branch name is not master
if [ "$BRANCH" = "master" ]; then

View File

@@ -1,26 +1,62 @@
#!/bin/sh
# Get the last tag
TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*" 2>/dev/null)
# Merge commits from this branch are counted
DEVELOPBRANCH="yggdrasil-network/develop"
# Get the number of commits from the last tag
COUNT=$(git rev-list $TAG..HEAD --count 2>/dev/null)
# Get the last tag
TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*\.0" 2>/dev/null)
# Get last merge to master
MERGE=$(git rev-list $TAG..master --grep "from $DEVELOPBRANCH" 2>/dev/null | head -n 1)
# Get the number of merges since the last merge to master
PATCH=$(git rev-list $TAG..master --count --merges --grep="from $DEVELOPBRANCH" 2>/dev/null)
# Decide whether we should prepend the version with "v" - the default is that
# we do because we use it in git tags, but we might not always need it
PREPEND="v"
if [ "$1" = "--bare" ]; then
PREPEND=""
fi
# If it fails then there's no last tag - go from the first commit
if [ $? != 0 ]; then
COUNT=$(git rev-list HEAD --count 2>/dev/null)
PATCH=$(git rev-list HEAD --count 2>/dev/null)
printf 'v0.0.%d' "$COUNT"
exit -1
# Complain if the git history is not available
if [ $? != 0 ]; then
printf 'unknown'
exit 1
fi
printf '%s0.0.%d' "$PREPEND" "$PATCH"
exit 1
fi
# Get the number of merges on the current branch since the last tag
BUILD=$(git rev-list $TAG..HEAD --count --merges)
# Split out into major, minor and patch numbers
MAJOR=$(echo $TAG | cut -c 2- | cut -d "." -f 1)
MINOR=$(echo $TAG | cut -c 2- | cut -d "." -f 2)
# Get the current checked out branch
BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Output in the desired format
if [ $COUNT = 0 ]; then
printf 'v%d.%d' "$MAJOR" "$MINOR"
if [ $PATCH = 0 ]; then
if [ ! -z $FULL ]; then
printf '%s%d.%d.0' "$PREPEND" "$MAJOR" "$MINOR"
else
printf '%s%d.%d' "$PREPEND" "$MAJOR" "$MINOR"
fi
else
printf 'v%d.%d.%d' "$MAJOR" "$MINOR" "$COUNT"
printf '%s%d.%d.%d' "$PREPEND" "$MAJOR" "$MINOR" "$PATCH"
fi
# Add the build tag on non-master branches
if [ $BRANCH != "master" ]; then
if [ $BUILD != 0 ]; then
printf -- "-%04d" "$BUILD"
fi
fi

View File

@@ -1,188 +0,0 @@
# Yggdrasil-go
## What is it?
This is a toy implementation of an encrypted IPv6 network.
A number of years ago, I started to spend some of my free time studying and routing schemes, and eventually decided that it made sense to come up with my own.
After much time spent reflecting on the problem, and a few failed starts, I eventually cobbled together one that seemed to have, more or less, the performance characteristics I was looking for.
I resolved to eventually write a proof-of-principle / test implementation, and I thought it would make sense to include many of the nice bells and whistles that I've grown accustomed to from using [cjdns](https://github.com/cjdelisle/cjdns), plus a few additional features that I wanted to test.
Fast forward through a couple years of procrastination, and I've finally started working on it in my limited spare time.
I've found that it's now marginally more interesting than embarrassing, so here it is.
The routing scheme was designed for scalable name-independent routing on graphs with an internet-like topology.
By internet-like, I mean that the network has a densely connected core with many triangles, a diameter that increases slowly with network size, and where any sparse edges tend to be relatively tree-like, all of which appear to be common features of large graphs describing "organically" grown relationships.
By scalable name-independent routing, I mean:
1. Scalable: resource consumption should grow slowly with the size of the network.
In particular, for internet-like networks, the goal is to use only a (poly)logarithmic amount of memory, use a logarithmic amount of bandwidth per one-hop neighbor for control traffic, and to maintain low average multiplicative path stretch (introducing overhead of perhaps a few percent) that does not become worse as the network grows.
2. Name-independent: a node's identifier should be independent of network topology and state, such that a node may freely change their identifier in a static network, or keep it static under state changes in a dynamic network.
In particular, addresses are self-assigned and derived from a public key, which circumvents the use of a centralized addressing authority or public key infrastructure.
Running this code will:
1. Set up a `tun` device and assign it a Unique Local Address (ULA) in `fd00::/8`.
2. Connect to other nodes running the software.
3. Route traffic for and through other nodes.
A device's ULA is actually from `fd00::/9`, and a matching `/64` prefix is available under `fd80::/9`. This allows the node to advertise a route on its LAN, as a workaround for unsupported devices.
## Building
1. Install Go (tested on 1.9, I use [godeb](https://github.com/niemeyer/godeb)).
2. Clone this repository.
2. `./build`
It's written in Go because I felt like learning a new language, and Go seemed like an easy language to learn while still being a reasonable choice for language to prototype network code.
Note that the build script defines its own `$GOPATH`, so the build and its dependencies should be self contained.
It only works on Linux at this time, because a little code (related to the `tun` device) is platform dependent, and changing that hasn't been a high priority.
## Running
To run the program, you'll need permission to create a `tun` device and configure it using `ip`.
If you don't want to mess with capabilities for the `tun` device, then using `sudo` should work, with the usual security caveats about running a program as root.
To run with default settings:
1. `./yggdrasil --autoconf`
That will generate a new set of keys (and an IP address) each time the program is run.
The program will bind to all addresses on a random port and listen for incoming connections.
It will send announcements over IPv6 link-local multicast, and attempt to start a connection if it hears an announcement from another device.
In practice, you probably want to run this instead:
1. `./yggdrasil --genconf > conf.json`
2. `./yggdrasil --useconf < conf.json`
The first step generates a configuration file with a set of cryptographic keys and default settings.
The second step runs the program using the configuration provided in that file.
Because ULAs are derived from keys, using a fixed set of keys causes a node to keep the same address each time the program is run.
If you want to use it as an overlay network on top of e.g. the internet, then you can do so by adding the address and port of the device you want to connect to (as a string, e.g. `"1.2.3.4:5678"`) to the list of `Peers` in the configuration file.
This should accept IPv4 and IPv6 addresses, and I think it should resolve host/domain names, but I haven't really tested that, so your mileage may vary.
You can also configure which address and/or port to listen on by editing the configuration file, in case you want to bind to a specific address or listen for incoming connections on a fixed port.
Also note that the nodes is connected to the network through a `tun` device, so it follows point-to-point semantics.
This means it's limited to routing traffic with source and destination addresses in `fd00::/8`--you can't add a prefix to your routing table "via" an address in that range, as the router has no idea who you meant to send it to.
In particular, this means you can't set a working default route that *directly* uses the overlay network, but I've had success *indirectly* using it to connect to an off-the-shelf VPN that I can use as a default route for internet access.
## Optional: advertise a prefix locally
Suppose a node has been given the address: `fd00:1111:2222:3333:4444:5555:6666:7777`
Then the node may also use addresses from the prefix: `fd80:1111:2222:3333::/64` (note the `fd00` -> `fd80`, a separate `/9` is used for prefixes).
To advertise this prefix and a route to `fd00::/8`, the following seems to work for me:
1. Enable IPv6 forwarding (e.g. `sysctl -w net.ipv6.conf.all.forwarding=1` or add it to sysctl.conf).
2. `ip addr add fd80:1111:2222:3333::1/64 dev eth0` or similar, to assign an address for the router to use in that prefix, where the LAN is reachable through `eth0`.
3. Install/run `radvd` with something like the following in `/etc/radvd.conf`:
```
interface eth0
{
AdvSendAdvert on;
prefix fd80:1111:2222:3333::/64 {
AdvOnLink on;
AdvAutonomous on;
};
route fd00::/8 {};
};
```
Now any IPv6-enabled device in the LAN can use stateless address auto-configuration to assign itself a working `fd00::/8` address from the `/64` prefix, and communicate with the wider network through the router, without requiring any special configuration for each device.
I've used this to e.g. get my phone on the network.
Note that there are a some differences when accessing the network this way:
1. There are 64 fewer bits of address space available for self-certifying addresses.
This means that it is 64 bits easier to brute force a prefix collision than collision for a full node's IP address. As such, you may want to change addresses frequently, or else brute force an address with more security bits (see: `misc/genkeys.go`).
2. The LAN depends on the router for cryptography.
So while traffic going through the WAN is encrypted, the LAN is still just a LAN. You may want to secure your network.
3. Related to the above, the cryptography and I/O through the `tun` device both place additional load on the router, above what is normally present from forwarding packets between full nodes in the network, so the router may need more computing power to reach line rate.
## How does it work?
Consider the internet, which uses a network-of-networks model with address aggregation.
Addresses are allocated by a central authority, as blocks of contiguous addresses with a matching prefix.
Within a network, each node may represent one or more prefixes, with each prefix representing a network of one or more nodes.
On the largest scale, BGP is used to route traffic between networks (autonomous systems), and other protocols can be used to route within a network.
The effectiveness of such hierarchical addressing and routing strategies depend on network topology, with the internet's observed topology being the worst case of all known topologies from a scalability standpoint (see [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) for a better explanation of the issue, but the problem is essentially that address aggregation is ineffective in a network with a large number of nodes and a small diameter).
The routing scheme implemented by this code tries a different approach.
Instead of using assigned addresses and a routing table based on prefixes and address aggregation, routing and addressing are handled through a combination of:
1. Self-assigned cryptographically generated addresses, to handle address allocation without a central authority.
2. A kademlia-like distributed hash table, to look up a node's (name-dependent) routing information from their (name-independent routing) IP address.
3. A name-dependent routing scheme based on greedy routing in a metric space, constructed from an arbitrarily rooted spanning tree, which gives a reasonable approximation of the true distance between nodes for certain network topologies (namely the scale-free topology that seems to emerge in many large graphs, including the internet). The spanning tree embedding takes stability into account when selecting which one-hop neighbor to use as a parent, and path selection uses (poorly) estimated available bandwidth as a criteria, subject to the constraint that metric space distances must decrease with each hop. Incidentally, the name `yggdrasil` was selected for this test code because that's obviously what you call an immense tree that connects worlds.
The network then presents itself as having a single "flat" address with no aggregation.
Under the hood, it runs as an overlay on top of existing IP networks.
Link-local IPv6 multicast traffic is used to advertise on the underlying networks, which can as easily be a wired or wireless LAN, a direct (e.g. ethernet) connection between two devices, a wireless ad-hoc network, etc.
Additional connections can be added manually to peer over networks where link-local multicast is insufficient, which allows you to e.g. use the internet to bridge local networks.
The name-dependent routing layer uses cryptographically signed (`Ed25519`) path-vector-like routing messages, similar to S-BGP, which should prevent route poisoning and related attacks.
For encryption, it uses the Go implementation of the `nacl/box` scheme, which is built from a Curve25519 key exchange with XSalsa20 as a stream cypher and Poly1305 for integrity and authentication.
Permanent keys are used for protocol traffic, including the ephemeral key exchange, and a hash of a node's permanent public key is used to construct a node's address.
Ephemeral keys are used for encapsulated IP(v6) traffic, which provides forward secrecy.
Go's `crypto/rand` library is used for nonce generation.
In short, I've tried to not make this a complete security disaster, but the code hasn't been independently audited and I'm nothing close to a security expert, so it should be considered a proof-of-principle rather than a safe implementation.
At a minimum, I know of no way to prevent gray hole attacks.
I realize that this is a terribly short description of how it works, so I may elaborate further in another document if the need arises.
Otherwise, I guess you could try to read my terrible and poorly documented code if you want to know more.
## Related work
A lot of inspiration comes from [cjdns](https://github.com/cjdelisle/cjdns).
I'm a contributor to that project, and I wanted to test out some ideas that weren't convenient to prototype in the existing code base, which is why I wrote this toy.
On the routing side, a lot of influence came from compact routing.
A number of compact routing schemes are evaluated in [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) and may be used as a basis for comparison.
When tested in a simplified simulation environment on CAIDA's 9204-node "skitter" network graph used in that paper, I observed an average multiplicative stretch of about 1.08 with my routing scheme, as implemented here.
This can be lowered to less than 1.02 using a source-routed version of the algorithm and including node degree as an additional parameter of the embedding, which is of academic interest, but degree's unverifiability makes it impractical for this implementation.
In either case, this only requires 1 routing table entry per one-hop neighbor (this averages ~6 for in the skitter network graph), plus a logarithmic number of DHT entries (expected to be ~26, based on extrapolations from networks with a few hundred nodes--running the full implementation on the skitter graph is impractical on my machine).
I don't think stretch is really an appropriate metric, as it doesn't consider the difference to total network cost from a high-stretch short path vs a high-stretch long path.
In this scheme, and I believe in most compact routing schemes, longer paths tend to have lower multiplicative stretch, and shorter paths are more likely to have longer stretch.
I would argue that this is preferable to the alternative.
While I use a slightly different approach, the idea to try a greedy routing scheme was inspired by the use of greedy routing on networks embedded in the hyperbolic plane (such as [Kleinberg's work](https://doi.org/10.1109%2FINFCOM.2007.221) and [Greedy Forwarding on the NDN Testbed](https://www.caida.org/research/routing/greedy_forwarding_ndn/)).
I use distance on a spanning tree as the metric, as seems to work well on the types of networks I'm concerned with, and it simplifies other aspects of the implementation.
The hyperbolic embedding algorithms I'm aware of, or specifically the distributed ones, operate by constructing a spanning tree of the network and then embedding the tree.
So I don't see much harm, at present, of skipping the hyperbolic plane and directly using the tree for the metric space.
## Misc. notes
This is a toy experiment / proof-of-concept.
It's only meant to test if / how well some ideas work.
I have no idea what I'm doing, so for all I know it's entirely possible that it could crash your computer, eat your homework, or set fire to your house.
Some parts are also written to be as bad as I could make them while still being technically correct, in an effort to make bugs obvious if they occur, which means that even when it does work it may be fragile and error prone.
In particular, you should expect it to perform poorly under mobility events, and to converge slowly in dynamic networks. All else being equal, this implementation should tend to prefer long-lived links over short-lived ones when embedding, and (poorly estimated) high bandwidth links over low bandwidth ones when forwarding traffic. As such, in multi-homed or mobile scenarios, there may be some tendency for it to make decisions you disagree with.
While stretch is low on internet-like graphs, the best upper bound I've established on the *additive* stretch of this scheme, after convergence, is the same as for tree routing: proportional to network diameter. For sparse graphs with a large diameter, the scheme may not find particularly efficient paths, even under ideal circumstances. I would argue that such networks tend not to grow large enough for scalability to be an issue, so another routing scheme is better suited to those networks.
Regarding the announce-able prefix thing, what I wanted to do is use `fc00::/7`, where `fc00::/8` is for nodes and `fd00::/8` is for prefixes.
I would also possibly widen the prefixes to `/48`, to match [rfc4193](https://tools.ietf.org/html/rfc4193), and possibly provide an option to keep using a `/64` by splitting it into two `/9` blocks (where `/64` prefixes would continue to live in `fd80::/9`), or else convince myself that the security implications of another 16 bits don't matter (to avoid the complexity of splitting it into two `/9` ranges for prefixes).
Using `fc00::/8` this way would cause issues if trying to also run cjdns.
Since I like cjdns, and want the option of running it on the same nodes, I've decided not to do that.
If I ever give up on avoiding cjdns conflicts, then I may change the addressing scheme to match the above.
Despite the tree being constructed from path-vector-like routing messages, there's no support for routing policy right now.
As a result, peer relationships are bimodal: either you're not connected to someone, or you're connected and you'll route traffic *to* and *through* them.
Nodes also accept all incoming connections, so if you want to limit who can connect then you'll need to provide some other kind of access controls.
The current implementation does all of its setup when the program starts, and then nothing can be reconfigured without restarting the program.
At some point I may add a remote API, so a running node can be reconfigured (to e.g. add/remove peers) without restarting, or probe the internal state of the router to get useful debugging info.
So far, things seem to work the way I want/expect without much trouble, so I haven't felt the need to do this yet.
Some parts of the implementation can take advantage of multiple cores, but other parts that could simply do not.
Some parts are fast, but other parts are slower than they have any right to be, e.g. I can't figure out why some syscalls are as expensive as they are, so the `tun` in particular tends to be a CPU bottleneck (multi-queue could help in some cases, but that just spreads the cost around, and it doesn't help with single streams of traffic).
The Go runtime's GC tends to have short pauses, but it does have pauses.
So even if the ideas that went into this routing scheme turn out to be useful, this implementation is likely to remain mediocre at best for the foreseeable future.
If the is thing works well and the protocol stabilizes, then it's worth considering re-implementation and/or a formal spec and RFC.
In such a case, it's entirely reasonable to change parts of the spec purely to make the efficient implementation easier (e.g. it makes sense to want zero-copy networking, but a couple parts of the current protocol might make that impractical).

View File

@@ -2,133 +2,147 @@
Note: This is a very rough early draft.
Yggdrasil is a routing protocol designed for scalable name-independent routing on internet-like graphs.
The design is built around a name-dependent routing scheme which uses distance on a spanning tree as a metric for greedy routing, and a kademlia-like distributed hash table to facilitate lookups of metric space routing information from static cryptographically generated identifiers.
This approach can find routes on any network, as it reduces to spanning tree routing in the worst case, but is observed to be particularly efficient on internet-like graphs.
In an effort to mitigate many forms of attacks, the routing scheme only uses information which is either cryptographically verifiable or based on observed local network state.
The implementation is distributed and runs on dynamic graphs, though this implementation may not converge quickly enough to be practical on networks with high node mobility.
This document attempts to give a rough overview of how some of the key parts of the protocol are implemented, as well as an explanation of why a few subtle points are handled the way they are.
Yggdrasil is an encrypted IPv6 network running in the [`200::/7` address range](https://en.wikipedia.org/wiki/Unique_local_address).
It is an experimental/toy network, so failure is acceptable, as long as it's instructive to see how it breaks if/when everything falls apart.
IP addresses are derived from cryptographic keys, to reduce the need for public key infrastructure.
A form of locator/identifier separation (similar in goal to [LISP](https://en.wikipedia.org/wiki/Locator/Identifier_Separation_Protocol)) is used to map static identifiers (IP addresses) onto dynamic routing information (locators), using a [distributed hash table](https://en.wikipedia.org/wiki/Distributed_hash_table) (DHT).
Locators are used to approximate the distance between nodes in the network, where the approximate distance is the length of a real worst-case-scenario path through the network.
This is (arguably) easier to secure and requires less information about the network than commonly used routing schemes.
While not technically a [compact routing scheme](https://arxiv.org/abs/0708.2309), tests on real-world networks suggest that routing in this style incurs stretch comparable to the name-dependent compact routing schemes designed for static networks.
Compared to compact routing schemes, Yggdrasil appears to have smaller average routing table sizes, works on dynamic networks, and is name-independent.
It currently lacks the provable bounds of compact routing schemes, and there's a serious argument to be made that it cheats by stretching the definition of some of the above terms, but the main point to be emphasized is that there are trade-offs between different concerns when trying to route traffic, and we'd rather make every part *good* than try to make any one part *perfect*.
In that sense, Yggdrasil seems to be competitive, on what are supposedly realistic networks, with compact routing schemes.
## Addressing
Addresses in Yggdrasil are derived from a truncated version of a `NodeID`.
The `NodeID` itself is a sha512sum of a node's permanent public Curve25519 key.
Each node's IPv6 address is then assigned from the lower half of the `fd00::/8` prefix using the following approach:
Yggdrasil uses a truncated version of a `NodeID` to assign addresses.
An address is assigned from the `200::/7` prefix, according to the following:
1. Begin with `0xfd` as the first byte of the address.
1. Begin with `0x02` as the first byte of the address, or `0x03` if it's a `/64` prefix.
2. Count the number of leading `1` bits in the NodeID.
3. Set the second byte of the address to the number of leading `1` bits, subject to the constraint that this is still in the lower half of the address range (it is unlikely that a node will have 128 or more leading `1` bits in a sha512sum hash, for the foreseeable future).
3. Set the second byte of the address to the number of leading `1` bits in the NodeID (8 bit unsigned integer, at most 255).
4. Append the NodeID to the remaining bits of the address, truncating the leading `1` bits and the first `0` bit, to a total address size of 128 bits.
The last bit of the first byte is used to flag if an address is for a router (`200::/8`), or part of an advertised prefix (`300::/8`), where each router owns a `/64` that matches their address (except with the eight bit set to 1 instead of 0).
This allows the prefix to be advertised to the router's LAN, so unsupported devices can still connect to the network (e.g. network printers).
The NodeID is a [sha512sum](https://en.wikipedia.org/wiki/SHA-512) of a node's public encryption key.
Addresses are checked that they match NodeID, to prevent address spoofing.
As such, while a 128 bit IPv6 address is likely too short to be considered secure by cryptographer standards, there is a significant cost in attempting to cause an address collision.
Addresses can be made more secure by brute force generating a large number of leading `1` bits in the NodeID.
When connecting to a node, the IP address is unpacked into the known bits of the NodeID and a matching bitmask to track which bits are significant.
A node is only communicated with if its `NodeID` matches its public key and the known `NodeID` bits from the address.
It is important to note that only `NodeID` is used internally for routing, so the addressing scheme could in theory be changed without breaking compatibility with intermediate routers.
This may become useful if the IPv6 address range ever needs to be changed, or if a new addressing format that allows for more significant bits is ever implemented by the OS.
This has been done once, when moving the address range from the `fd00::/8` ULA range to the reserved-but-[deprecated](https://tools.ietf.org/html/rfc4048) `200::/7` range.
Further addressing scheme changes could occur if, for example, an IPv7 format ever emerges.
### Cryptography
Public key encryption is done using the `golang.org/x/crypto/nacl/box`, which uses Curve25519, XSalsa20, and Poly1305 for key exchange, encryption, and authentication.
Public key encryption is done using the `golang.org/x/crypto/nacl/box`, which uses [Curve25519](https://en.wikipedia.org/wiki/Curve25519), [XSalsa20](https://en.wikipedia.org/wiki/Salsa20), and [Poly1305](https://en.wikipedia.org/wiki/Poly1305) for key exchange, encryption, and authentication (interoperable with [NaCl](https://en.wikipedia.org/wiki/NaCl_(software))).
Permanent keys are used only for protocol traffic, with random nonces generated on a per-packet basis using `crypto/rand` from Go's standard library.
Ephemeral session keys are generated for encapsulated IPv6 traffic, using the same set of primitives, with random initial nonces that are subsequently incremented.
Ephemeral session keys (for [forward secrecy](https://en.wikipedia.org/wiki/Forward_secrecy)) are generated for encapsulated IPv6 traffic, using the same set of primitives, with random initial nonces that are subsequently incremented.
A list of recently received session nonces is kept (as a bitmask) and checked to reject duplicated packets, in an effort to block duplicate packets and replay attacks.
A separate private key is generated and used for signing with Ed25519, which is used by the name-dependent routing layer to secure construction of the spanning tree, with a TreeID hash of a node's public Ed key being used to select the highest TreeID as the root of the tree.
A separate set of keys are generated and used for signing with [Ed25519](https://en.wikipedia.org/wiki/Ed25519), which is used by the routing layer to secure construction of a spanning tree.
### Prefixes
Recall that each node's address is in the lower half of the address range, I.e. `fd00::/9`. A `/64` prefix is made available to each node under `fd80::/9`, where the remaining bits of the prefix match the node's address under `fd00::/9`.
Recall that each node's address is in the lower half of the address range, I.e. `200::/8`. A `/64` prefix is made available to each node under `300::/8`, where the remaining bits of the prefix match the node's address under `200::/8`.
A node may optionally advertise a prefix on their local area network, which allows unsupported or legacy devices with IPv6 support to connect to the network.
Note that there are 64 fewer bits of `NodeID` available to check in each address from a routing prefix, so it makes sense to brute force a `NodeID` with more significant bits in the address if this approach is to be used.
Running `genkeys.go` will do this by default.
## Name-independent routing
## Locators and Routing
A distributed hash table is used to facilitate the lookup of a node's name-dependent routing `coords` from a `NodeID`.
A kademlia-like peer structure and xor metric are used in the DHT layout, but only peering info is used--there is no key:value store.
In contrast with standard kademlia, instead of using iterative parallel lookups, a recursive lookup strategy is used.
This is an intentional design decision to make the DHT more fragile--the intent is for DHT inconsistencies to lead to lookup failures, because of concerns that the iterative parallel approach may hide DHT bugs.
Locators are generated using information from a spanning tree (described below).
The result is that each node has a set of [coordinates in a greedy metric space](https://en.wikipedia.org/wiki/Greedy_embedding).
These coordinates are used as a distance label.
Given the coordinates of any two nodes, it is possible to calculate the length of some real path through the network between the two nodes.
In particular, the DHT is bootstrapped off of a node's one-hop neighbors, and I've observed that this causes a standard kademlia implementation to diverge in the general case.
To get around this, buckets are updated more aggressively, and the least recently pinged node from each bucket is flushed to make room for new nodes as soon as a response is heard from them.
This appears to fix the bootstrapping issues on all networks where they had been observed in testing, but recursive lookups are kept for the time being to continue monitoring the issue.
However, recursive lookups require fewer round trips, so they are expected to be lower latency.
As such, even if a switch to iterative parallel lookups was made, the recursive lookup functionality may be kept and used optimistically to minimize handshake time in stable networks.
Traffic is forwarded using a [greedy routing](https://en.wikipedia.org/wiki/Small-world_routing#Greedy_routing) scheme, where each node forwards the packet to a one-hop neighbor that is closer to the destination (according to this distance metric) than the current node.
In particular, when a packet needs to be forward, a node will forward it to whatever peer is closest to the destination in the greedy [metric space](https://en.wikipedia.org/wiki/Metric_space) used by the network, provided that the peer is closer to the destination than the current node.
Other than these differences, the DHT is more-or-less what you might expect from a kad implementation.
If no closer peers are idle, then the packet is queued in FIFO order, with separate queues per destination coords (currently, as a bit of a hack, IPv6 flow labels are embedeed after the end of the significant part of the coords, so queues distinguish between different traffic streams with the same destination).
Whenever the node finishes forwarding a packet to a peer, it checks the queues, and will forward the first packet from the queue with the maximum `<age of first packet>/<queue size in bytes>`, i.e. the bandwidth the queue is attempting to use, subject to the constraint that the peer is a valid next hop (i.e. closer to the destination than the current node).
If no non-empty queue is available, then the peer is added to the idle set, forward packets when the need arises.
## Name-dependent routing
This acts as a crude approximation of backpressure routing, where the remote queue sizes are assumed to be equal to the distance of a node from a destination (rather than communicating queue size information), and packets are never forwarded "backwards" through the network, but congestion on a local link is routed around when possible.
The queue selection strategy behaves similar to shortest-queue-first, in that a larger fration of available bandwith to sessions that attempt to use less bandwidth, and is loosely based on the rationale behind some proposed solutions to the [cake-cutting](https://en.wikipedia.org/wiki/Fair_cake-cutting) problem.
A spanning tree is constructed and used for name-dependent routing.
The basic idea is to use the distance between nodes *on the tree* as a distance metric, and then perform greedy routing in that metric space.
As the tree is constructed from a subset of the real links in the network, this distance metric (unlike the DHT's xor metric) has some relationship with the underlying physical network.
In the worst case, greedy routing with this metric reduces to routing on the spanning tree, which should be comparable to ethernet.
However, greedy routing can use any link, provided that the node on the other end of the link is closer to the destination, so this allows the use of off-tree shortcuts, with the possibility and effectiveness of this being topology dependent.
The main assumption that Yggdrasil's performance hinges on, is that this distance metric is close to real network distance, on average, in realistic networks.
The name dependent scheme is implemented in roughly the following way:
1. Each node generates a set of Ed25519 keys for signing routing messages, with a `TreeID` defined as the sha512sum of a node's public signing key.
2. If a node doesn't know a better (higher `TreeID`) root for the tree, then it makes itself the root of its own tree.
3. Nodes periodically send announcement messages to neighbors, which specify a sequence number for that node's current locator in the tree.
4. When a node A sees an unrecognized sequence number from a neighbor B, then A asks B to send them a locator.
5. This locator is sent in the form of a path from the root, through B, and ending at A.
6. Each hop in the path includes the public signing key of the next hop, and a signature for the full path from the root to the next hop, to prevent forgery of path information (similar to S-BGP).
7. The first hop, from the root, includes a signed sequence number which must increase (implemented as a unix timestamp, for convenience), which is used to detect root timeouts and prevent replays.
The highest `TreeID` approach to root selection is just to ensure that nodes select the same root, otherwise distance calculations wouldn't work.
Root selection has a minor effect on the stretch of the paths selected by the network, but this effect was seen to be small compared to the minimum stretch, for nearly all choices of root.
The current implementation tracks how long a neighbor has been advertising a locator for the same path, and it prefers to select a parent with a stable locator and a short distance to the root (maximize uptime/distance).
When forwarding traffic, the next hop is selected taking bandwidth to the next hop and distance to the destination into account (maximize bandwidth/distance), subject to the requirement that distance must always decrease.
The bandwidth estimation isn't very good, but it correlates well enough that e.g. when a slow wifi and a fast ethernet link to the same node are available, it typically uses the ethernet link.
However, if the ethernet link comes up while the wifi link is under heavy use, then it tends to keep using the wifi link until things settle down, and only switches to ethernet after the wireless link is no longer overloaded.
A better approach to bandwidth estimation could probably switch to the new link faster.
The queue size is limited to 4 MB. If a packet is added to a queue and the total size of all queues is larger than this threshold, then a random queue is selected (with odds proportional to relative queue sizes), and the first packet from that queue is dropped, with the process repeated until the total queue size drops below the allowed threshold.
Note that this forwarding procedure generalizes to nodes that are not one-hop neighbors, but the current implementation omits the use of more distant neighbors, as this is expected to be a minor optimization (it would add per-link control traffic to pass path-vector-like information about a subset of the network, which is a lot of overhead compared to the current setup).
## Other implementation details
### Spanning Tree
In case you hadn't noticed, this implementation is written in Go.
That decision was made because the designer and initial author (@Arceliar) felt like learning a new language when the implementation was started, and the Go language seemed like an OK choice for prototyping a network application.
While Go's GC pauses are small, they do exist, so this implementation probably isn't suited to applications that require very low latency and jitter.
A [spanning tree](https://en.wikipedia.org/wiki/Spanning_tree) is constructed with the tree rooted at the highest TreeID, where TreeID is equal to a sha512sum of a node's public [Ed25519](https://en.wikipedia.org/wiki/Ed25519) key (used for signing).
A node sends periodic advertisement messages to each neighbor.
The advertisement contains the coords that match the path from the root through the node, plus one additional hop from the node to the neighbor being advertised to.
Each hop in this advertisement includes a matching ed25519 signature.
These signatures prevent nodes from forging arbitrary routing advertisements.
Aside from that, an effort was made to write each part of it to be as "bad" (i.e. fragile) as could be managed while still being technically correct.
That's a decision made for debugging purposes: the intent is to make any bugs as obvious as possible, so they can more easily be found and fixed in a small or simulated network.
The first hop, from the root, also includes a sequence number, which must be updated periodically.
A node will blacklist the current root (keeping a record of the last sequence number observed) if the root fails to update for longer than some timeout (currently hard coded at 1 minute).
Normally, a root node will update their sequence number for frequently than this (once every 30 seconds).
Nodes are throttled to ignore updates with a new sequence number for some period after updating their most recently seen sequence number (currently this cooldown is 15 seconds).
The implementation chooses to set the sequence number equal to the unix time on the root's clock, so that a new (higher) sequence number will be selected if the root is restarted and the clock is not set back.
This implementation runs as an overlay network on top of regular IPv4 or IPv6 traffic.
It uses link-local IPv6 multicast traffic to automatically connect to devices on the same network, but it can also be fed a list of address:port pairs to connect to.
This can be used to e.g. set up two local networks and bridge them over the internet.
Other than the root node, every other node in the network must select one of its neighbors to use as their parent.
This selection is done by tracking when each neighbor first sends us a message with a new timestamp from the root, to determine the ordering of the latency of each path from the root, to each neighbor, and then to the node that's searching for a parent.
These relative latencies are tracked by, for each neighbor, keeping a score vs each other neighbor.
If a neighbor sends a message with an updated timestamp before another neighbor, then the faster neighbor's score is increased by 1.
If the neighbor sends a message slower, then the score is decreased by 2, to make sure that a node must be reliably faster (at least 2/3 of the time) to see a net score increase over time.
If a node begins to advertise new coordinates, then its score vs all other nodes is reset to 0.
A node switches to a new parent if a neighbor's score (vs the current parent) reaches some threshold, currently 240, which corresponds to about 2 hours of being a reliably faster path.
The intended outcome of this process is that stable connections from fixed infrastructure near the "core" of the network should (eventually) select parents that minimize latency from the root to themselves, while the more dynamic parts of the network, presumably more towards the edges, will try to favor reliability when selecting a parent.
## Performance
The distance metric between nodes is simply the distance between the nodes if they routed on the spanning tree.
This is equal to the sum of the distance from each node to the last common ancestor of the two nodes being compared.
The locator then consists of a root's key, timestamp, and coordinates representing each hop in the path from the root to the node.
In practice, only the coords are used for routing, while the root and timestamp, along with all the per-hop signatures, are needed to securely construct the spanning tree.
This section compares Yggdrasil with the results in [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) (specifically table 1) from tests on the 9204-node [skitter](https://www.caida.org/tools/measurement/skitter/) network maps from [caida](https://www.caida.org/).
## Name-independent routing
A [simplified version](misc/sim/treesim-forward.py) of this routing scheme was written (long before the Yggdrasil implementation was started), and tested for comparison with the results from the above paper.
This version includes only the name-dependent part of the routing scheme, but the overhead of the name-independent portion is easy enough to check with the full implementation.
In summary:
A [Chord](https://en.wikipedia.org/wiki/Chord_(peer-to-peer))-like Distributed Hash Table (DHT) is used as a distributed database that maps NodeIDs onto coordinates in the spanning tree metric space.
The DHT is Chord-like in that it uses a successor/predecessor structure to do lookups in `O(n)` time with `O(1)` entries, then augments this with some additional information, adding roughly `O(logn)` additional entries, to reduce the lookup time to something around `O(logn)`.
In the long term, the idea is to favor spending our bandwidth making sure the minimum `O(1)` part is right, to prioritize correctness, and then try to conserve bandwidth (and power) by being a bit lazy about checking the remaining `O(logn)` portion when it's not in use.
1. Multiplicative stretch is approximately 1.08 with Yggdrasil, using unweighted links undirected links, as in the paper.
2. A modified version can get this as low as 1.01, but it depends on knowing the degree of each one-hop neighbor, which it is not obviously possible to cryptographically secure, and it requires using source routing to find a path from A to B and from B to A, and then have both nodes use whichever path was observed to be shorter.
3. In either case, approximately 6 routing table entries are needed, on average, for the name-dependent routing scheme, where each node needs one routing table entry per one-hop neighbor.
4. Approximately 30 DHT entries are needed to facilitate name-independent routing.
This requires a lookup and caches the results, so old information needs to time out to work on dynamic networks.
The schemes it's being compared to only work on static networks, where a similar approach would be fine, so this seems like a reasonably fair comparison.
The stretch of that initial lookup can be *very* high, but it's only for a couple of round trips to look up keys and then do the ephemeral key exchange, so this may be an acceptable tradeoff (it's probably more expensive than a DNS lookup, but is similar in principle and effect).
5. Both the name-dependent and name-independent routing table entries are of a size proportional to the length of the path between the root and the node, which is at most the diameter of the network after things have fully converged, but name-dependent routing table entries tend to be much larger in practice due to the size of cryptographic signatures (64 bytes for a signature + 32 for the signing key).
6. The name-dependent routing scheme only sends messages about one-hop neighbors on the link between those neighbors, so if you measure things by per *link* overhead instead of per *node*, then this doesn't seem so bad to me.
7. The name-independent routing scheme scales like a DHT running as an overlay on top of the router-level topology, so the per-link and per-node overhead are going to be topology dependent.
This hasn't been studied in a lot of detail, but for realistic topologies, where yggdrasil routing seems to approximate shortest path routing, academic research has shown that shortest path routing does not lead to congestion.
To be specific, the DHT stores the immediate successor of a node, plus the next node it manages to find which is strictly closer (by the tree hop-count metric) than all previous nodes.
The same process is repeated for predecessor nodes, and lookups walk the network in the predecessor direction, with each key being owned by its successor (to make sure defaulting to 0 for unknown bits of a `NodeID` doesn't cause us to overshoot the target during a lookup).
In addition, all of a node's one-hop neighbors are included in the DHT, since we get this information "for free", and we must include it in our DHT to ensure that the network doesn't diverge to a broken state (though I suspect that only adding parents or parent-child relationships may be sufficient -- worth trying to prove or disprove, if somebody's bored).
The DHT differs from Chord in that there are no values in the key:value store -- it only stores information about DHT peers -- and that it uses a [Kademlia](https://en.wikipedia.org/wiki/Kademlia)-inspired iterative-parallel lookup process.
The designer (@Arceliar) believes that the main reason Yggdrasil performs so well is because it stores information about all one-hop neighbors.
Consider that, if Yggdrasil did not maintain state about all one-hop neighbors, but the protocol still had the ability to forward to all of them through some mechanism (i.e. source routing), then the OS still needs a way to forward traffic to them.
In most cases, this would require some form of per-neighbor state to be stored by the OS, either because there's one dedicated interface per peer or because there are entries in an arp/NDP table to reach multiple devices over a shared switch.
So while compact routing schemes have nice theoretical limits, which do not require even as much state as one entry per one-hop neighbor, that property does not seem realistic if the implementation is running at the router level (as opposed to the AS level).
As such, keeping one entry per neighbor may be reasonable, especially if nodes with a high degree have proportionally more resources available to them, but it is possible that something may have been overlooked in the design.
To summarize the entire routing procedure, when given only a node's IP address, the goal is to find a route to the destination.
That happens through 3 steps:
## Disclaimer
1. The address is unpacked into the known bits of a NodeID and a bitmask to signal which bits of the NodeID are known (the unknown bits are ignored).
2. A DHT search is performed, which normally results in a response from the node closest in the DHT keyspace to the target `NodeID`. The response contains the node's curve25519 public key, which is checked to match the `NodeID` (and therefore the address), as well as the node's coordinates.
3. Using the keys and coords from the above step, an ephemeral key exchange occurs between the source and destination nodes. These ephemeral session keys are used to encrypt any ordinary IPv6 traffic that may be encapsulated and sent between the nodes.
From that point, the session keys and coords are cached and used to encrypt and send traffic between nodes. This is *mostly* transparent to the user: the initial DHT lookup and key exchange takes at least 2 round trips, so there's some delay before session setup completes and normal IPv6 traffic can flow. This is similar to the delay caused by a DNS lookup, although it generally takes longer, as a DHT lookup requires multiple iterations to reach the destination.
## Project Status and Plans
The current (Go) implementation is considered alpha, so compatibility with future versions is neither guaranteed nor expected.
While users are discouraged from running anything truly critical on top of it, as of writing, it seems reliable enough for day-to-day use.
As an "alpha" quality release, Yggdrasil *should* at least be able to detect incompatible versions when it sees them, and warn the users that an update may be needed.
A "beta" quality release should know enough to be compatible in the face of wire format changes, and reasonably feature complete.
A "stable" 1.0 release, if it ever happens, would probably be feature complete, with no expectation of future wire format changes, and free of known critical bugs.
Roughly speaking, there are a few obvious ways the project could turn out:
1. The developers could lose interest before it goes anywhere.
2. The project could be reasonably complete (beta or stable), but never gain a significant number of users.
3. The network may grow large enough that fundamental (non-fixable) design problems appear, which is hopefully a learning experience, but the project may die as a result.
4. The network may grow large, but never hit any design problems, in which case we need to think about either moving the important parts into other projects ([cjdns](https://github.com/cjdelisle/cjdns)) or rewriting compatible implementations that are better optimized for the target platforms (e.g. a linux kernel module).
That last one is probably impossible, because the speed of light would *eventually* become a problem, for a sufficiently large network.
If the only thing limiting network growth turns out to be the underlying physics, then that arguably counts as a win.
Also, note that some design decisions were made for ease-of-programming or ease-of-testing reasons, and likely need to be reconsidered at some point.
In particular, Yggdrasil currently uses TCP for connections with one-hop neighbors, which introduces an additional layer of buffering that can lead to increased and/or unstable latency in congested areas of the network.
This is a draft version of documentation for a work-in-progress protocol.
The design and implementation should be considered pre-alpha, with any and all aspects subject to change in light of ongoing R&D.
It is possible that this document and the code base may fall out of sync with eachother.
Some details that are known to be likely to change, packet formats in particular, have been omitted.

14
go.mod Normal file
View File

@@ -0,0 +1,14 @@
module github.com/yggdrasil-network/yggdrasil-go
require (
github.com/docker/libcontainer v2.2.1+incompatible
github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0
github.com/mitchellh/mapstructure v1.1.2
github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/net v0.0.0-20181207154023-610586996380
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e
golang.org/x/text v0.3.0
)

20
go.sum Normal file
View File

@@ -0,0 +1,20 @@
github.com/docker/libcontainer v2.2.1+incompatible h1:++SbbkCw+X8vAd4j2gOCzZ2Nn7s2xFALTf7LZKmM1/0=
github.com/docker/libcontainer v2.2.1+incompatible/go.mod h1:osvj61pYsqhNCMLGX31xr7klUBhHb/ZBuXS0o1Fvwbw=
github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0 h1:YnZmFjg0Nvk8851WTVWlqMC1ecJH07Ctz+Ezxx4u54g=
github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0/go.mod h1:rUi0/YffDo1oXBOGn1KRq7Fr07LX48XEBecQnmwjsAo=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165 h1:Oo7Yfu5lEQLGvvh2p9Z8FRHJIsl7fdOCK9xXFNBkqmQ=
github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165/go.mod h1:l+Zao6IpQ+6d/y7LnYnOfbfOeU/9xRiTi4HLVpnkcTg=
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w=
github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY=
github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae h1:MYCANF1kehCG6x6G+/9txLfq6n3lS5Vp0Mxn1hdiBAc=
github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae/go.mod h1:R0SBCsugm+Sf1katgTb2t7GXMm+nRIv43tM4VDZbaOs=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0=
golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo=
golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -16,7 +16,7 @@ import "encoding/hex"
import "flag"
import "fmt"
import "runtime"
import . "yggdrasil"
import . "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
var doSig = flag.Bool("sig", false, "generate new signing keys instead")

View File

@@ -51,12 +51,12 @@ ip netns exec node4 ip link set lo up
ip netns exec node5 ip link set lo up
ip netns exec node6 ip link set lo up
ip netns exec node1 ./run --autoconf --pprof &> /dev/null &
ip netns exec node2 ./run --autoconf --pprof &> /dev/null &
ip netns exec node3 ./run --autoconf --pprof &> /dev/null &
ip netns exec node4 ./run --autoconf --pprof &> /dev/null &
ip netns exec node5 ./run --autoconf --pprof &> /dev/null &
ip netns exec node6 ./run --autoconf --pprof &> /dev/null &
ip netns exec node1 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
ip netns exec node2 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
ip netns exec node3 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
ip netns exec node4 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
ip netns exec node5 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
ip netns exec node6 env PPROFLISTEN=localhost:6060 ./run --autoconf &> /dev/null &
echo "Started, to continue you should (possibly w/ sudo):"
echo "kill" $(jobs -p)

View File

@@ -1,4 +1,2 @@
#!/bin/bash
export GOPATH=$PWD
go get -d yggdrasil
go run -tags debug misc/sim/treesim.go
go run -tags debug misc/sim/treesim.go "$@"

View File

@@ -8,10 +8,11 @@ import "strconv"
import "time"
import "log"
import "runtime"
import "runtime/pprof"
import "flag"
import . "yggdrasil"
import . "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
////////////////////////////////////////////////////////////////////////////////
@@ -267,6 +268,7 @@ func pingNodes(store map[[32]byte]*Node) {
copy(packet[8:24], sourceAddr)
copy(packet[24:40], destAddr)
copy(packet[40:], bs)
packet[0] = 6 << 4
source.send <- packet
}
destCount := 0
@@ -279,17 +281,7 @@ func pingNodes(store map[[32]byte]*Node) {
}
destAddr := dest.core.DEBUG_getAddr()[:]
ticker := time.NewTicker(150 * time.Millisecond)
ch := make(chan bool, 1)
ch <- true
doTicker := func() {
for range ticker.C {
select {
case ch <- true:
default:
}
}
}
go doTicker()
sendTo(payload, destAddr)
for loop := true; loop; {
select {
case packet := <-dest.recv:
@@ -298,8 +290,9 @@ func pingNodes(store map[[32]byte]*Node) {
loop = false
}
}
case <-ch:
case <-ticker.C:
sendTo(payload, destAddr)
//dumpDHTSize(store) // note that this uses racey functions to read things...
}
}
ticker.Stop()
@@ -386,7 +379,7 @@ func (n *Node) startTCP(listen string) {
}
func (n *Node) connectTCP(remoteAddr string) {
n.core.AddPeer(remoteAddr)
n.core.AddPeer(remoteAddr, remoteAddr)
}
////////////////////////////////////////////////////////////////////////////////
@@ -437,7 +430,7 @@ func main() {
pingNodes(kstore)
//pingBench(kstore) // Only after disabling debug output
//stressTest(kstore)
//time.Sleep(120*time.Second)
//time.Sleep(120 * time.Second)
dumpDHTSize(kstore) // note that this uses racey functions to read things...
if false {
// This connects the sim to the local network
@@ -456,4 +449,5 @@ func main() {
var block chan struct{}
<-block
}
runtime.GC()
}

53
src/config/config.go Normal file
View File

@@ -0,0 +1,53 @@
package config
// NodeConfig defines all configuration values needed to run a signle yggdrasil node
type NodeConfig struct {
Listen string `comment:"Listen address for peer connections. Default is to listen for all\nTCP connections over IPv4 and IPv6 with a random port."`
AdminListen string `comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."`
Peers []string `comment:"List of connection strings for static peers in URI format, e.g.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j."`
InterfacePeers map[string][]string `comment:"List of connection strings for static peers in URI format, arranged\nby source interface, e.g. { \"eth0\": [ tcp://a.b.c.d:e ] }. Note that\nSOCKS peerings will NOT be affected by this option and should go in\nthe \"Peers\" section instead."`
ReadTimeout int32 `comment:"Read timeout for connections, specified in milliseconds. If less\nthan 6000 and not negative, 6000 (the default) is used. If negative,\nreads won't time out."`
AllowedEncryptionPublicKeys []string `comment:"List of peer encryption public keys to allow or incoming TCP\nconnections from. If left empty/undefined then all connections\nwill be allowed by default."`
EncryptionPublicKey string `comment:"Your public encryption key. Your peers may ask you for this to put\ninto their AllowedEncryptionPublicKeys configuration."`
EncryptionPrivateKey string `comment:"Your private encryption key. DO NOT share this with anyone!"`
SigningPublicKey string `comment:"Your public signing key. You should not ordinarily need to share\nthis with anyone."`
SigningPrivateKey string `comment:"Your private signing key. DO NOT share this with anyone!"`
MulticastInterfaces []string `comment:"Regular expressions for which interfaces multicast peer discovery\nshould be enabled on. If none specified, multicast peer discovery is\ndisabled. The default value is .* which uses all interfaces."`
IfName string `comment:"Local network interface name for TUN/TAP adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN/TAP."`
IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."`
IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
SessionFirewall SessionFirewall `comment:"The session firewall controls who can send/receive network traffic\nto/from. This is useful if you want to protect this node without\nresorting to using a real firewall. This does not affect traffic\nbeing routed via this node to somewhere else. Rules are prioritised as\nfollows: blacklist, whitelist, always allow outgoing, direct, remote."`
TunnelRouting TunnelRouting `comment:"Allow tunneling non-Yggdrasil traffic over Yggdrasil. This effectively\nallows you to use Yggdrasil to route to, or to bridge other networks,\nsimilar to a VPN tunnel. Tunnelling works between any two nodes and\ndoes not require them to be directly peered."`
SwitchOptions SwitchOptions `comment:"Advanced options for tuning the switch. Normally you will not need\nto edit these options."`
//Net NetConfig `comment:"Extended options for connecting to peers over other networks."`
}
// NetConfig defines network/proxy related configuration values
type NetConfig struct {
Tor TorConfig `comment:"Experimental options for configuring peerings over Tor."`
I2P I2PConfig `comment:"Experimental options for configuring peerings over I2P."`
}
// SessionFirewall controls the session firewall configuration
type SessionFirewall struct {
Enable bool `comment:"Enable or disable the session firewall. If disabled, network traffic\nfrom any node will be allowed. If enabled, the below rules apply."`
AllowFromDirect bool `comment:"Allow network traffic from directly connected peers."`
AllowFromRemote bool `comment:"Allow network traffic from remote nodes on the network that you are\nnot directly peered with."`
AlwaysAllowOutbound bool `comment:"Allow outbound network traffic regardless of AllowFromDirect or\nAllowFromRemote. This does allow a remote node to send unsolicited\ntraffic back to you for the length of the session."`
WhitelistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always accepted,\nregardless of AllowFromDirect or AllowFromRemote."`
BlacklistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always rejected,\nregardless of the whitelist, AllowFromDirect or AllowFromRemote."`
}
// TunnelRouting contains the crypto-key routing tables for tunneling
type TunnelRouting struct {
Enable bool `comment:"Enable or disable tunnel routing."`
IPv6Destinations map[string]string `comment:"IPv6 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"aaaa:bbbb:cccc::/e\": \"boxpubkey\", ... }"`
IPv6Sources []string `comment:"Optional IPv6 source subnets which are allowed to be tunnelled in\naddition to this node's Yggdrasil address/subnet. If not\nspecified, only traffic originating from this node's Yggdrasil\naddress or subnet will be tunnelled."`
IPv4Destinations map[string]string `comment:"IPv4 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"a.b.c.d/e\": \"boxpubkey\", ... }"`
IPv4Sources []string `comment:"IPv4 source subnets which are allowed to be tunnelled. Unlike for\nIPv6, this option is required for bridging IPv4 traffic. Only\ntraffic with a source matching these subnets will be tunnelled."`
}
// SwitchOptions contains tuning options for the switch
type SwitchOptions struct {
MaxTotalQueueSize uint64 `comment:"Maximum size of all switch queues combined (in bytes)."`
}

18
src/defaults/defaults.go Normal file
View File

@@ -0,0 +1,18 @@
package defaults
// Defines which parameters are expected by default for configuration on a
// specific platform. These values are populated in the relevant defaults_*.go
// for the platform being targeted. They must be set.
type platformDefaultParameters struct {
// Admin socket
DefaultAdminListen string
// Configuration (used for yggdrasilctl)
DefaultConfigFile string
// TUN/TAP
MaximumIfMTU int
DefaultIfMTU int
DefaultIfName string
DefaultIfTAPMode bool
}

View File

@@ -0,0 +1,21 @@
// +build darwin
package defaults
// Sane defaults for the macOS/Darwin platform. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
// Configuration (used for yggdrasilctl)
DefaultConfigFile: "/etc/yggdrasil.conf",
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "auto",
DefaultIfTAPMode: false,
}
}

View File

@@ -0,0 +1,21 @@
// +build freebsd
package defaults
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
// Configuration (used for yggdrasilctl)
DefaultConfigFile: "/etc/yggdrasil.conf",
// TUN/TAP
MaximumIfMTU: 32767,
DefaultIfMTU: 32767,
DefaultIfName: "/dev/tap0",
DefaultIfTAPMode: true,
}
}

View File

@@ -0,0 +1,21 @@
// +build linux
package defaults
// Sane defaults for the Linux platform. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
// Configuration (used for yggdrasilctl)
DefaultConfigFile: "/etc/yggdrasil.conf",
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "auto",
DefaultIfTAPMode: false,
}
}

View File

@@ -0,0 +1,21 @@
// +build netbsd
package defaults
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
// Configuration (used for yggdrasilctl)
DefaultConfigFile: "/etc/yggdrasil.conf",
// TUN/TAP
MaximumIfMTU: 9000,
DefaultIfMTU: 9000,
DefaultIfName: "/dev/tap0",
DefaultIfTAPMode: true,
}
}

View File

@@ -0,0 +1,21 @@
// +build openbsd
package defaults
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
// Configuration (used for yggdrasilctl)
DefaultConfigFile: "/etc/yggdrasil.conf",
// TUN/TAP
MaximumIfMTU: 16384,
DefaultIfMTU: 16384,
DefaultIfName: "/dev/tap0",
DefaultIfTAPMode: true,
}
}

View File

@@ -0,0 +1,21 @@
// +build !linux,!darwin,!windows,!openbsd,!freebsd,!netbsd
package defaults
// Sane defaults for the other platforms. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "tcp://localhost:9001",
// Configuration (used for yggdrasilctl)
DefaultConfigFile: "/etc/yggdrasil.conf",
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "none",
DefaultIfTAPMode: false,
}
}

View File

@@ -0,0 +1,21 @@
// +build windows
package defaults
// Sane defaults for the Windows platform. The "default" options may be
// may be replaced by the running configuration.
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "tcp://localhost:9001",
// Configuration (used for yggdrasilctl)
DefaultConfigFile: "C:\\Program Files\\Yggdrasil\\yggdrasil.conf",
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "auto",
DefaultIfTAPMode: true,
}
}

View File

@@ -7,9 +7,10 @@ type address [16]byte
type subnet [8]byte
// address_prefix is the prefix used for all addresses and subnets in the network.
// The current implementation requires this to be a multiple of 8 bits.
// The current implementation requires this to be a muliple of 8 bits + 7 bits.
// The 8th bit of the last byte is used to signal nodes (0) or /64 prefixes (1).
// Nodes that configure this differently will be unable to communicate with eachother, though routing and the DHT machinery *should* still work.
var address_prefix = [...]byte{0xfd}
var address_prefix = [...]byte{0x02}
// isValid returns true if an address falls within the range used by nodes in the network.
func (a *address) isValid() bool {
@@ -18,24 +19,24 @@ func (a *address) isValid() bool {
return false
}
}
return (*a)[len(address_prefix)]&0x80 == 0
return true
}
// isValid returns true if a prefix falls within the range usable by the network.
func (s *subnet) isValid() bool {
for idx := range address_prefix {
l := len(address_prefix)
for idx := range address_prefix[:l-1] {
if (*s)[idx] != address_prefix[idx] {
return false
}
}
return (*s)[len(address_prefix)]&0x80 != 0
return (*s)[l-1] == address_prefix[l-1]|0x01
}
// address_addrForNodeID takes a *NodeID as an argument and returns an *address.
// This address begins with the address prefix.
// The next bit is 0 for an address, and 1 for a subnet.
// The following 7 bits are set to the number of leading 1 bits in the NodeID.
// The NodeID, excluding the leading 1 bits and the first leading 1 bit, is truncated to the appropriate length and makes up the remainder of the address.
// This subnet begins with the address prefix, with the last bit set to 0 to indicate an address.
// The following 8 bits are set to the number of leading 1 bits in the NodeID.
// The NodeID, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the address.
func address_addrForNodeID(nid *NodeID) *address {
// 128 bit address
// Begins with prefix
@@ -67,16 +68,15 @@ func address_addrForNodeID(nid *NodeID) *address {
}
}
copy(addr[:], address_prefix[:])
addr[len(address_prefix)] = ones & 0x7f
addr[len(address_prefix)] = ones
copy(addr[len(address_prefix)+1:], temp)
return &addr
}
// address_subnetForNodeID takes a *NodeID as an argument and returns a *subnet.
// This subnet begins with the address prefix.
// The next bit is 0 for an address, and 1 for a subnet.
// The following 7 bits are set to the number of leading 1 bits in the NodeID.
// The NodeID, excluding the leading 1 bits and the first leading 1 bit, is truncated to the appropriate length and makes up the remainder of the subnet.
// This subnet begins with the address prefix, with the last bit set to 1 to indicate a prefix.
// The following 8 bits are set to the number of leading 1 bits in the NodeID.
// The NodeID, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the subnet.
func address_subnetForNodeID(nid *NodeID) *subnet {
// Exactly as the address version, with two exceptions:
// 1) The first bit after the fixed prefix is a 1 instead of a 0
@@ -84,7 +84,7 @@ func address_subnetForNodeID(nid *NodeID) *subnet {
addr := *address_addrForNodeID(nid)
var snet subnet
copy(snet[:], addr[:])
snet[len(address_prefix)] |= 0x80
snet[len(address_prefix)-1] |= 0x01
return &snet
}
@@ -97,7 +97,7 @@ func (a *address) getNodeIDandMask() (*NodeID, *NodeID) {
// This means truncated leading 1s, first leading 0, and visible part of addr
var nid NodeID
var mask NodeID
ones := int(a[len(address_prefix)] & 0x7f)
ones := int(a[len(address_prefix)])
for idx := 0; idx < ones; idx++ {
nid[idx/8] |= 0x80 >> byte(idx%8)
}
@@ -125,7 +125,7 @@ func (s *subnet) getNodeIDandMask() (*NodeID, *NodeID) {
// As with the address version, but visible parts of the subnet prefix instead
var nid NodeID
var mask NodeID
ones := int(s[len(address_prefix)] & 0x7f)
ones := int(s[len(address_prefix)])
for idx := 0; idx < ones; idx++ {
nid[idx/8] |= 0x80 >> byte(idx%8)
}

View File

@@ -13,6 +13,8 @@ import (
"strings"
"sync/atomic"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
)
// TODO: Add authentication
@@ -20,6 +22,7 @@ import (
type admin struct {
core *Core
listenaddr string
listener net.Listener
handlers []admin_handlerInfo
}
@@ -49,12 +52,12 @@ func (a *admin) addHandler(name string, args []string, handler func(admin_info)
func (a *admin) init(c *Core, listenaddr string) {
a.core = c
a.listenaddr = listenaddr
a.addHandler("help", nil, func(in admin_info) (admin_info, error) {
a.addHandler("list", []string{}, func(in admin_info) (admin_info, error) {
handlers := make(map[string]interface{})
for _, handler := range a.handlers {
handlers[handler.name] = admin_info{"fields": handler.args}
}
return admin_info{"help": handlers}, nil
return admin_info{"list": handlers}, nil
})
a.addHandler("dot", []string{}, func(in admin_info) (admin_info, error) {
return admin_info{"dot": string(a.getResponse_dot())}, nil
@@ -87,6 +90,10 @@ func (a *admin) init(c *Core, listenaddr string) {
}
return admin_info{"switchpeers": switchpeers}, nil
})
a.addHandler("getSwitchQueues", []string{}, func(in admin_info) (admin_info, error) {
queues := a.getData_getSwitchQueues()
return admin_info{"switchqueues": queues.asMap()}, nil
})
a.addHandler("getDHT", []string{}, func(in admin_info) (admin_info, error) {
sort := "ip"
dht := make(admin_info)
@@ -109,8 +116,14 @@ func (a *admin) init(c *Core, listenaddr string) {
}
return admin_info{"sessions": sessions}, nil
})
a.addHandler("addPeer", []string{"uri"}, func(in admin_info) (admin_info, error) {
if a.addPeer(in["uri"].(string)) == nil {
a.addHandler("addPeer", []string{"uri", "[interface]"}, func(in admin_info) (admin_info, error) {
// Set sane defaults
intf := ""
// Has interface been specified?
if itf, ok := in["interface"]; ok {
intf = itf.(string)
}
if a.addPeer(in["uri"].(string), intf) == nil {
return admin_info{
"added": []string{
in["uri"].(string),
@@ -155,15 +168,15 @@ func (a *admin) init(c *Core, listenaddr string) {
})
a.addHandler("setTunTap", []string{"name", "[tap_mode]", "[mtu]"}, func(in admin_info) (admin_info, error) {
// Set sane defaults
iftapmode := getDefaults().defaultIfTAPMode
ifmtu := getDefaults().defaultIfMTU
iftapmode := defaults.GetDefaults().DefaultIfTAPMode
ifmtu := defaults.GetDefaults().DefaultIfMTU
// Has TAP mode been specified?
if tap, ok := in["tap_mode"]; ok {
iftapmode = tap.(bool)
}
// Check we have enough params for MTU
if mtu, ok := in["mtu"]; ok {
if mtu.(float64) >= 1280 && ifmtu <= getDefaults().maximumIfMTU {
if mtu.(float64) >= 1280 && ifmtu <= defaults.GetDefaults().MaximumIfMTU {
ifmtu = int(in["mtu"].(float64))
}
}
@@ -201,7 +214,7 @@ func (a *admin) init(c *Core, listenaddr string) {
"not_added": []string{
in["box_pub_key"].(string),
},
}, errors.New("Failed to add allowed box pub key")
}, errors.New("Failed to add allowed key")
}
})
a.addHandler("removeAllowedEncryptionPublicKey", []string{"box_pub_key"}, func(in admin_info) (admin_info, error) {
@@ -216,28 +229,152 @@ func (a *admin) init(c *Core, listenaddr string) {
"not_removed": []string{
in["box_pub_key"].(string),
},
}, errors.New("Failed to remove allowed box pub key")
}, errors.New("Failed to remove allowed key")
}
})
a.addHandler("addSourceSubnet", []string{"subnet"}, func(in admin_info) (admin_info, error) {
var err error
a.core.router.doAdmin(func() {
err = a.core.router.cryptokey.addSourceSubnet(in["subnet"].(string))
})
if err == nil {
return admin_info{"added": []string{in["subnet"].(string)}}, nil
} else {
return admin_info{"not_added": []string{in["subnet"].(string)}}, errors.New("Failed to add source subnet")
}
})
a.addHandler("addRoute", []string{"subnet", "box_pub_key"}, func(in admin_info) (admin_info, error) {
var err error
a.core.router.doAdmin(func() {
err = a.core.router.cryptokey.addRoute(in["subnet"].(string), in["box_pub_key"].(string))
})
if err == nil {
return admin_info{"added": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, nil
} else {
return admin_info{"not_added": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, errors.New("Failed to add route")
}
})
a.addHandler("getSourceSubnets", []string{}, func(in admin_info) (admin_info, error) {
var subnets []string
a.core.router.doAdmin(func() {
getSourceSubnets := func(snets []net.IPNet) {
for _, subnet := range snets {
subnets = append(subnets, subnet.String())
}
}
getSourceSubnets(a.core.router.cryptokey.ipv4sources)
getSourceSubnets(a.core.router.cryptokey.ipv6sources)
})
return admin_info{"source_subnets": subnets}, nil
})
a.addHandler("getRoutes", []string{}, func(in admin_info) (admin_info, error) {
routes := make(admin_info)
a.core.router.doAdmin(func() {
getRoutes := func(ckrs []cryptokey_route) {
for _, ckr := range ckrs {
routes[ckr.subnet.String()] = hex.EncodeToString(ckr.destination[:])
}
}
getRoutes(a.core.router.cryptokey.ipv4routes)
getRoutes(a.core.router.cryptokey.ipv6routes)
})
return admin_info{"routes": routes}, nil
})
a.addHandler("removeSourceSubnet", []string{"subnet"}, func(in admin_info) (admin_info, error) {
var err error
a.core.router.doAdmin(func() {
err = a.core.router.cryptokey.removeSourceSubnet(in["subnet"].(string))
})
if err == nil {
return admin_info{"removed": []string{in["subnet"].(string)}}, nil
} else {
return admin_info{"not_removed": []string{in["subnet"].(string)}}, errors.New("Failed to remove source subnet")
}
})
a.addHandler("removeRoute", []string{"subnet", "box_pub_key"}, func(in admin_info) (admin_info, error) {
var err error
a.core.router.doAdmin(func() {
err = a.core.router.cryptokey.removeRoute(in["subnet"].(string), in["box_pub_key"].(string))
})
if err == nil {
return admin_info{"removed": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, nil
} else {
return admin_info{"not_removed": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, errors.New("Failed to remove route")
}
})
a.addHandler("dhtPing", []string{"box_pub_key", "coords", "[target]"}, func(in admin_info) (admin_info, error) {
if in["target"] == nil {
in["target"] = "none"
}
result, err := a.admin_dhtPing(in["box_pub_key"].(string), in["coords"].(string), in["target"].(string))
if err == nil {
infos := make(map[string]map[string]string, len(result.Infos))
for _, dinfo := range result.Infos {
info := map[string]string{
"box_pub_key": hex.EncodeToString(dinfo.key[:]),
"coords": fmt.Sprintf("%v", dinfo.coords),
}
addr := net.IP(address_addrForNodeID(getNodeID(&dinfo.key))[:]).String()
infos[addr] = info
}
return admin_info{"nodes": infos}, nil
} else {
return admin_info{}, err
}
})
}
// start runs the admin API socket to listen for / respond to admin API calls.
func (a *admin) start() error {
go a.listen()
if a.listenaddr != "none" && a.listenaddr != "" {
go a.listen()
}
return nil
}
// cleans up when stopping
func (a *admin) close() error {
return a.listener.Close()
}
// listen is run by start and manages API connections.
func (a *admin) listen() {
l, err := net.Listen("tcp", a.listenaddr)
u, err := url.Parse(a.listenaddr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
if _, err := os.Stat(a.listenaddr[7:]); err == nil {
a.core.log.Println("WARNING:", a.listenaddr[7:], "already exists and may be in use by another process")
}
a.listener, err = net.Listen("unix", a.listenaddr[7:])
if err == nil {
switch a.listenaddr[7:8] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(a.listenaddr[7:], 0660); err != nil {
a.core.log.Println("WARNING:", a.listenaddr[:7], "may have unsafe permissions!")
}
}
}
case "tcp":
a.listener, err = net.Listen("tcp", u.Host)
default:
// err = errors.New(fmt.Sprint("protocol not supported: ", u.Scheme))
a.listener, err = net.Listen("tcp", a.listenaddr)
}
} else {
a.listener, err = net.Listen("tcp", a.listenaddr)
}
if err != nil {
a.core.log.Printf("Admin socket failed to listen: %v", err)
os.Exit(1)
}
defer l.Close()
a.core.log.Printf("Admin socket listening on %s", l.Addr().String())
a.core.log.Printf("%s admin socket listening on %s",
strings.ToUpper(a.listener.Addr().Network()),
a.listener.Addr().String())
defer a.listener.Close()
for {
conn, err := l.Accept()
conn, err := a.listener.Accept()
if err == nil {
a.handleRequest(conn)
}
@@ -286,7 +423,7 @@ func (a *admin) handleRequest(conn net.Conn) {
handlers:
for _, handler := range a.handlers {
// We've found the handler that matches the request
if recv["request"] == handler.name {
if strings.ToLower(recv["request"].(string)) == strings.ToLower(handler.name) {
// Check that we have all the required arguments
for _, arg := range handler.args {
// An argument in [square brackets] is optional and not required,
@@ -353,7 +490,6 @@ func (n *admin_nodeInfo) toString() string {
out = append(out, fmt.Sprintf("%v: %v", p.key, p.val))
}
return strings.Join(out, ", ")
return fmt.Sprint(*n)
}
// printInfos returns a newline separated list of strings from admin_nodeInfos, e.g. a printable string of info about all peers.
@@ -367,12 +503,12 @@ func (a *admin) printInfos(infos []admin_nodeInfo) string {
}
// addPeer triggers a connection attempt to a node.
func (a *admin) addPeer(addr string) error {
func (a *admin) addPeer(addr string, sintf string) error {
u, err := url.Parse(addr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "tcp":
a.core.tcp.connect(u.Host)
a.core.tcp.connect(u.Host, sintf)
case "socks":
a.core.tcp.connectSOCKS(u.Host, u.Path[1:])
default:
@@ -384,7 +520,7 @@ func (a *admin) addPeer(addr string) error {
if strings.HasPrefix(addr, "tcp:") {
addr = addr[4:]
}
a.core.tcp.connect(addr)
a.core.tcp.connect(addr, "")
return nil
}
return nil
@@ -406,7 +542,7 @@ func (a *admin) startTunWithMTU(ifname string, iftapmode bool, ifmtu int) error
_ = a.core.tun.close()
// Then reconfigure and start it
addr := a.core.router.addr
straddr := fmt.Sprintf("%s/%v", net.IP(addr[:]).String(), 8*len(address_prefix))
straddr := fmt.Sprintf("%s/%v", net.IP(addr[:]).String(), 8*len(address_prefix)-1)
if ifname != "none" {
err := a.core.tun.setup(ifname, iftapmode, straddr, ifmtu)
if err != nil {
@@ -434,10 +570,18 @@ func (a *admin) getData_getSelf() *admin_nodeInfo {
table := a.core.switchTable.table.Load().(lookupTable)
coords := table.self.getCoords()
self := admin_nodeInfo{
{"box_pub_key", hex.EncodeToString(a.core.boxPub[:])},
{"ip", a.core.GetAddress().String()},
{"subnet", a.core.GetSubnet().String()},
{"coords", fmt.Sprint(coords)},
}
if name := GetBuildName(); name != "unknown" {
self = append(self, admin_pair{"build_name", name})
}
if version := GetBuildVersion(); version != "unknown" {
self = append(self, admin_pair{"build_version", version})
}
return &self
}
@@ -459,6 +603,8 @@ func (a *admin) getData_getPeers() []admin_nodeInfo {
{"uptime", int(time.Since(p.firstSeen).Seconds())},
{"bytes_sent", atomic.LoadUint64(&p.bytesSent)},
{"bytes_recvd", atomic.LoadUint64(&p.bytesRecvd)},
{"endpoint", p.endpoint},
{"box_pub_key", hex.EncodeToString(p.box[:])},
}
peerInfos = append(peerInfos, info)
}
@@ -481,34 +627,66 @@ func (a *admin) getData_getSwitchPeers() []admin_nodeInfo {
{"ip", net.IP(addr[:]).String()},
{"coords", fmt.Sprint(coords)},
{"port", elem.port},
{"bytes_sent", atomic.LoadUint64(&peer.bytesSent)},
{"bytes_recvd", atomic.LoadUint64(&peer.bytesRecvd)},
{"endpoint", peer.endpoint},
{"box_pub_key", hex.EncodeToString(peer.box[:])},
}
peerInfos = append(peerInfos, info)
}
return peerInfos
}
// getData_getSwitchQueues returns info from Core.switchTable for an queue data.
func (a *admin) getData_getSwitchQueues() admin_nodeInfo {
var peerInfos admin_nodeInfo
switchTable := &a.core.switchTable
getSwitchQueues := func() {
queues := make([]map[string]interface{}, 0)
for k, v := range switchTable.queues.bufs {
nexthop := switchTable.bestPortForCoords([]byte(k))
queue := map[string]interface{}{
"queue_id": k,
"queue_size": v.size,
"queue_packets": len(v.packets),
"queue_port": nexthop,
}
queues = append(queues, queue)
}
peerInfos = admin_nodeInfo{
{"queues", queues},
{"queues_count", len(switchTable.queues.bufs)},
{"queues_size", switchTable.queues.size},
{"highest_queues_count", switchTable.queues.maxbufs},
{"highest_queues_size", switchTable.queues.maxsize},
{"maximum_queues_size", switchTable.queueTotalMaxSize},
}
}
a.core.switchTable.doAdmin(getSwitchQueues)
return peerInfos
}
// getData_getDHT returns info from Core.dht for an admin response.
func (a *admin) getData_getDHT() []admin_nodeInfo {
var infos []admin_nodeInfo
now := time.Now()
getDHT := func() {
for i := 0; i < a.core.dht.nBuckets(); i++ {
b := a.core.dht.getBucket(i)
getInfo := func(vs []*dhtInfo, isPeer bool) {
for _, v := range vs {
addr := *address_addrForNodeID(v.getNodeID())
info := admin_nodeInfo{
{"ip", net.IP(addr[:]).String()},
{"coords", fmt.Sprint(v.coords)},
{"bucket", i},
{"peer_only", isPeer},
{"last_seen", int(now.Sub(v.recv).Seconds())},
}
infos = append(infos, info)
}
now := time.Now()
var dhtInfos []*dhtInfo
for _, v := range a.core.dht.table {
dhtInfos = append(dhtInfos, v)
}
sort.SliceStable(dhtInfos, func(i, j int) bool {
return dht_ordered(&a.core.dht.nodeID, dhtInfos[i].getNodeID(), dhtInfos[j].getNodeID())
})
for _, v := range dhtInfos {
addr := *address_addrForNodeID(v.getNodeID())
info := admin_nodeInfo{
{"ip", net.IP(addr[:]).String()},
{"coords", fmt.Sprint(v.coords)},
{"last_seen", int(now.Sub(v.recv).Seconds())},
{"box_pub_key", hex.EncodeToString(v.key[:])},
}
getInfo(b.other, false)
getInfo(b.peers, true)
infos = append(infos, info)
}
}
a.core.router.doAdmin(getDHT)
@@ -528,6 +706,7 @@ func (a *admin) getData_getSessions() []admin_nodeInfo {
{"was_mtu_fixed", sinfo.wasMTUFixed},
{"bytes_sent", sinfo.bytesSent},
{"bytes_recvd", sinfo.bytesRecvd},
{"box_pub_key", hex.EncodeToString(sinfo.theirPermPub[:])},
}
infos = append(infos, info)
}
@@ -569,6 +748,64 @@ func (a *admin) removeAllowedEncryptionPublicKey(bstr string) (err error) {
return
}
// Send a DHT ping to the node with the provided key and coords, optionally looking up the specified target NodeID.
func (a *admin) admin_dhtPing(keyString, coordString, targetString string) (dhtRes, error) {
var key boxPubKey
if keyBytes, err := hex.DecodeString(keyString); err != nil {
return dhtRes{}, err
} else {
copy(key[:], keyBytes)
}
var coords []byte
for _, cstr := range strings.Split(strings.Trim(coordString, "[]"), " ") {
if cstr == "" {
// Special case, happens if trimmed is the empty string, e.g. this is the root
continue
}
if u64, err := strconv.ParseUint(cstr, 10, 8); err != nil {
return dhtRes{}, err
} else {
coords = append(coords, uint8(u64))
}
}
resCh := make(chan *dhtRes, 1)
info := dhtInfo{
key: key,
coords: coords,
}
target := *info.getNodeID()
if targetString == "none" {
// Leave the default target in place
} else if targetBytes, err := hex.DecodeString(targetString); err != nil {
return dhtRes{}, err
} else if len(targetBytes) != len(target) {
return dhtRes{}, errors.New("Incorrect target NodeID length")
} else {
target = NodeID{}
copy(target[:], targetBytes)
}
rq := dhtReqKey{info.key, target}
sendPing := func() {
a.core.dht.addCallback(&rq, func(res *dhtRes) {
defer func() { recover() }()
select {
case resCh <- res:
default:
}
})
a.core.dht.ping(&info, &target)
}
a.core.router.doAdmin(sendPing)
go func() {
time.Sleep(6 * time.Second)
close(resCh)
}()
for res := range resCh {
return *res, nil
}
return dhtRes{}, errors.New(fmt.Sprintf("DHT ping timeout: %s", keyString))
}
// getResponse_dot returns a response for a graphviz dot formatted representation of the known parts of the network.
// This is color-coded and labeled, and includes the self node, switch peers, nodes known to the DHT, and nodes with open sessions.
// The graph is structured as a tree with directed links leading away from the root.
@@ -582,9 +819,16 @@ func (a *admin) getResponse_dot() []byte {
name string
key string
parent string
port switchPort
options string
}
infos := make(map[string]nodeInfo)
// Get coords as a slice of strings, FIXME? this looks very fragile
coordSlice := func(coords string) []string {
tmp := strings.Replace(coords, "[", "", -1)
tmp = strings.Replace(tmp, "]", "", -1)
return strings.Split(tmp, " ")
}
// First fill the tree with all known nodes, no parents
addInfo := func(nodes []admin_nodeInfo, options string, tag string) {
for _, node := range nodes {
@@ -598,6 +842,14 @@ func (a *admin) getResponse_dot() []byte {
} else {
info.name = n["ip"].(string)
}
coordsSplit := coordSlice(info.key)
if len(coordsSplit) != 0 {
portStr := coordsSplit[len(coordsSplit)-1]
portUint, err := strconv.ParseUint(portStr, 10, 64)
if err == nil {
info.port = switchPort(portUint)
}
}
infos[info.key] = info
}
}
@@ -605,12 +857,6 @@ func (a *admin) getResponse_dot() []byte {
addInfo(sessions, "fillcolor=\"#acf3fd\" style=filled fontname=\"sans serif\"", "Open session") // blue
addInfo(peers, "fillcolor=\"#ffffb5\" style=filled fontname=\"sans serif\"", "Connected peer") // yellow
addInfo(append([]admin_nodeInfo(nil), *self), "fillcolor=\"#a5ff8a\" style=filled fontname=\"sans serif\"", "This node") // green
// Get coords as a slice of strings, FIXME? this looks very fragile
coordSlice := func(coords string) []string {
tmp := strings.Replace(coords, "[", "", -1)
tmp = strings.Replace(tmp, "]", "", -1)
return strings.Split(tmp, " ")
}
// Now go through and create placeholders for any missing nodes
for _, info := range infos {
// This is ugly string manipulation
@@ -624,6 +870,16 @@ func (a *admin) getResponse_dot() []byte {
newInfo.name = "?"
newInfo.key = key
newInfo.options = "fontname=\"sans serif\" style=dashed color=\"#999999\" fontcolor=\"#999999\""
coordsSplit := coordSlice(newInfo.key)
if len(coordsSplit) != 0 {
portStr := coordsSplit[len(coordsSplit)-1]
portUint, err := strconv.ParseUint(portStr, 10, 64)
if err == nil {
newInfo.port = switchPort(portUint)
}
}
infos[key] = newInfo
}
}
@@ -642,10 +898,12 @@ func (a *admin) getResponse_dot() []byte {
keys = append(keys, info.key)
}
// sort
less := func(i, j int) bool {
sort.SliceStable(keys, func(i, j int) bool {
return keys[i] < keys[j]
}
sort.Slice(keys, less)
})
sort.SliceStable(keys, func(i, j int) bool {
return infos[keys[i]].port < infos[keys[j]].port
})
// Now print it all out
var out []byte
put := func(s string) {
@@ -663,11 +921,7 @@ func (a *admin) getResponse_dot() []byte {
if info.key == info.parent {
continue
} // happens for the root, skip it
coordsSplit := coordSlice(key)
if len(coordsSplit) == 0 {
continue
}
port := coordsSplit[len(coordsSplit)-1]
port := fmt.Sprint(info.port)
style := "fontname=\"sans serif\""
if infos[info.parent].name == "?" || infos[info.key].name == "?" {
style = "fontname=\"sans serif\" style=dashed color=\"#999999\" fontcolor=\"#999999\""

348
src/yggdrasil/ckr.go Normal file
View File

@@ -0,0 +1,348 @@
package yggdrasil
import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"net"
"sort"
)
// This module implements crypto-key routing, similar to Wireguard, where we
// allow traffic for non-Yggdrasil ranges to be routed over Yggdrasil.
type cryptokey struct {
core *Core
enabled bool
ipv4routes []cryptokey_route
ipv6routes []cryptokey_route
ipv4cache map[address]cryptokey_route
ipv6cache map[address]cryptokey_route
ipv4sources []net.IPNet
ipv6sources []net.IPNet
}
type cryptokey_route struct {
subnet net.IPNet
destination boxPubKey
}
// Initialise crypto-key routing. This must be done before any other CKR calls.
func (c *cryptokey) init(core *Core) {
c.core = core
c.ipv4routes = make([]cryptokey_route, 0)
c.ipv6routes = make([]cryptokey_route, 0)
c.ipv4cache = make(map[address]cryptokey_route, 0)
c.ipv6cache = make(map[address]cryptokey_route, 0)
c.ipv4sources = make([]net.IPNet, 0)
c.ipv6sources = make([]net.IPNet, 0)
}
// Enable or disable crypto-key routing.
func (c *cryptokey) setEnabled(enabled bool) {
c.enabled = enabled
}
// Check if crypto-key routing is enabled.
func (c *cryptokey) isEnabled() bool {
return c.enabled
}
// Check whether the given address (with the address length specified in bytes)
// matches either the current node's address, the node's routed subnet or the
// list of subnets specified in IPv4Sources/IPv6Sources.
func (c *cryptokey) isValidSource(addr address, addrlen int) bool {
ip := net.IP(addr[:addrlen])
if addrlen == net.IPv6len {
// Does this match our node's address?
if bytes.Equal(addr[:16], c.core.router.addr[:16]) {
return true
}
// Does this match our node's subnet?
if bytes.Equal(addr[:8], c.core.router.subnet[:8]) {
return true
}
}
// Does it match a configured CKR source?
if c.isEnabled() {
// Build our references to the routing sources
var routingsources *[]net.IPNet
// Check if the prefix is IPv4 or IPv6
if addrlen == net.IPv6len {
routingsources = &c.ipv6sources
} else if addrlen == net.IPv4len {
routingsources = &c.ipv4sources
} else {
return false
}
for _, subnet := range *routingsources {
if subnet.Contains(ip) {
return true
}
}
}
// Doesn't match any of the above
return false
}
// Adds a source subnet, which allows traffic with these source addresses to
// be tunnelled using crypto-key routing.
func (c *cryptokey) addSourceSubnet(cidr string) error {
// Is the CIDR we've been given valid?
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
// Get the prefix length and size
_, prefixsize := ipnet.Mask.Size()
// Build our references to the routing sources
var routingsources *[]net.IPNet
// Check if the prefix is IPv4 or IPv6
if prefixsize == net.IPv6len*8 {
routingsources = &c.ipv6sources
} else if prefixsize == net.IPv4len*8 {
routingsources = &c.ipv4sources
} else {
return errors.New("Unexpected prefix size")
}
// Check if we already have this CIDR
for _, subnet := range *routingsources {
if subnet.String() == ipnet.String() {
return errors.New("Source subnet already configured")
}
}
// Add the source subnet
*routingsources = append(*routingsources, *ipnet)
c.core.log.Println("Added CKR source subnet", cidr)
return nil
}
// Adds a destination route for the given CIDR to be tunnelled to the node
// with the given BoxPubKey.
func (c *cryptokey) addRoute(cidr string, dest string) error {
// Is the CIDR we've been given valid?
ipaddr, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
// Get the prefix length and size
_, prefixsize := ipnet.Mask.Size()
// Build our references to the routing table and cache
var routingtable *[]cryptokey_route
var routingcache *map[address]cryptokey_route
// Check if the prefix is IPv4 or IPv6
if prefixsize == net.IPv6len*8 {
routingtable = &c.ipv6routes
routingcache = &c.ipv6cache
} else if prefixsize == net.IPv4len*8 {
routingtable = &c.ipv4routes
routingcache = &c.ipv4cache
} else {
return errors.New("Unexpected prefix size")
}
// Is the route an Yggdrasil destination?
var addr address
var snet subnet
copy(addr[:], ipaddr)
copy(snet[:], ipnet.IP)
if addr.isValid() || snet.isValid() {
return errors.New("Can't specify Yggdrasil destination as crypto-key route")
}
// Do we already have a route for this subnet?
for _, route := range *routingtable {
if route.subnet.String() == ipnet.String() {
return errors.New(fmt.Sprintf("Route already exists for %s", cidr))
}
}
// Decode the public key
if bpk, err := hex.DecodeString(dest); err != nil {
return err
} else if len(bpk) != boxPubKeyLen {
return errors.New(fmt.Sprintf("Incorrect key length for %s", dest))
} else {
// Add the new crypto-key route
var key boxPubKey
copy(key[:], bpk)
*routingtable = append(*routingtable, cryptokey_route{
subnet: *ipnet,
destination: key,
})
// Sort so most specific routes are first
sort.Slice(*routingtable, func(i, j int) bool {
im, _ := (*routingtable)[i].subnet.Mask.Size()
jm, _ := (*routingtable)[j].subnet.Mask.Size()
return im > jm
})
// Clear the cache as this route might change future routing
// Setting an empty slice keeps the memory whereas nil invokes GC
for k := range *routingcache {
delete(*routingcache, k)
}
c.core.log.Println("Added CKR destination subnet", cidr)
return nil
}
}
// Looks up the most specific route for the given address (with the address
// length specified in bytes) from the crypto-key routing table. An error is
// returned if the address is not suitable or no route was found.
func (c *cryptokey) getPublicKeyForAddress(addr address, addrlen int) (boxPubKey, error) {
// Check if the address is a valid Yggdrasil address - if so it
// is exempt from all CKR checking
if addr.isValid() {
return boxPubKey{}, errors.New("Cannot look up CKR for Yggdrasil addresses")
}
// Build our references to the routing table and cache
var routingtable *[]cryptokey_route
var routingcache *map[address]cryptokey_route
// Check if the prefix is IPv4 or IPv6
if addrlen == net.IPv6len {
routingtable = &c.ipv6routes
routingcache = &c.ipv6cache
} else if addrlen == net.IPv4len {
routingtable = &c.ipv4routes
routingcache = &c.ipv4cache
} else {
return boxPubKey{}, errors.New("Unexpected prefix size")
}
// Check if there's a cache entry for this addr
if route, ok := (*routingcache)[addr]; ok {
return route.destination, nil
}
// No cache was found - start by converting the address into a net.IP
ip := make(net.IP, addrlen)
copy(ip[:addrlen], addr[:])
// Check if we have a route. At this point c.ipv6routes should be
// pre-sorted so that the most specific routes are first
for _, route := range *routingtable {
// Does this subnet match the given IP?
if route.subnet.Contains(ip) {
// Check if the routing cache is above a certain size, if it is evict
// a random entry so we can make room for this one. We take advantage
// of the fact that the iteration order is random here
for k := range *routingcache {
if len(*routingcache) < 1024 {
break
}
delete(*routingcache, k)
}
// Cache the entry for future packets to get a faster lookup
(*routingcache)[addr] = route
// Return the boxPubKey
return route.destination, nil
}
}
// No route was found if we got to this point
return boxPubKey{}, errors.New(fmt.Sprintf("No route to %s", ip.String()))
}
// Removes a source subnet, which allows traffic with these source addresses to
// be tunnelled using crypto-key routing.
func (c *cryptokey) removeSourceSubnet(cidr string) error {
// Is the CIDR we've been given valid?
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
// Get the prefix length and size
_, prefixsize := ipnet.Mask.Size()
// Build our references to the routing sources
var routingsources *[]net.IPNet
// Check if the prefix is IPv4 or IPv6
if prefixsize == net.IPv6len*8 {
routingsources = &c.ipv6sources
} else if prefixsize == net.IPv4len*8 {
routingsources = &c.ipv4sources
} else {
return errors.New("Unexpected prefix size")
}
// Check if we already have this CIDR
for idx, subnet := range *routingsources {
if subnet.String() == ipnet.String() {
*routingsources = append((*routingsources)[:idx], (*routingsources)[idx+1:]...)
c.core.log.Println("Removed CKR source subnet", cidr)
return nil
}
}
return errors.New("Source subnet not found")
}
// Removes a destination route for the given CIDR to be tunnelled to the node
// with the given BoxPubKey.
func (c *cryptokey) removeRoute(cidr string, dest string) error {
// Is the CIDR we've been given valid?
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
return err
}
// Get the prefix length and size
_, prefixsize := ipnet.Mask.Size()
// Build our references to the routing table and cache
var routingtable *[]cryptokey_route
var routingcache *map[address]cryptokey_route
// Check if the prefix is IPv4 or IPv6
if prefixsize == net.IPv6len*8 {
routingtable = &c.ipv6routes
routingcache = &c.ipv6cache
} else if prefixsize == net.IPv4len*8 {
routingtable = &c.ipv4routes
routingcache = &c.ipv4cache
} else {
return errors.New("Unexpected prefix size")
}
// Decode the public key
bpk, err := hex.DecodeString(dest)
if err != nil {
return err
} else if len(bpk) != boxPubKeyLen {
return errors.New(fmt.Sprintf("Incorrect key length for %s", dest))
}
netStr := ipnet.String()
for idx, route := range *routingtable {
if bytes.Equal(route.destination[:], bpk) && route.subnet.String() == netStr {
*routingtable = append((*routingtable)[:idx], (*routingtable)[idx+1:]...)
for k := range *routingcache {
delete(*routingcache, k)
}
c.core.log.Printf("Removed CKR destination subnet %s via %s\n", cidr, dest)
return nil
}
}
return errors.New(fmt.Sprintf("Route does not exists for %s", cidr))
}

View File

@@ -1,24 +0,0 @@
package config
// NodeConfig defines all configuration values needed to run a signle yggdrasil node
type NodeConfig struct {
Listen string `comment:"Listen address for peer connections. Default is to listen for all\nTCP connections over IPv4 and IPv6 with a random port."`
AdminListen string `comment:"Listen address for admin connections Default is to listen for local\nconnections only on TCP port 9001."`
Peers []string `comment:"List of connection strings for static peers in URI format, i.e.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j"`
AllowedEncryptionPublicKeys []string `comment:"List of peer encryption public keys to allow or incoming TCP\nconnections from. If left empty/undefined then all connections\nwill be allowed by default."`
EncryptionPublicKey string `comment:"Your public encryption key. Your peers may ask you for this to put\ninto their AllowedEncryptionPublicKeys configuration."`
EncryptionPrivateKey string `comment:"Your private encryption key. DO NOT share this with anyone!"`
SigningPublicKey string `comment:"Your public signing key. You should not ordinarily need to share\nthis with anyone."`
SigningPrivateKey string `comment:"Your private signing key. DO NOT share this with anyone!"`
MulticastInterfaces []string `comment:"Regular expressions for which interfaces multicast peer discovery\nshould be enabled on. If none specified, multicast peer discovery is\ndisabled. The default value is .* which uses all interfaces."`
IfName string `comment:"Local network interface name for TUN/TAP adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN/TAP."`
IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."`
IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
//Net NetConfig `comment:"Extended options for connecting to peers over other networks."`
}
// NetConfig defines network/proxy related configuration values
type NetConfig struct {
Tor TorConfig `comment:"Experimental options for configuring peerings over Tor."`
I2P I2PConfig `comment:"Experimental options for configuring peerings over I2P."`
}

View File

@@ -8,9 +8,13 @@ import (
"net"
"regexp"
"yggdrasil/config"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
)
var buildName string
var buildVersion string
// The Core object represents the Yggdrasil node. You should create a Core
// object for each Yggdrasil node you plan to run.
type Core struct {
@@ -21,7 +25,6 @@ type Core struct {
sigPriv sigPrivKey
switchTable switchTable
peers peers
sigs sigManager
sessions sessions
router router
dht dht
@@ -49,7 +52,6 @@ func (c *Core) init(bpub *boxPubKey,
c.boxPub, c.boxPriv = *bpub, *bpriv
c.sigPub, c.sigPriv = *spub, *spriv
c.admin.core = c
c.sigs.init()
c.searches.init(c)
c.dht.init(c)
c.sessions.init(c)
@@ -60,12 +62,38 @@ func (c *Core) init(bpub *boxPubKey,
c.tun.init(c)
}
// Get the current build name. This is usually injected if built from git,
// or returns "unknown" otherwise.
func GetBuildName() string {
if buildName == "" {
return "unknown"
}
return buildName
}
// Get the current build version. This is usually injected if built from git,
// or returns "unknown" otherwise.
func GetBuildVersion() string {
if buildVersion == "" {
return "unknown"
}
return buildVersion
}
// Starts up Yggdrasil using the provided NodeConfig, and outputs debug logging
// through the provided log.Logger. The started stack will include TCP and UDP
// sockets, a multicast discovery socket, an admin socket, router, switch and
// DHT node.
func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
c.log = log
if name := GetBuildName(); name != "unknown" {
c.log.Println("Build name:", name)
}
if version := GetBuildVersion(); version != "unknown" {
c.log.Println("Build version:", version)
}
c.log.Println("Starting up...")
var boxPub boxPubKey
@@ -96,16 +124,59 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
c.init(&boxPub, &boxPriv, &sigPub, &sigPriv)
c.admin.init(c, nc.AdminListen)
if err := c.tcp.init(c, nc.Listen); err != nil {
if err := c.tcp.init(c, nc.Listen, nc.ReadTimeout); err != nil {
c.log.Println("Failed to start TCP interface")
return err
}
if nc.SwitchOptions.MaxTotalQueueSize >= SwitchQueueTotalMinSize {
c.switchTable.queueTotalMaxSize = nc.SwitchOptions.MaxTotalQueueSize
}
if err := c.switchTable.start(); err != nil {
c.log.Println("Failed to start switch")
return err
}
c.sessions.setSessionFirewallState(nc.SessionFirewall.Enable)
c.sessions.setSessionFirewallDefaults(
nc.SessionFirewall.AllowFromDirect,
nc.SessionFirewall.AllowFromRemote,
nc.SessionFirewall.AlwaysAllowOutbound,
)
c.sessions.setSessionFirewallWhitelist(nc.SessionFirewall.WhitelistEncryptionPublicKeys)
c.sessions.setSessionFirewallBlacklist(nc.SessionFirewall.BlacklistEncryptionPublicKeys)
if err := c.router.start(); err != nil {
c.log.Println("Failed to start router")
return err
}
c.router.cryptokey.setEnabled(nc.TunnelRouting.Enable)
if c.router.cryptokey.isEnabled() {
c.log.Println("Crypto-key routing enabled")
for ipv6, pubkey := range nc.TunnelRouting.IPv6Destinations {
if err := c.router.cryptokey.addRoute(ipv6, pubkey); err != nil {
panic(err)
}
}
for _, source := range nc.TunnelRouting.IPv6Sources {
if c.router.cryptokey.addSourceSubnet(source); err != nil {
panic(err)
}
}
for ipv4, pubkey := range nc.TunnelRouting.IPv4Destinations {
if err := c.router.cryptokey.addRoute(ipv4, pubkey); err != nil {
panic(err)
}
}
for _, source := range nc.TunnelRouting.IPv4Sources {
if c.router.cryptokey.addSourceSubnet(source); err != nil {
panic(err)
}
}
}
if err := c.admin.start(); err != nil {
c.log.Println("Failed to start admin socket")
return err
@@ -117,7 +188,7 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
}
ip := net.IP(c.router.addr[:]).String()
if err := c.tun.start(nc.IfName, nc.IfTAPMode, fmt.Sprintf("%s/8", ip), nc.IfMTU); err != nil {
if err := c.tun.start(nc.IfName, nc.IfTAPMode, fmt.Sprintf("%s/%d", ip, 8*len(address_prefix)-1), nc.IfMTU); err != nil {
c.log.Println("Failed to start TUN/TAP")
return err
}
@@ -130,6 +201,7 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error {
func (c *Core) Stop() {
c.log.Println("Stopping...")
c.tun.close()
c.admin.close()
}
// Generates a new encryption keypair. The encryption keys are used to
@@ -175,8 +247,8 @@ func (c *Core) SetLogger(log *log.Logger) {
// Adds a peer. This should be specified in the peer URI format, i.e.
// tcp://a.b.c.d:e, udp://a.b.c.d:e, socks://a.b.c.d:e/f.g.h.i:j
func (c *Core) AddPeer(addr string) error {
return c.admin.addPeer(addr)
func (c *Core) AddPeer(addr string, sintf string) error {
return c.admin.addPeer(addr, sintf)
}
// Adds an expression to select multicast interfaces for peer discovery. This
@@ -192,26 +264,31 @@ func (c *Core) AddAllowedEncryptionPublicKey(boxStr string) error {
return c.admin.addAllowedEncryptionPublicKey(boxStr)
}
// Gets the default admin listen address for your platform.
func (c *Core) GetAdminDefaultListen() string {
return defaults.GetDefaults().DefaultAdminListen
}
// Gets the default TUN/TAP interface name for your platform.
func (c *Core) GetTUNDefaultIfName() string {
return getDefaults().defaultIfName
return defaults.GetDefaults().DefaultIfName
}
// Gets the default TUN/TAP interface MTU for your platform. This can be as high
// as 65535, depending on platform, but is never lower than 1280.
func (c *Core) GetTUNDefaultIfMTU() int {
return getDefaults().defaultIfMTU
return defaults.GetDefaults().DefaultIfMTU
}
// Gets the maximum supported TUN/TAP interface MTU for your platform. This
// can be as high as 65535, depending on platform, but is never lower than 1280.
func (c *Core) GetTUNMaximumIfMTU() int {
return getDefaults().maximumIfMTU
return defaults.GetDefaults().MaximumIfMTU
}
// Gets the default TUN/TAP interface mode for your platform.
func (c *Core) GetTUNDefaultIfTAPMode() bool {
return getDefaults().defaultIfTAPMode
return defaults.GetDefaults().DefaultIfTAPMode
}
// Gets the current TUN/TAP interface name.

View File

@@ -10,7 +10,7 @@ package yggdrasil
import _ "golang.org/x/net/ipv6" // TODO put this somewhere better
import "golang.org/x/net/proxy"
//import "golang.org/x/net/proxy"
import "fmt"
import "net"
@@ -20,6 +20,22 @@ import "regexp"
import _ "net/http/pprof"
import "net/http"
import "runtime"
import "os"
import "github.com/yggdrasil-network/yggdrasil-go/src/defaults"
// Start the profiler in debug builds, if the required environment variable is set.
func init() {
envVarName := "PPROFLISTEN"
hostPort := os.Getenv(envVarName)
switch {
case hostPort == "":
fmt.Printf("DEBUG: %s not set, profiler not started.\n", envVarName)
default:
fmt.Printf("DEBUG: Starting pprof on %s\n", hostPort)
go func() { fmt.Println(http.ListenAndServe(hostPort, nil)) }()
}
}
// Starts the function profiler. This is only supported when built with
// '-tags build'.
@@ -35,6 +51,7 @@ func (c *Core) Init() {
bpub, bpriv := newBoxKeys()
spub, spriv := newSigKeys()
c.init(bpub, bpriv, spub, spriv)
c.switchTable.start()
c.router.start()
}
@@ -67,7 +84,7 @@ func (c *Core) DEBUG_getPeers() *peers {
func (ps *peers) DEBUG_newPeer(box boxPubKey, sig sigPubKey, link boxSharedKey) *peer {
//in <-chan []byte,
//out chan<- []byte) *peer {
return ps.newPeer(&box, &sig, &link) //, in, out)
return ps.newPeer(&box, &sig, &link, "(simulator)") //, in, out)
}
/*
@@ -126,7 +143,42 @@ func (l *switchLocator) DEBUG_getCoords() []byte {
}
func (c *Core) DEBUG_switchLookup(dest []byte) switchPort {
return c.switchTable.lookup(dest)
return c.switchTable.DEBUG_lookup(dest)
}
// This does the switch layer lookups that decide how to route traffic.
// Traffic uses greedy routing in a metric space, where the metric distance between nodes is equal to the distance between them on the tree.
// Traffic must be routed to a node that is closer to the destination via the metric space distance.
// In the event that two nodes are equally close, it gets routed to the one with the longest uptime (due to the order that things are iterated over).
// The size of the outgoing packet queue is added to a node's tree distance when the cost of forwarding to a node, subject to the constraint that the real tree distance puts them closer to the destination than ourself.
// Doing so adds a limited form of backpressure routing, based on local information, which allows us to forward traffic around *local* bottlenecks, provided that another greedy path exists.
func (t *switchTable) DEBUG_lookup(dest []byte) switchPort {
table := t.getTable()
myDist := table.self.dist(dest)
if myDist == 0 {
return 0
}
// cost is in units of (expected distance) + (expected queue size), where expected distance is used as an approximation of the minimum backpressure gradient needed for packets to flow
ports := t.core.peers.getPorts()
var best switchPort
bestCost := int64(^uint64(0) >> 1)
for _, info := range table.elems {
dist := info.locator.dist(dest)
if !(dist < myDist) {
continue
}
//p, isIn := ports[info.port]
_, isIn := ports[info.port]
if !isIn {
continue
}
cost := int64(dist) // + p.getQueueSize()
if cost < bestCost {
best = info.port
bestCost = cost
}
}
return best
}
/*
@@ -177,27 +229,25 @@ func DEBUG_wire_encode_coords(coords []byte) []byte {
// DHT, via core
func (c *Core) DEBUG_getDHTSize() int {
total := 0
for bidx := 0; bidx < c.dht.nBuckets(); bidx++ {
b := c.dht.getBucket(bidx)
total += len(b.peers)
total += len(b.other)
}
var total int
c.router.doAdmin(func() {
total = len(c.dht.table)
})
return total
}
// TUN defaults
func (c *Core) DEBUG_GetTUNDefaultIfName() string {
return getDefaults().defaultIfName
return defaults.GetDefaults().DefaultIfName
}
func (c *Core) DEBUG_GetTUNDefaultIfMTU() int {
return getDefaults().defaultIfMTU
return defaults.GetDefaults().DefaultIfMTU
}
func (c *Core) DEBUG_GetTUNDefaultIfTAPMode() bool {
return getDefaults().defaultIfTAPMode
return defaults.GetDefaults().DefaultIfTAPMode
}
// udpInterface
@@ -347,12 +397,13 @@ func (c *Core) DEBUG_maybeSendUDPKeys(saddr string) {
////////////////////////////////////////////////////////////////////////////////
func (c *Core) DEBUG_addPeer(addr string) {
err := c.admin.addPeer(addr)
err := c.admin.addPeer(addr, "")
if err != nil {
panic(err)
}
}
/*
func (c *Core) DEBUG_addSOCKSConn(socksaddr, peeraddr string) {
go func() {
dialer, err := proxy.SOCKS5("tcp", socksaddr, nil, proxy.Direct)
@@ -370,10 +421,11 @@ func (c *Core) DEBUG_addSOCKSConn(socksaddr, peeraddr string) {
}
}()
}
*/
//*
func (c *Core) DEBUG_setupAndStartGlobalTCPInterface(addrport string) {
if err := c.tcp.init(c, addrport); err != nil {
if err := c.tcp.init(c, addrport, 0); err != nil {
c.log.Println("Failed to start TCP interface:", err)
panic(err)
}
@@ -384,7 +436,7 @@ func (c *Core) DEBUG_getGlobalTCPAddr() *net.TCPAddr {
}
func (c *Core) DEBUG_addTCPConn(saddr string) {
c.tcp.call(saddr)
c.tcp.call(saddr, nil, "")
}
//*/
@@ -452,25 +504,45 @@ func (c *Core) DEBUG_addAllowedEncryptionPublicKey(boxStr string) {
func DEBUG_simLinkPeers(p, q *peer) {
// Sets q.out() to point to p and starts p.linkLoop()
p.linkOut, q.linkOut = make(chan []byte, 1), make(chan []byte, 1)
go func() {
for bs := range p.linkOut {
q.handlePacket(bs)
goWorkers := func(source, dest *peer) {
source.linkOut = make(chan []byte, 1)
send := make(chan []byte, 1)
source.out = func(bs []byte) {
send <- bs
}
}()
go func() {
for bs := range q.linkOut {
p.handlePacket(bs)
}
}()
p.out = func(bs []byte) {
go q.handlePacket(bs)
go source.linkLoop()
go func() {
var packets [][]byte
for {
select {
case packet := <-source.linkOut:
packets = append(packets, packet)
continue
case packet := <-send:
packets = append(packets, packet)
source.core.switchTable.idleIn <- source.port
continue
default:
}
if len(packets) > 0 {
dest.handlePacket(packets[0])
packets = packets[1:]
continue
}
select {
case packet := <-source.linkOut:
packets = append(packets, packet)
case packet := <-send:
packets = append(packets, packet)
source.core.switchTable.idleIn <- source.port
}
}
}()
}
q.out = func(bs []byte) {
go p.handlePacket(bs)
}
go p.linkLoop()
go q.linkLoop()
goWorkers(p, q)
goWorkers(q, p)
p.core.switchTable.idleIn <- p.port
q.core.switchTable.idleIn <- q.port
}
func (c *Core) DEBUG_simFixMTU() {

View File

@@ -1,38 +1,15 @@
package yggdrasil
/*
This part has the (kademlia-like) distributed hash table
It's used to look up coords for a NodeID
Every node participates in the DHT, and the DHT stores no real keys/values
(Only the peer relationships / lookups are needed)
This version is intentionally fragile, by being recursive instead of iterative
(it's also not parallel, as a result)
This is to make sure that DHT black holes are visible if they exist
(the iterative parallel approach tends to get around them sometimes)
I haven't seen this get stuck on blackholes, but I also haven't proven it can't
Slight changes *do* make it blackhole hard, bootstrapping isn't an easy problem
*/
// A chord-like Distributed Hash Table (DHT).
// Used to look up coords given a NodeID and bitmask (taken from an IPv6 address).
// Keeps track of immediate successor, predecessor, and all peers.
// Also keeps track of other nodes if they're closer in tree space than all other known nodes encountered when heading in either direction to that point, under the hypothesis that, for the kinds of networks we care about, this should probabilistically include the node needed to keep lookups to near O(logn) steps.
import (
"sort"
"time"
)
// Number of DHT buckets, equal to the number of bits in a NodeID.
// Note that, in practice, nearly all of these will be empty.
const dht_bucket_number = 8 * NodeIDLen
// Number of nodes to keep in each DHT bucket.
// Additional entries may be kept for peers, for bootstrapping reasons, if they don't already have an entry in the bucket.
const dht_bucket_size = 2
// Number of responses to include in a lookup.
// If extras are given, they will be truncated from the response handler to prevent abuse.
const dht_lookup_size = 16
// dhtInfo represents everything we know about a node in the DHT.
@@ -41,11 +18,9 @@ type dhtInfo struct {
nodeID_hidden *NodeID
key boxPubKey
coords []byte
send time.Time // When we last sent a message
recv time.Time // When we last received a message
pings int // Decide when to drop
throttle time.Duration // Time to wait before pinging a node to bootstrap buckets, increases exponentially from 1 second to 1 minute
bootstrapSend time.Time // The time checked/updated as part of throttle checks
recv time.Time // When we last received a message
pings int // Time out if at least 3 consecutive maintenance pings drop
throttle time.Duration
}
// Returns the *NodeID associated with dhtInfo.key, calculating it on the fly the first time or from a cache all subsequent times.
@@ -56,12 +31,6 @@ func (info *dhtInfo) getNodeID() *NodeID {
return info.nodeID_hidden
}
// The nodes we known in a bucket (a region of keyspace with a matching prefix of some length).
type bucket struct {
peers []*dhtInfo
other []*dhtInfo
}
// Request for a node to do a lookup.
// Includes our key and coords so they can send a response back, and the destination NodeID we want to ask about.
type dhtReq struct {
@@ -74,30 +43,28 @@ type dhtReq struct {
// Includes the key and coords of the node that's responding, and the destination they were asked about.
// The main part is Infos []*dhtInfo, the lookup response.
type dhtRes struct {
Key boxPubKey // key to respond to
Coords []byte // coords to respond to
Key boxPubKey // key of the sender
Coords []byte // coords of the sender
Dest NodeID
Infos []*dhtInfo // response
}
// Information about a node, either taken from our table or from a lookup response.
// Used to schedule pings at a later time (they're throttled to 1/second for background maintenance traffic).
type dht_rumor struct {
info *dhtInfo
target *NodeID
// Parts of a DHT req usable as a key in a map.
type dhtReqKey struct {
key boxPubKey
dest NodeID
}
// The main DHT struct.
// Includes a slice of buckets, to organize known nodes based on their region of keyspace.
// Also includes information about outstanding DHT requests and the rumor mill of nodes to ping at some point.
type dht struct {
core *Core
nodeID NodeID
buckets_hidden [dht_bucket_number]bucket // Extra is for the self-bucket
peers chan *dhtInfo // other goroutines put incoming dht updates here
reqs map[boxPubKey]map[NodeID]time.Time
offset int
rumorMill []dht_rumor
core *Core
nodeID NodeID
peers chan *dhtInfo // other goroutines put incoming dht updates here
reqs map[dhtReqKey]time.Time // Keeps track of recent outstanding requests
callbacks map[dhtReqKey]dht_callbackInfo // Search and admin lookup callbacks
// These next two could be replaced by a single linked list or similar...
table map[NodeID]*dhtInfo
imp []*dhtInfo
}
// Initializes the DHT.
@@ -105,11 +72,98 @@ func (t *dht) init(c *Core) {
t.core = c
t.nodeID = *t.core.GetNodeID()
t.peers = make(chan *dhtInfo, 1024)
t.reqs = make(map[boxPubKey]map[NodeID]time.Time)
t.callbacks = make(map[dhtReqKey]dht_callbackInfo)
t.reset()
}
// Resets the DHT in response to coord changes.
// This empties all info from the DHT and drops outstanding requests.
func (t *dht) reset() {
t.reqs = make(map[dhtReqKey]time.Time)
t.table = make(map[NodeID]*dhtInfo)
t.imp = nil
}
// Does a DHT lookup and returns up to dht_lookup_size results.
func (t *dht) lookup(nodeID *NodeID, everything bool) []*dhtInfo {
results := make([]*dhtInfo, 0, len(t.table))
for _, info := range t.table {
results = append(results, info)
}
if len(results) > dht_lookup_size {
// Drop the middle part, so we keep some nodes before and after.
// This should help to bootstrap / recover more quickly.
sort.SliceStable(results, func(i, j int) bool {
return dht_ordered(nodeID, results[i].getNodeID(), results[j].getNodeID())
})
newRes := make([]*dhtInfo, 0, len(results))
newRes = append(newRes, results[len(results)-dht_lookup_size/2:]...)
newRes = append(newRes, results[:len(results)-dht_lookup_size/2]...)
results = newRes
results = results[:dht_lookup_size]
}
return results
}
// Insert into table, preserving the time we last sent a packet if the node was already in the table, otherwise setting that time to now.
func (t *dht) insert(info *dhtInfo) {
if *info.getNodeID() == t.nodeID {
// This shouldn't happen, but don't add it if it does
return
}
info.recv = time.Now()
if oldInfo, isIn := t.table[*info.getNodeID()]; isIn {
sameCoords := true
if len(info.coords) != len(oldInfo.coords) {
sameCoords = false
} else {
for idx := 0; idx < len(info.coords); idx++ {
if info.coords[idx] != oldInfo.coords[idx] {
sameCoords = false
break
}
}
}
if sameCoords {
info.throttle = oldInfo.throttle
}
}
t.imp = nil // It needs to update to get a pointer to the new info
t.table[*info.getNodeID()] = info
}
// Return true if first/second/third are (partially) ordered correctly.
func dht_ordered(first, second, third *NodeID) bool {
lessOrEqual := func(first, second *NodeID) bool {
for idx := 0; idx < NodeIDLen; idx++ {
if first[idx] > second[idx] {
return false
}
if first[idx] < second[idx] {
return true
}
}
return true
}
firstLessThanSecond := lessOrEqual(first, second)
secondLessThanThird := lessOrEqual(second, third)
thirdLessThanFirst := lessOrEqual(third, first)
switch {
case firstLessThanSecond && secondLessThanThird:
// Nothing wrapped around 0, the easy case
return true
case thirdLessThanFirst && firstLessThanSecond:
// Third wrapped around 0
return true
case secondLessThanThird && thirdLessThanFirst:
// Second (and third) wrapped around 0
return true
}
return false
}
// Reads a request, performs a lookup, and responds.
// If the node that sent the request isn't in our DHT, but should be, then we add them.
// Update info about the node that sent the request.
func (t *dht) handleReq(req *dhtReq) {
// Send them what they asked for
loc := t.core.switchTable.getLocator()
@@ -121,270 +175,14 @@ func (t *dht) handleReq(req *dhtReq) {
Infos: t.lookup(&req.Dest, false),
}
t.sendRes(&res, req)
// Also (possibly) add them to our DHT
// Also add them to our DHT
info := dhtInfo{
key: req.Key,
coords: req.Coords,
}
t.insertIfNew(&info, false) // This seems DoSable (we just trust their coords...)
//if req.dest != t.nodeID { t.ping(&info, info.getNodeID()) } // Or spam...
}
// Reads a lookup response, checks that we had sent a matching request, and processes the response info.
// This mainly consists of updating the node we asked in our DHT (they responded, so we know they're still alive), and adding the response info to the rumor mill.
func (t *dht) handleRes(res *dhtRes) {
t.core.searches.handleDHTRes(res)
reqs, isIn := t.reqs[res.Key]
if !isIn {
return
if _, isIn := t.table[*info.getNodeID()]; !isIn && t.isImportant(&info) {
t.ping(&info, nil)
}
_, isIn = reqs[res.Dest]
if !isIn {
return
}
now := time.Now()
rinfo := dhtInfo{
key: res.Key,
coords: res.Coords,
send: now, // Technically wrong but should be OK...
recv: now,
throttle: time.Second,
bootstrapSend: now,
}
// If they're already in the table, then keep the correct send time
bidx, isOK := t.getBucketIndex(rinfo.getNodeID())
if !isOK {
return
}
b := t.getBucket(bidx)
for _, oldinfo := range b.peers {
if oldinfo.key == rinfo.key {
rinfo.send = oldinfo.send
rinfo.throttle = oldinfo.throttle
rinfo.bootstrapSend = oldinfo.bootstrapSend
}
}
for _, oldinfo := range b.other {
if oldinfo.key == rinfo.key {
rinfo.send = oldinfo.send
rinfo.throttle = oldinfo.throttle
rinfo.bootstrapSend = oldinfo.bootstrapSend
}
}
// Insert into table
t.insert(&rinfo, false)
if res.Dest == *rinfo.getNodeID() {
return
} // No infinite recursions
if len(res.Infos) > dht_lookup_size {
// Ignore any "extra" lookup results
res.Infos = res.Infos[:dht_lookup_size]
}
for _, info := range res.Infos {
if dht_firstCloserThanThird(info.getNodeID(), &res.Dest, rinfo.getNodeID()) {
t.addToMill(info, info.getNodeID())
}
}
}
// Does a DHT lookup and returns the results, sorted in ascending order of distance from the destination.
func (t *dht) lookup(nodeID *NodeID, allowCloser bool) []*dhtInfo {
// FIXME this allocates a bunch, sorts, and keeps the part it likes
// It would be better to only track the part it likes to begin with
addInfos := func(res []*dhtInfo, infos []*dhtInfo) []*dhtInfo {
for _, info := range infos {
if info == nil {
panic("Should never happen!")
}
if allowCloser || dht_firstCloserThanThird(info.getNodeID(), nodeID, &t.nodeID) {
res = append(res, info)
}
}
return res
}
var res []*dhtInfo
for bidx := 0; bidx < t.nBuckets(); bidx++ {
b := t.getBucket(bidx)
res = addInfos(res, b.peers)
res = addInfos(res, b.other)
}
doSort := func(infos []*dhtInfo) {
less := func(i, j int) bool {
return dht_firstCloserThanThird(infos[i].getNodeID(),
nodeID,
infos[j].getNodeID())
}
sort.SliceStable(infos, less)
}
doSort(res)
if len(res) > dht_lookup_size {
res = res[:dht_lookup_size]
}
return res
}
// Gets the bucket for a specified matching prefix length.
func (t *dht) getBucket(bidx int) *bucket {
return &t.buckets_hidden[bidx]
}
// Lists the number of buckets.
func (t *dht) nBuckets() int {
return len(t.buckets_hidden)
}
// Inserts a node into the DHT if they meet certain requirements.
// In particular, they must either be a peer that's not already in the DHT, or else be someone we should insert into the DHT (see: shouldInsert).
func (t *dht) insertIfNew(info *dhtInfo, isPeer bool) {
// Insert if no "other" entry already exists
nodeID := info.getNodeID()
bidx, isOK := t.getBucketIndex(nodeID)
if !isOK {
return
}
b := t.getBucket(bidx)
if (isPeer && !b.containsOther(info)) || t.shouldInsert(info) {
// We've never heard this node before
// TODO is there a better time than "now" to set send/recv to?
// (Is there another "natural" choice that bootstraps faster?)
info.send = time.Now()
info.recv = info.send
t.insert(info, isPeer)
}
}
// Adds a node to the DHT, possibly removing another node in the process.
func (t *dht) insert(info *dhtInfo, isPeer bool) {
// First update the time on this info
info.recv = time.Now()
// Get the bucket for this node
nodeID := info.getNodeID()
bidx, isOK := t.getBucketIndex(nodeID)
if !isOK {
return
}
b := t.getBucket(bidx)
if !isPeer && !b.containsOther(info) {
// This is a new entry, give it an old age so it's pinged sooner
// This speeds up bootstrapping
info.recv = info.recv.Add(-time.Hour)
}
if isPeer || info.throttle > time.Minute {
info.throttle = time.Minute
}
// First drop any existing entry from the bucket
b.drop(&info.key)
// Now add to the *end* of the bucket
if isPeer {
// TODO make sure we don't duplicate peers in b.other too
b.peers = append(b.peers, info)
return
}
b.other = append(b.other, info)
// Shrink from the *front* to requied size
for len(b.other) > dht_bucket_size {
b.other = b.other[1:]
}
}
// Gets the bucket index for the bucket where we would put the given NodeID.
func (t *dht) getBucketIndex(nodeID *NodeID) (int, bool) {
for bidx := 0; bidx < t.nBuckets(); bidx++ {
them := nodeID[bidx/8] & (0x80 >> byte(bidx%8))
me := t.nodeID[bidx/8] & (0x80 >> byte(bidx%8))
if them != me {
return bidx, true
}
}
return t.nBuckets(), false
}
// Helper called by containsPeer, containsOther, and contains.
// Returns true if a node with the same ID *and coords* is already in the given part of the bucket.
func dht_bucket_check(newInfo *dhtInfo, infos []*dhtInfo) bool {
// Compares if key and coords match
if newInfo == nil {
panic("Should never happen")
}
for _, info := range infos {
if info == nil {
panic("Should never happen")
}
if info.key != newInfo.key {
continue
}
if len(info.coords) != len(newInfo.coords) {
continue
}
match := true
for idx := 0; idx < len(info.coords); idx++ {
if info.coords[idx] != newInfo.coords[idx] {
match = false
break
}
}
if match {
return true
}
}
return false
}
// Calls bucket_check over the bucket's peers infos.
func (b *bucket) containsPeer(info *dhtInfo) bool {
return dht_bucket_check(info, b.peers)
}
// Calls bucket_check over the bucket's other info.
func (b *bucket) containsOther(info *dhtInfo) bool {
return dht_bucket_check(info, b.other)
}
// returns containsPeer || containsOther
func (b *bucket) contains(info *dhtInfo) bool {
return b.containsPeer(info) || b.containsOther(info)
}
// Removes a node with the corresponding key, if any, from a bucket.
func (b *bucket) drop(key *boxPubKey) {
clean := func(infos []*dhtInfo) []*dhtInfo {
cleaned := infos[:0]
for _, info := range infos {
if info.key == *key {
continue
}
cleaned = append(cleaned, info)
}
return cleaned
}
b.peers = clean(b.peers)
b.other = clean(b.other)
}
// Sends a lookup request to the specified node.
func (t *dht) sendReq(req *dhtReq, dest *dhtInfo) {
// Send a dhtReq to the node in dhtInfo
bs := req.encode()
shared := t.core.sessions.getSharedKey(&t.core.boxPriv, &dest.key)
payload, nonce := boxSeal(shared, bs, nil)
p := wire_protoTrafficPacket{
Coords: dest.coords,
ToKey: dest.key,
FromKey: t.core.boxPub,
Nonce: *nonce,
Payload: payload,
}
packet := p.encode()
t.core.router.out(packet)
reqsToDest, isIn := t.reqs[dest.key]
if !isIn {
t.reqs[dest.key] = make(map[NodeID]time.Time)
reqsToDest, isIn = t.reqs[dest.key]
if !isIn {
panic("This should never happen")
}
}
reqsToDest[req.Dest] = time.Now()
}
// Sends a lookup response to the specified node.
@@ -404,56 +202,73 @@ func (t *dht) sendRes(res *dhtRes, req *dhtReq) {
t.core.router.out(packet)
}
// Returns true of a bucket contains no peers and no other nodes.
func (b *bucket) isEmpty() bool {
return len(b.peers)+len(b.other) == 0
type dht_callbackInfo struct {
f func(*dhtRes)
time time.Time
}
// Gets the next node that should be pinged from the bucket.
// There's a cooldown of 6 seconds between ping attempts for each node, to give them time to respond.
// It returns the least recently pinged node, subject to that send cooldown.
func (b *bucket) nextToPing() *dhtInfo {
// Check the nodes in the bucket
// Return whichever one responded least recently
// Delay of 6 seconds between pinging the same node
// Gives them time to respond
// And time between traffic loss from short term congestion in the network
var toPing *dhtInfo
update := func(infos []*dhtInfo) {
for _, next := range infos {
if time.Since(next.send) < 6*time.Second {
continue
}
if toPing == nil || next.recv.Before(toPing.recv) {
toPing = next
}
}
// Adds a callback and removes it after some timeout.
func (t *dht) addCallback(rq *dhtReqKey, callback func(*dhtRes)) {
info := dht_callbackInfo{callback, time.Now().Add(6 * time.Second)}
t.callbacks[*rq] = info
}
// Reads a lookup response, checks that we had sent a matching request, and processes the response info.
// This mainly consists of updating the node we asked in our DHT (they responded, so we know they're still alive), and deciding if we want to do anything with their responses
func (t *dht) handleRes(res *dhtRes) {
rq := dhtReqKey{res.Key, res.Dest}
if callback, isIn := t.callbacks[rq]; isIn {
callback.f(res)
delete(t.callbacks, rq)
}
update(b.peers)
update(b.other)
return toPing
}
// Returns a useful target address to ask about for pings.
// Equal to the our node's ID, except for exactly 1 bit at the bucket index.
func (t *dht) getTarget(bidx int) *NodeID {
targetID := t.nodeID
targetID[bidx/8] ^= 0x80 >> byte(bidx%8)
return &targetID
}
// Sends a ping to a node, or removes the node if it has failed to respond to too many pings.
// If target is nil, we will ask the node about our own NodeID.
func (t *dht) ping(info *dhtInfo, target *NodeID) {
if info.pings > 2 {
bidx, isOK := t.getBucketIndex(info.getNodeID())
if !isOK {
panic("This should never happen")
}
b := t.getBucket(bidx)
b.drop(&info.key)
_, isIn := t.reqs[rq]
if !isIn {
return
}
delete(t.reqs, rq)
rinfo := dhtInfo{
key: res.Key,
coords: res.Coords,
}
if t.isImportant(&rinfo) {
t.insert(&rinfo)
}
for _, info := range res.Infos {
if *info.getNodeID() == t.nodeID {
continue
} // Skip self
if _, isIn := t.table[*info.getNodeID()]; isIn {
// TODO? don't skip if coords are different?
continue
}
if t.isImportant(info) {
t.ping(info, nil)
}
}
}
// Sends a lookup request to the specified node.
func (t *dht) sendReq(req *dhtReq, dest *dhtInfo) {
// Send a dhtReq to the node in dhtInfo
bs := req.encode()
shared := t.core.sessions.getSharedKey(&t.core.boxPriv, &dest.key)
payload, nonce := boxSeal(shared, bs, nil)
p := wire_protoTrafficPacket{
Coords: dest.coords,
ToKey: dest.key,
FromKey: t.core.boxPub,
Nonce: *nonce,
Payload: payload,
}
packet := p.encode()
t.core.router.out(packet)
rq := dhtReqKey{dest.key, req.Dest}
t.reqs[rq] = time.Now()
}
// Sends a lookup to this info, looking for the target.
func (t *dht) ping(info *dhtInfo, target *NodeID) {
// Creates a req for the node at dhtInfo, asking them about the target (if one is given) or themself (if no target is given)
if target == nil {
target = &t.nodeID
}
@@ -464,160 +279,120 @@ func (t *dht) ping(info *dhtInfo, target *NodeID) {
Coords: coords,
Dest: *target,
}
info.pings++
info.send = time.Now()
t.sendReq(&req, info)
}
// Adds a node info and target to the rumor mill.
// The node will be asked about the target at a later point, if doing so would still be useful at the time.
func (t *dht) addToMill(info *dhtInfo, target *NodeID) {
rumor := dht_rumor{
info: info,
target: target,
}
t.rumorMill = append(t.rumorMill, rumor)
}
// Regular periodic maintenance.
// If the mill is empty, it adds two pings to the rumor mill.
// The first is to the node that responded least recently, provided that it's been at least 1 minute, to make sure we eventually detect and remove unresponsive nodes.
// The second is used for bootstrapping, and attempts to fill some bucket, iterating over buckets and resetting after it hits the last non-empty one.
// If the mill is not empty, it pops nodes from the mill until it finds one that would be useful to ping (see: shouldInsert), and then pings it.
// Periodic maintenance work to keep important DHT nodes alive.
func (t *dht) doMaintenance() {
// First clean up reqs
for key, reqs := range t.reqs {
for target, timeout := range reqs {
if time.Since(timeout) > time.Minute {
delete(reqs, target)
}
}
if len(reqs) == 0 {
delete(t.reqs, key)
now := time.Now()
newReqs := make(map[dhtReqKey]time.Time, len(t.reqs))
for key, start := range t.reqs {
if now.Sub(start) < 6*time.Second {
newReqs[key] = start
}
}
if len(t.rumorMill) == 0 {
// Ping the least recently contacted node
// This is to make sure we eventually notice when someone times out
var oldest *dhtInfo
last := 0
for bidx := 0; bidx < t.nBuckets(); bidx++ {
b := t.getBucket(bidx)
if !b.isEmpty() {
last = bidx
toPing := b.nextToPing()
if toPing == nil {
continue
} // We've recently pinged everyone in b
if oldest == nil || toPing.recv.Before(oldest.recv) {
oldest = toPing
}
}
t.reqs = newReqs
newCallbacks := make(map[dhtReqKey]dht_callbackInfo, len(t.callbacks))
for key, callback := range t.callbacks {
if now.Before(callback.time) {
newCallbacks[key] = callback
}
if oldest != nil && time.Since(oldest.recv) > time.Minute {
// Ping the oldest node in the DHT, but don't ping nodes that have been checked within the last minute
t.addToMill(oldest, nil)
}
// Refresh buckets
if t.offset > last {
t.offset = 0
}
target := t.getTarget(t.offset)
func() {
closer := t.lookup(target, false)
for _, info := range closer {
// Throttled ping of a node that's closer to the destination
if time.Since(info.recv) > info.throttle {
t.addToMill(info, target)
t.offset++
info.bootstrapSend = time.Now()
info.throttle *= 2
if info.throttle > time.Minute {
info.throttle = time.Minute
}
return
}
}
if len(closer) == 0 {
// If we don't know of anyone closer at all, then there's a hole in our dht
// Ping the closest node we know and ignore the throttle, to try to fill it
for _, info := range t.lookup(target, true) {
t.addToMill(info, target)
t.offset++
return
}
}
}()
//t.offset++
}
for len(t.rumorMill) > 0 {
var rumor dht_rumor
rumor, t.rumorMill = t.rumorMill[0], t.rumorMill[1:]
if rumor.target == rumor.info.getNodeID() {
// Note that the above is a pointer comparison, and target can be nil
// This is only for adding new nodes (learned from other lookups)
// It only makes sense to ping if the node isn't already in the table
if !t.shouldInsert(rumor.info) {
continue
t.callbacks = newCallbacks
for infoID, info := range t.table {
if now.Sub(info.recv) > time.Minute || info.pings > 3 {
delete(t.table, infoID)
t.imp = nil
}
}
for _, info := range t.getImportant() {
if now.Sub(info.recv) > info.throttle {
t.ping(info, nil)
info.pings++
info.throttle += time.Second
if info.throttle > 30*time.Second {
info.throttle = 30 * time.Second
}
}
t.ping(rumor.info, rumor.target)
break
}
}
// Returns true if it would be worth pinging the specified node.
// This requires that the bucket doesn't already contain the node, and that either the bucket isn't full yet or the node is closer to us in keyspace than some other node in that bucket.
func (t *dht) shouldInsert(info *dhtInfo) bool {
bidx, isOK := t.getBucketIndex(info.getNodeID())
if !isOK {
// Gets a list of important nodes, used by isImportant.
func (t *dht) getImportant() []*dhtInfo {
if t.imp == nil {
// Get a list of all known nodes
infos := make([]*dhtInfo, 0, len(t.table))
for _, info := range t.table {
infos = append(infos, info)
}
// Sort them by increasing order in distance along the ring
sort.SliceStable(infos, func(i, j int) bool {
// Sort in order of predecessors (!), reverse from chord normal, because it plays nicer with zero bits for unknown parts of target addresses
return dht_ordered(infos[j].getNodeID(), infos[i].getNodeID(), &t.nodeID)
})
// Keep the ones that are no further than the closest seen so far
minDist := ^uint64(0)
loc := t.core.switchTable.getLocator()
important := infos[:0]
for _, info := range infos {
dist := uint64(loc.dist(info.coords))
if dist < minDist {
minDist = dist
important = append(important, info)
}
}
var temp []*dhtInfo
minDist = ^uint64(0)
for idx := len(infos) - 1; idx >= 0; idx-- {
info := infos[idx]
dist := uint64(loc.dist(info.coords))
if dist < minDist {
minDist = dist
temp = append(temp, info)
}
}
for idx := len(temp) - 1; idx >= 0; idx-- {
important = append(important, temp[idx])
}
t.imp = important
}
return t.imp
}
// Returns true if this is a node we need to keep track of for the DHT to work.
func (t *dht) isImportant(ninfo *dhtInfo) bool {
if ninfo.key == t.core.boxPub {
return false
}
b := t.getBucket(bidx)
if b.containsOther(info) {
return false
}
if len(b.other) < dht_bucket_size {
return true
}
for _, other := range b.other {
if dht_firstCloserThanThird(info.getNodeID(), &t.nodeID, other.getNodeID()) {
important := t.getImportant()
// Check if ninfo is of equal or greater importance to what we already know
loc := t.core.switchTable.getLocator()
ndist := uint64(loc.dist(ninfo.coords))
minDist := ^uint64(0)
for _, info := range important {
if (*info.getNodeID() == *ninfo.getNodeID()) ||
(ndist < minDist && dht_ordered(info.getNodeID(), ninfo.getNodeID(), &t.nodeID)) {
// Either the same node, or a better one
return true
}
dist := uint64(loc.dist(info.coords))
if dist < minDist {
minDist = dist
}
}
minDist = ^uint64(0)
for idx := len(important) - 1; idx >= 0; idx-- {
info := important[idx]
if (*info.getNodeID() == *ninfo.getNodeID()) ||
(ndist < minDist && dht_ordered(&t.nodeID, ninfo.getNodeID(), info.getNodeID())) {
// Either the same node, or a better one
return true
}
dist := uint64(loc.dist(info.coords))
if dist < minDist {
minDist = dist
}
}
// We didn't find any important node that ninfo is better than
return false
}
// Returns true if the keyspace distance between the first and second node is smaller than the keyspace distance between the second and third node.
func dht_firstCloserThanThird(first *NodeID,
second *NodeID,
third *NodeID) bool {
for idx := 0; idx < NodeIDLen; idx++ {
f := first[idx] ^ second[idx]
t := third[idx] ^ second[idx]
if f == t {
continue
}
return f < t
}
return false
}
// Resets the DHT in response to coord changes.
// This empties all buckets, resets the bootstrapping cycle to 0, and empties the rumor mill.
// It adds all old "other" node info to the rumor mill, so they'll be pinged quickly.
// If those nodes haven't also changed coords, then this is a relatively quick way to notify those nodes of our new coords and re-add them to our own DHT if they respond.
func (t *dht) reset() {
// This is mostly so bootstrapping will reset to resend coords into the network
t.offset = 0
t.rumorMill = nil // reset mill
for _, b := range t.buckets_hidden {
b.peers = b.peers[:0]
for _, info := range b.other {
// Add other nodes to the rumor mill so they'll be pinged soon
// This will hopefully tell them our coords and re-learn theirs quickly if they haven't changed
t.addToMill(info, info.getNodeID())
}
b.other = b.other[:0]
}
}

View File

@@ -13,6 +13,7 @@ import (
"encoding/binary"
"errors"
"net"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv6"
@@ -23,11 +24,17 @@ type macAddress [6]byte
const len_ETHER = 14
type icmpv6 struct {
tun *tunDevice
peermac macAddress
peerlladdr net.IP
mylladdr net.IP
mymac macAddress
tun *tunDevice
mylladdr net.IP
mymac macAddress
peermacs map[address]neighbor
}
type neighbor struct {
mac macAddress
learned bool
lastadvertisement time.Time
lastsolicitation time.Time
}
// Marshal returns the binary encoding of h.
@@ -52,13 +59,16 @@ func ipv6Header_Marshal(h *ipv6.Header) ([]byte, error) {
// addresses.
func (i *icmpv6) init(t *tunDevice) {
i.tun = t
i.peermacs = make(map[address]neighbor)
// Our MAC address and link-local address
copy(i.mymac[:], []byte{
0x02, 0x00, 0x00, 0x00, 0x00, 0x02})
i.mymac = macAddress{
0x02, 0x00, 0x00, 0x00, 0x00, 0x02}
i.mylladdr = net.IP{
0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFE}
copy(i.mymac[:], i.tun.core.router.addr[:])
copy(i.mylladdr[9:], i.tun.core.router.addr[1:])
}
// Parses an incoming ICMPv6 packet. The packet provided may be either an
@@ -73,7 +83,7 @@ func (i *icmpv6) parse_packet(datain []byte) {
if i.tun.iface.IsTAP() {
response, err = i.parse_packet_tap(datain)
} else {
response, err = i.parse_packet_tun(datain)
response, err = i.parse_packet_tun(datain, nil)
}
if err != nil {
@@ -89,16 +99,14 @@ func (i *icmpv6) parse_packet(datain []byte) {
// A response buffer is also created for the response message, also complete
// with ethernet headers.
func (i *icmpv6) parse_packet_tap(datain []byte) ([]byte, error) {
// Store the peer MAC address
copy(i.peermac[:6], datain[6:12])
// Ignore non-IPv6 frames
if binary.BigEndian.Uint16(datain[12:14]) != uint16(0x86DD) {
return nil, nil
}
// Hand over to parse_packet_tun to interpret the IPv6 packet
ipv6packet, err := i.parse_packet_tun(datain[len_ETHER:])
mac := datain[6:12]
ipv6packet, err := i.parse_packet_tun(datain[len_ETHER:], &mac)
if err != nil {
return nil, err
}
@@ -120,7 +128,7 @@ func (i *icmpv6) parse_packet_tap(datain []byte) ([]byte, error) {
// sanity checks on the packet - i.e. is the packet an ICMPv6 packet, does the
// ICMPv6 message match a known expected type. The relevant handler function
// is then called and a response packet may be returned.
func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) {
func (i *icmpv6) parse_packet_tun(datain []byte, datamac *[]byte) ([]byte, error) {
// Parse the IPv6 packet headers
ipv6Header, err := ipv6.ParseHeader(datain[:ipv6.HeaderLen])
if err != nil {
@@ -137,9 +145,6 @@ func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) {
return nil, err
}
// Store the peer link local address, it will come in useful later
copy(i.peerlladdr[:], ipv6Header.Src[:])
// Parse the ICMPv6 message contents
icmpv6Header, err := icmp.ParseMessage(58, datain[ipv6.HeaderLen:])
if err != nil {
@@ -149,24 +154,35 @@ func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) {
// Check for a supported message type
switch icmpv6Header.Type {
case ipv6.ICMPTypeNeighborSolicitation:
{
response, err := i.handle_ndp(datain[ipv6.HeaderLen:])
if err == nil {
// Create our ICMPv6 response
responsePacket, err := i.create_icmpv6_tun(
ipv6Header.Src, i.mylladdr,
ipv6.ICMPTypeNeighborAdvertisement, 0,
&icmp.DefaultMessageBody{Data: response})
if err != nil {
return nil, err
}
// Send it back
return responsePacket, nil
} else {
response, err := i.handle_ndp(datain[ipv6.HeaderLen:])
if err == nil {
// Create our ICMPv6 response
responsePacket, err := i.create_icmpv6_tun(
ipv6Header.Src, i.mylladdr,
ipv6.ICMPTypeNeighborAdvertisement, 0,
&icmp.DefaultMessageBody{Data: response})
if err != nil {
return nil, err
}
// Send it back
return responsePacket, nil
} else {
return nil, err
}
case ipv6.ICMPTypeNeighborAdvertisement:
if datamac != nil {
var addr address
var mac macAddress
copy(addr[:], ipv6Header.Src[:])
copy(mac[:], (*datamac)[:])
neighbor := i.peermacs[addr]
neighbor.mac = mac
neighbor.learned = true
neighbor.lastadvertisement = time.Now()
i.peermacs[addr] = neighbor
}
return nil, errors.New("No response needed")
}
return nil, errors.New("ICMPv6 type not matched")
@@ -238,6 +254,42 @@ func (i *icmpv6) create_icmpv6_tun(dst net.IP, src net.IP, mtype ipv6.ICMPType,
return responsePacket, nil
}
func (i *icmpv6) create_ndp_tap(dst address) ([]byte, error) {
// Create the ND payload
var payload [28]byte
copy(payload[:4], []byte{0x00, 0x00, 0x00, 0x00})
copy(payload[4:20], dst[:])
copy(payload[20:22], []byte{0x01, 0x01})
copy(payload[22:28], i.mymac[:6])
// Create the ICMPv6 solicited-node address
var dstaddr address
copy(dstaddr[:13], []byte{
0xFF, 0x02, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0xFF})
copy(dstaddr[13:], dst[13:16])
// Create the multicast MAC
var dstmac macAddress
copy(dstmac[:2], []byte{0x33, 0x33})
copy(dstmac[2:6], dstaddr[12:16])
// Create the ND request
requestPacket, err := i.create_icmpv6_tap(
dstmac, dstaddr[:], i.mylladdr,
ipv6.ICMPTypeNeighborSolicitation, 0,
&icmp.DefaultMessageBody{Data: payload[:]})
if err != nil {
return nil, err
}
neighbor := i.peermacs[dstaddr]
neighbor.lastsolicitation = time.Now()
i.peermacs[dstaddr] = neighbor
return requestPacket, nil
}
// Generates a response to an NDP discovery packet. This is effectively called
// when the host operating system generates an NDP request for any address in
// the fd00::/8 range, so that the operating system knows to route that traffic
@@ -252,7 +304,7 @@ func (i *icmpv6) handle_ndp(in []byte) ([]byte, error) {
case source.isValid():
case snet.isValid():
default:
return nil, errors.New("Not an NDP for fd00::/8")
return nil, errors.New("Not an NDP for 0200::/7")
}
// Create our NDP message body response

View File

@@ -1,6 +1,7 @@
package yggdrasil
import (
"context"
"fmt"
"net"
"time"
@@ -35,7 +36,10 @@ func (m *multicast) start() error {
return err
}
listenString := fmt.Sprintf("[::]:%v", addr.Port)
conn, err := net.ListenPacket("udp6", listenString)
lc := net.ListenConfig{
Control: multicastReuse,
}
conn, err := lc.ListenPacket(context.Background(), "udp6", listenString)
if err != nil {
return err
}
@@ -153,6 +157,6 @@ func (m *multicast) listen() {
}
addr.Zone = from.Zone
saddr := addr.String()
m.core.tcp.connect(saddr)
m.core.tcp.connect(saddr, "")
}
}

View File

@@ -0,0 +1,9 @@
// +build !linux,!darwin,!netbsd,!freebsd,!openbsd,!dragonflybsd,!windows
package yggdrasil
import "syscall"
func multicastReuse(network string, address string, c syscall.RawConn) error {
return nil
}

View File

@@ -0,0 +1,22 @@
// +build linux darwin netbsd freebsd openbsd dragonflybsd
package yggdrasil
import "syscall"
import "golang.org/x/sys/unix"
func multicastReuse(network string, address string, c syscall.RawConn) error {
var control error
var reuseport error
control = c.Control(func(fd uintptr) {
reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
})
switch {
case reuseport != nil:
return reuseport
default:
return control
}
}

View File

@@ -0,0 +1,22 @@
// +build windows
package yggdrasil
import "syscall"
import "golang.org/x/sys/windows"
func multicastReuse(network string, address string, c syscall.RawConn) error {
var control error
var reuseaddr error
control = c.Control(func(fd uintptr) {
reuseaddr = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
})
switch {
case reuseaddr != nil:
return reuseaddr
default:
return control
}
}

View File

@@ -17,7 +17,7 @@ import (
type peers struct {
core *Core
mutex sync.Mutex // Synchronize writes to atomic
ports atomic.Value //map[Port]*peer, use CoW semantics
ports atomic.Value //map[switchPort]*peer, use CoW semantics
authMutex sync.RWMutex
allowedEncryptionPublicKeys map[boxPubKey]struct{}
}
@@ -74,43 +74,35 @@ func (ps *peers) putPorts(ports map[switchPort]*peer) {
ps.ports.Store(ports)
}
// Information known about a peer, including thier box/sig keys, precomputed shared keys (static and ephemeral), a handler for their outgoing traffic, and queue sizes for local backpressure.
// Information known about a peer, including thier box/sig keys, precomputed shared keys (static and ephemeral) and a handler for their outgoing traffic
type peer struct {
queueSize int64 // used to track local backpressure
bytesSent uint64 // To track bandwidth usage for getPeers
bytesRecvd uint64 // To track bandwidth usage for getPeers
// BUG: sync/atomic, 32 bit platforms need the above to be the first element
core *Core
port switchPort
box boxPubKey
sig sigPubKey
shared boxSharedKey
linkShared boxSharedKey
firstSeen time.Time // To track uptime for getPeers
linkOut (chan []byte) // used for protocol traffic (to bypass queues)
doSend (chan struct{}) // tell the linkLoop to send a switchMsg
dinfo *dhtInfo // used to keep the DHT working
out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes
close func() // Called when a peer is removed, to close the underlying connection, or via admin api
}
// Size of the queue of packets to be sent to the node.
func (p *peer) getQueueSize() int64 {
return atomic.LoadInt64(&p.queueSize)
}
// Used to increment or decrement the queue.
func (p *peer) updateQueueSize(delta int64) {
atomic.AddInt64(&p.queueSize, delta)
core *Core
port switchPort
box boxPubKey
sig sigPubKey
shared boxSharedKey
linkShared boxSharedKey
endpoint string
friendlyName string
firstSeen time.Time // To track uptime for getPeers
linkOut (chan []byte) // used for protocol traffic (to bypass queues)
doSend (chan struct{}) // tell the linkLoop to send a switchMsg
dinfo *dhtInfo // used to keep the DHT working
out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes
close func() // Called when a peer is removed, to close the underlying connection, or via admin api
}
// Creates a new peer with the specified box, sig, and linkShared keys, using the lowest unocupied port number.
func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey) *peer {
func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey, endpoint string) *peer {
now := time.Now()
p := peer{box: *box,
sig: *sig,
shared: *getSharedKey(&ps.core.boxPriv, box),
linkShared: *linkShared,
endpoint: endpoint,
firstSeen: now,
doSend: make(chan struct{}, 1),
core: ps.core}
@@ -138,7 +130,7 @@ func (ps *peers) removePeer(port switchPort) {
return
} // Can't remove self peer
ps.core.router.doAdmin(func() {
ps.core.switchTable.unlockedRemovePeer(port)
ps.core.switchTable.forgetPeer(port)
})
ps.mutex.Lock()
oldPorts := ps.getPorts()
@@ -183,7 +175,6 @@ func (p *peer) doSendSwitchMsgs() {
// This must be launched in a separate goroutine by whatever sets up the peer struct.
// It handles link protocol traffic.
func (p *peer) linkLoop() {
go p.doSendSwitchMsgs()
tick := time.NewTicker(time.Second)
defer tick.Stop()
for {
@@ -194,8 +185,11 @@ func (p *peer) linkLoop() {
}
p.sendSwitchMsg()
case _ = <-tick.C:
if p.dinfo != nil {
p.core.dht.peers <- p.dinfo
//break // FIXME disabled the below completely to test something
pdinfo := p.dinfo // FIXME this is a bad workarond NPE on the next line
if pdinfo != nil {
dinfo := *pdinfo
p.core.dht.peers <- &dinfo
}
}
}
@@ -229,19 +223,7 @@ func (p *peer) handleTraffic(packet []byte, pTypeLen int) {
// Drop traffic until the peer manages to send us at least one good switchMsg
return
}
coords, coordLen := wire_decode_coords(packet[pTypeLen:])
if coordLen >= len(packet) {
return
} // No payload
toPort := p.core.switchTable.lookup(coords)
if toPort == p.port {
return
}
to := p.core.peers.getPorts()[toPort]
if to == nil {
return
}
to.sendPacket(packet)
p.core.switchTable.packetIn <- packet
}
// This just calls p.out(packet) for now.
@@ -334,7 +316,7 @@ func (p *peer) handleSwitchMsg(packet []byte) {
sigMsg.Hops = msg.Hops[:idx]
loc.coords = append(loc.coords, hop.Port)
bs := getBytesForSig(&hop.Next, &sigMsg)
if !p.core.sigs.check(&prevKey, &hop.Sig, bs) {
if !verify(&prevKey, bs, &hop.Sig) {
p.core.peers.removePeer(p.port)
}
prevKey = hop.Next
@@ -353,7 +335,7 @@ func (p *peer) handleSwitchMsg(packet []byte) {
key: p.box,
coords: loc.getCoords(),
}
p.core.dht.peers <- &dinfo
//p.core.dht.peers <- &dinfo
p.dinfo = &dinfo
}

View File

@@ -1,14 +0,0 @@
// +build !debug
package yggdrasil
import (
"errors"
"log"
)
// Starts the function profiler. This is only supported when built with
// '-tags build'.
func StartProfiler(_ *log.Logger) error {
return errors.New("Release builds do not support -pprof, build using '-tags debug'")
}

View File

@@ -23,6 +23,7 @@ package yggdrasil
// The router then runs some sanity checks before passing it to the tun
import (
"bytes"
"time"
"golang.org/x/net/icmp"
@@ -32,22 +33,32 @@ import (
// The router struct has channels to/from the tun/tap device and a self peer (0), which is how messages are passed between this node and the peers/switch layer.
// The router's mainLoop goroutine is responsible for managing all information related to the dht, searches, and crypto sessions.
type router struct {
core *Core
addr address
in <-chan []byte // packets we received from the network, link to peer's "out"
out func([]byte) // packets we're sending to the network, link to peer's "in"
recv chan<- []byte // place where the tun pulls received packets from
send <-chan []byte // place where the tun puts outgoing packets
reset chan struct{} // signal that coords changed (re-init sessions/dht)
admin chan func() // pass a lambda for the admin socket to query stuff
core *Core
addr address
subnet subnet
in <-chan []byte // packets we received from the network, link to peer's "out"
out func([]byte) // packets we're sending to the network, link to peer's "in"
toRecv chan router_recvPacket // packets to handle via recvPacket()
recv chan<- []byte // place where the tun pulls received packets from
send <-chan []byte // place where the tun puts outgoing packets
reset chan struct{} // signal that coords changed (re-init sessions/dht)
admin chan func() // pass a lambda for the admin socket to query stuff
cryptokey cryptokey
}
// Packet and session info, used to check that the packet matches a valid IP range or CKR prefix before sending to the tun.
type router_recvPacket struct {
bs []byte
sinfo *sessionInfo
}
// Initializes the router struct, which includes setting up channels to/from the tun/tap.
func (r *router) init(core *Core) {
r.core = core
r.addr = *address_addrForNodeID(&r.core.dht.nodeID)
r.subnet = *address_subnetForNodeID(&r.core.dht.nodeID)
in := make(chan []byte, 32) // TODO something better than this...
p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{})
p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{}, "(self)")
p.out = func(packet []byte) {
// This is to make very sure it never blocks
select {
@@ -59,6 +70,7 @@ func (r *router) init(core *Core) {
}
r.in = in
r.out = func(packet []byte) { p.handlePacket(packet) } // The caller is responsible for go-ing if it needs to not block
r.toRecv = make(chan router_recvPacket, 32)
recv := make(chan []byte, 32)
send := make(chan []byte, 32)
r.recv = recv
@@ -66,7 +78,8 @@ func (r *router) init(core *Core) {
r.core.tun.recv = recv
r.core.tun.send = send
r.reset = make(chan struct{}, 1)
r.admin = make(chan func())
r.admin = make(chan func(), 32)
r.cryptokey.init(r.core)
// go r.mainLoop()
}
@@ -86,13 +99,19 @@ func (r *router) mainLoop() {
defer ticker.Stop()
for {
select {
case rp := <-r.toRecv:
r.recvPacket(rp.bs, rp.sinfo)
case p := <-r.in:
r.handleIn(p)
case p := <-r.send:
r.sendPacket(p)
case info := <-r.core.dht.peers:
r.core.dht.insertIfNew(info, false) // Insert as a normal node
r.core.dht.insertIfNew(info, true) // Insert as a peer
now := time.Now()
oldInfo, isIn := r.core.dht.table[*info.getNodeID()]
r.core.dht.insert(info)
if isIn && now.Sub(oldInfo.recv) < 45*time.Second {
info.recv = oldInfo.recv
}
case <-r.reset:
r.core.sessions.resetInits()
r.core.dht.reset()
@@ -101,6 +120,7 @@ func (r *router) mainLoop() {
// Any periodic maintenance stuff goes here
r.core.switchTable.doMaintenance()
r.core.dht.doMaintenance()
r.core.sessions.cleanup()
util_getBytes() // To slowly drain things
}
case f := <-r.admin:
@@ -115,30 +135,82 @@ func (r *router) mainLoop() {
// If the session hasn't responded recently, it triggers a ping or search to keep things alive or deal with broken coords *relatively* quickly.
// It also deals with oversized packets if there are MTU issues by calling into icmpv6.go to spoof PacketTooBig traffic, or DestinationUnreachable if the other side has their tun/tap disabled.
func (r *router) sendPacket(bs []byte) {
if len(bs) < 40 {
panic("Tried to send a packet shorter than a header...")
}
var sourceAddr address
var sourceSubnet subnet
copy(sourceAddr[:], bs[8:])
copy(sourceSubnet[:], bs[8:])
if !sourceAddr.isValid() && !sourceSubnet.isValid() {
var destAddr address
var destSnet subnet
var destPubKey *boxPubKey
var destNodeID *NodeID
var addrlen int
if bs[0]&0xf0 == 0x60 {
// Check if we have a fully-sized header
if len(bs) < 40 {
panic("Tried to send a packet shorter than an IPv6 header...")
}
// IPv6 address
addrlen = 16
copy(sourceAddr[:addrlen], bs[8:])
copy(destAddr[:addrlen], bs[24:])
copy(destSnet[:addrlen/2], bs[24:])
} else if bs[0]&0xf0 == 0x40 {
// Check if we have a fully-sized header
if len(bs) < 20 {
panic("Tried to send a packet shorter than an IPv4 header...")
}
// IPv4 address
addrlen = 4
copy(sourceAddr[:addrlen], bs[12:])
copy(destAddr[:addrlen], bs[16:])
} else {
// Unknown address length
return
}
var dest address
copy(dest[:], bs[24:])
var snet subnet
copy(snet[:], bs[24:])
if !dest.isValid() && !snet.isValid() {
if !r.cryptokey.isValidSource(sourceAddr, addrlen) {
// The packet had a source address that doesn't belong to us or our
// configured crypto-key routing source subnets
return
}
if !destAddr.isValid() && !destSnet.isValid() {
// The addresses didn't match valid Yggdrasil node addresses so let's see
// whether it matches a crypto-key routing range instead
if key, err := r.cryptokey.getPublicKeyForAddress(destAddr, addrlen); err == nil {
// A public key was found, get the node ID for the search
destPubKey = &key
destNodeID = getNodeID(destPubKey)
// Do a quick check to ensure that the node ID refers to a vaild Yggdrasil
// address or subnet - this might be superfluous
addr := *address_addrForNodeID(destNodeID)
copy(destAddr[:], addr[:])
copy(destSnet[:], addr[:])
if !destAddr.isValid() && !destSnet.isValid() {
return
}
} else {
// No public key was found in the CKR table so we've exhausted our options
return
}
}
doSearch := func(packet []byte) {
var nodeID, mask *NodeID
if dest.isValid() {
nodeID, mask = dest.getNodeIDandMask()
}
if snet.isValid() {
nodeID, mask = snet.getNodeIDandMask()
switch {
case destNodeID != nil:
// We already know the full node ID, probably because it's from a CKR
// route in which the public key is known ahead of time
nodeID = destNodeID
var m NodeID
for i := range m {
m[i] = 0xFF
}
mask = &m
case destAddr.isValid():
// We don't know the full node ID - try and use the address to generate
// a truncated node ID
nodeID, mask = destAddr.getNodeIDandMask()
case destSnet.isValid():
// We don't know the full node ID - try and use the subnet to generate
// a truncated node ID
nodeID, mask = destSnet.getNodeIDandMask()
default:
return
}
sinfo, isIn := r.core.searches.searches[*nodeID]
if !isIn {
@@ -151,11 +223,11 @@ func (r *router) sendPacket(bs []byte) {
}
var sinfo *sessionInfo
var isIn bool
if dest.isValid() {
sinfo, isIn = r.core.sessions.getByTheirAddr(&dest)
if destAddr.isValid() {
sinfo, isIn = r.core.sessions.getByTheirAddr(&destAddr)
}
if snet.isValid() {
sinfo, isIn = r.core.sessions.getByTheirSubnet(&snet)
if destSnet.isValid() {
sinfo, isIn = r.core.sessions.getByTheirSubnet(&destSnet)
}
switch {
case !isIn || !sinfo.init:
@@ -184,6 +256,14 @@ func (r *router) sendPacket(bs []byte) {
}
fallthrough // Also send the packet
default:
// If we know the public key ahead of time (i.e. a CKR route) then check
// if the session perm pub key matches before we send the packet to it
if destPubKey != nil {
if !bytes.Equal((*destPubKey)[:], sinfo.theirPermPub[:]) {
return
}
}
// Drop packets if the session MTU is 0 - this means that one or other
// side probably has their TUN adapter disabled
if sinfo.getMTU() == 0 {
@@ -234,29 +314,55 @@ func (r *router) sendPacket(bs []byte) {
// Don't continue - drop the packet
return
}
sinfo.send <- bs
}
}
// Called for incoming traffic by the session worker for that connection.
// Checks that the IP address is correct (matches the session) and passes the packet to the tun/tap.
func (r *router) recvPacket(bs []byte, theirAddr *address, theirSubnet *subnet) {
func (r *router) recvPacket(bs []byte, sinfo *sessionInfo) {
// Note: called directly by the session worker, not the router goroutine
if len(bs) < 24 {
util_putBytes(bs)
return
}
var source address
copy(source[:], bs[8:])
var sourceAddr address
var dest address
var snet subnet
copy(snet[:], bs[8:])
switch {
case source.isValid() && source == *theirAddr:
case snet.isValid() && snet == *theirSubnet:
default:
var addrlen int
if bs[0]&0xf0 == 0x60 {
// IPv6 address
addrlen = 16
copy(sourceAddr[:addrlen], bs[8:])
copy(dest[:addrlen], bs[24:])
copy(snet[:addrlen/2], bs[8:])
} else if bs[0]&0xf0 == 0x40 {
// IPv4 address
addrlen = 4
copy(sourceAddr[:addrlen], bs[12:])
copy(dest[:addrlen], bs[16:])
} else {
// Unknown address length
return
}
// Check that the packet is destined for either our Yggdrasil address or
// subnet, or that it matches one of the crypto-key routing source routes
if !r.cryptokey.isValidSource(dest, addrlen) {
util_putBytes(bs)
return
}
// See whether the packet they sent should have originated from this session
switch {
case sourceAddr.isValid() && sourceAddr == sinfo.theirAddr:
case snet.isValid() && snet == sinfo.theirSubnet:
default:
key, err := r.cryptokey.getPublicKeyForAddress(sourceAddr, addrlen)
if err != nil || key != sinfo.theirPermPub {
util_putBytes(bs)
return
}
}
//go func() { r.recv<-bs }()
r.recv <- bs
}

View File

@@ -11,6 +11,9 @@ package yggdrasil
// A new search packet is sent immediately after receiving a response
// A new search packet is sent periodically, once per second, in case a packet was dropped (this slowly causes the search to become parallel if the search doesn't timeout but also doesn't finish within 1 second for whatever reason)
// TODO?
// Some kind of max search steps, in case the node is offline, so we don't crawl through too much of the network looking for a destination that isn't there?
import (
"sort"
"time"
@@ -88,11 +91,13 @@ func (s *searches) handleDHTRes(res *dhtRes) {
func (s *searches) addToSearch(sinfo *searchInfo, res *dhtRes) {
// Add responses to toVisit if closer to dest than the res node
from := dhtInfo{key: res.Key, coords: res.Coords}
sinfo.visited[*from.getNodeID()] = true
for _, info := range res.Infos {
if sinfo.visited[*info.getNodeID()] {
if *info.getNodeID() == s.core.dht.nodeID || sinfo.visited[*info.getNodeID()] {
continue
}
if dht_firstCloserThanThird(info.getNodeID(), &res.Dest, from.getNodeID()) {
if dht_ordered(&sinfo.dest, info.getNodeID(), from.getNodeID()) {
// Response is closer to the destination
sinfo.toVisit = append(sinfo.toVisit, info)
}
}
@@ -107,7 +112,8 @@ func (s *searches) addToSearch(sinfo *searchInfo, res *dhtRes) {
}
// Sort
sort.SliceStable(sinfo.toVisit, func(i, j int) bool {
return dht_firstCloserThanThird(sinfo.toVisit[i].getNodeID(), &res.Dest, sinfo.toVisit[j].getNodeID())
// Should return true if i is closer to the destination than j
return dht_ordered(&res.Dest, sinfo.toVisit[i].getNodeID(), sinfo.toVisit[j].getNodeID())
})
// Truncate to some maximum size
if len(sinfo.toVisit) > search_MAX_SEARCH_SIZE {
@@ -126,11 +132,9 @@ func (s *searches) doSearchStep(sinfo *searchInfo) {
// Send to the next search target
var next *dhtInfo
next, sinfo.toVisit = sinfo.toVisit[0], sinfo.toVisit[1:]
var oldPings int
oldPings, next.pings = next.pings, 0
rq := dhtReqKey{next.key, sinfo.dest}
s.core.dht.addCallback(&rq, s.handleDHTRes)
s.core.dht.ping(next, &sinfo.dest)
next.pings = oldPings // Don't evict a node for searching with it too much
sinfo.visited[*next.getNodeID()] = true
}
}
@@ -184,6 +188,10 @@ func (s *searches) checkDHTRes(info *searchInfo, res *dhtRes) bool {
sinfo, isIn := s.core.sessions.getByTheirPerm(&res.Key)
if !isIn {
sinfo = s.core.sessions.createSession(&res.Key)
if sinfo == nil {
// nil if the DHT search finished but the session wasn't allowed
return true
}
_, isIn := s.core.sessions.getByTheirPerm(&res.Key)
if !isIn {
panic("This should never happen")

View File

@@ -4,7 +4,11 @@ package yggdrasil
// It's responsible for keeping track of open sessions to other nodes
// The session information consists of crypto keys and coords
import "time"
import (
"bytes"
"encoding/hex"
"time"
)
// All the information we know about an active session.
// This includes coords, permanent and ephemeral keys, handles and nonces, various sorts of timing information for timeout and maintenance, and some metadata for the admin API.
@@ -72,7 +76,10 @@ func (s *sessionInfo) update(p *sessionPing) bool {
if p.MTU >= 1280 || p.MTU == 0 {
s.theirMTU = p.MTU
}
s.coords = append([]byte{}, p.Coords...)
if !bytes.Equal(s.coords, p.Coords) {
// allocate enough space for additional coords
s.coords = append(make([]byte, 0, len(p.Coords)+11), p.Coords...)
}
now := time.Now()
s.time = now
s.tstamp = p.Tstamp
@@ -89,7 +96,8 @@ func (s *sessionInfo) timedout() bool {
// Sessions are indexed by handle.
// Additionally, stores maps of address/subnet onto keys, and keys onto handles.
type sessions struct {
core *Core
core *Core
lastCleanup time.Time
// Maps known permanent keys to their shared key, used by DHT a lot
permShared map[boxPubKey]*boxSharedKey
// Maps (secret) handle onto session info
@@ -100,6 +108,13 @@ type sessions struct {
byTheirPerm map[boxPubKey]*handle
addrToPerm map[address]*boxPubKey
subnetToPerm map[subnet]*boxPubKey
// Options from the session firewall
sessionFirewallEnabled bool
sessionFirewallAllowsDirect bool
sessionFirewallAllowsRemote bool
sessionFirewallAlwaysAllowsOutbound bool
sessionFirewallWhitelist []string
sessionFirewallBlacklist []string
}
// Initializes the session struct.
@@ -111,6 +126,85 @@ func (ss *sessions) init(core *Core) {
ss.byTheirPerm = make(map[boxPubKey]*handle)
ss.addrToPerm = make(map[address]*boxPubKey)
ss.subnetToPerm = make(map[subnet]*boxPubKey)
ss.lastCleanup = time.Now()
}
// Enable or disable the session firewall
func (ss *sessions) setSessionFirewallState(enabled bool) {
ss.sessionFirewallEnabled = enabled
}
// Set the session firewall defaults (first parameter is whether to allow
// sessions from direct peers, second is whether to allow from remote nodes).
func (ss *sessions) setSessionFirewallDefaults(allowsDirect bool, allowsRemote bool, alwaysAllowsOutbound bool) {
ss.sessionFirewallAllowsDirect = allowsDirect
ss.sessionFirewallAllowsRemote = allowsRemote
ss.sessionFirewallAlwaysAllowsOutbound = alwaysAllowsOutbound
}
// Set the session firewall whitelist - nodes always allowed to open sessions.
func (ss *sessions) setSessionFirewallWhitelist(whitelist []string) {
ss.sessionFirewallWhitelist = whitelist
}
// Set the session firewall blacklist - nodes never allowed to open sessions.
func (ss *sessions) setSessionFirewallBlacklist(blacklist []string) {
ss.sessionFirewallBlacklist = blacklist
}
// Determines whether the session with a given publickey is allowed based on
// session firewall rules.
func (ss *sessions) isSessionAllowed(pubkey *boxPubKey, initiator bool) bool {
// Allow by default if the session firewall is disabled
if !ss.sessionFirewallEnabled {
return true
}
// Prepare for checking whitelist/blacklist
var box boxPubKey
// Reject blacklisted nodes
for _, b := range ss.sessionFirewallBlacklist {
key, err := hex.DecodeString(b)
if err == nil {
copy(box[:boxPubKeyLen], key)
if box == *pubkey {
return false
}
}
}
// Allow whitelisted nodes
for _, b := range ss.sessionFirewallWhitelist {
key, err := hex.DecodeString(b)
if err == nil {
copy(box[:boxPubKeyLen], key)
if box == *pubkey {
return true
}
}
}
// Allow outbound sessions if appropriate
if ss.sessionFirewallAlwaysAllowsOutbound {
if initiator {
return true
}
}
// Look and see if the pubkey is that of a direct peer
var isDirectPeer bool
for _, peer := range ss.core.peers.ports.Load().(map[switchPort]*peer) {
if peer.box == *pubkey {
isDirectPeer = true
break
}
}
// Allow direct peers if appropriate
if ss.sessionFirewallAllowsDirect && isDirectPeer {
return true
}
// Allow remote nodes if appropriate
if ss.sessionFirewallAllowsRemote && !isDirectPeer {
return true
}
// Finally, default-deny if not matching any of the above rules
return false
}
// Gets the session corresponding to a given handle.
@@ -166,6 +260,11 @@ func (ss *sessions) getByTheirSubnet(snet *subnet) (*sessionInfo, bool) {
// Creates a new session and lazily cleans up old/timedout existing sessions.
// This includse initializing session info to sane defaults (e.g. lowest supported MTU).
func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo {
if ss.sessionFirewallEnabled {
if !ss.isSessionAllowed(theirPermKey, true) {
return nil
}
}
sinfo := sessionInfo{}
sinfo.core = ss.core
sinfo.theirPermPub = *theirPermKey
@@ -202,13 +301,6 @@ func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo {
sinfo.send = make(chan []byte, 32)
sinfo.recv = make(chan *wire_trafficPacket, 32)
go sinfo.doWorker()
// Do some cleanup
// Time thresholds almost certainly could use some adjusting
for _, s := range ss.sinfos {
if s.timedout() {
s.close()
}
}
ss.sinfos[sinfo.myHandle] = &sinfo
ss.byMySes[sinfo.mySesPub] = &sinfo.myHandle
ss.byTheirPerm[sinfo.theirPermPub] = &sinfo.myHandle
@@ -217,6 +309,54 @@ func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo {
return &sinfo
}
func (ss *sessions) cleanup() {
// Time thresholds almost certainly could use some adjusting
for k := range ss.permShared {
// Delete a key, to make sure this eventually shrinks to 0
delete(ss.permShared, k)
break
}
if time.Since(ss.lastCleanup) < time.Minute {
return
}
for _, s := range ss.sinfos {
if s.timedout() {
s.close()
}
}
permShared := make(map[boxPubKey]*boxSharedKey, len(ss.permShared))
for k, v := range ss.permShared {
permShared[k] = v
}
ss.permShared = permShared
sinfos := make(map[handle]*sessionInfo, len(ss.sinfos))
for k, v := range ss.sinfos {
sinfos[k] = v
}
ss.sinfos = sinfos
byMySes := make(map[boxPubKey]*handle, len(ss.byMySes))
for k, v := range ss.byMySes {
byMySes[k] = v
}
ss.byMySes = byMySes
byTheirPerm := make(map[boxPubKey]*handle, len(ss.byTheirPerm))
for k, v := range ss.byTheirPerm {
byTheirPerm[k] = v
}
ss.byTheirPerm = byTheirPerm
addrToPerm := make(map[address]*boxPubKey, len(ss.addrToPerm))
for k, v := range ss.addrToPerm {
addrToPerm[k] = v
}
ss.addrToPerm = addrToPerm
subnetToPerm := make(map[subnet]*boxPubKey, len(ss.subnetToPerm))
for k, v := range ss.subnetToPerm {
subnetToPerm[k] = v
}
ss.subnetToPerm = subnetToPerm
ss.lastCleanup = time.Now()
}
// Closes a session, removing it from sessions maps and killing the worker goroutine.
func (sinfo *sessionInfo) close() {
delete(sinfo.core.sessions.sinfos, sinfo.myHandle)
@@ -253,7 +393,7 @@ func (ss *sessions) getSharedKey(myPriv *boxPrivKey,
return skey
}
// First do some cleanup
const maxKeys = dht_bucket_number * dht_bucket_size
const maxKeys = 1024
for key := range ss.permShared {
// Remove a random key until the store is small enough
if len(ss.permShared) < maxKeys {
@@ -297,6 +437,12 @@ func (ss *sessions) sendPingPong(sinfo *sessionInfo, isPong bool) {
func (ss *sessions) handlePing(ping *sessionPing) {
// Get the corresponding session (or create a new session)
sinfo, isIn := ss.getByTheirPerm(&ping.SendPermPub)
// Check the session firewall
if !isIn && ss.sessionFirewallEnabled {
if !ss.isSessionAllowed(&ping.SendPermPub, false) {
return
}
}
if !isIn || sinfo.timedout() {
if isIn {
sinfo.close()
@@ -415,12 +561,42 @@ func (sinfo *sessionInfo) doWorker() {
func (sinfo *sessionInfo) doSend(bs []byte) {
defer util_putBytes(bs)
if !sinfo.init {
// To prevent using empty session keys
return
} // To prevent using empty session keys
}
// code isn't multithreaded so appending to this is safe
coords := sinfo.coords
// Read IPv6 flowlabel field (20 bits).
// Assumes packet at least contains IPv6 header.
flowkey := uint64(bs[1]&0x0f)<<16 | uint64(bs[2])<<8 | uint64(bs[3])
// Check if the flowlabel was specified
if flowkey == 0 {
// Does the packet meet the minimum UDP packet size? (others are bigger)
if len(bs) >= 48 {
// Is the protocol TCP, UDP, SCTP?
if bs[6] == 0x06 || bs[6] == 0x11 || bs[6] == 0x84 {
// if flowlabel was unspecified (0), try to use known protocols' ports
// protokey: proto | sport | dport
flowkey = uint64(bs[6])<<32 /* proto */ |
uint64(bs[40])<<24 | uint64(bs[41])<<16 /* sport */ |
uint64(bs[42])<<8 | uint64(bs[43]) /* dport */
}
}
}
// If we have a flowkey, either through the IPv6 flowlabel field or through
// known TCP/UDP/SCTP proto-sport-dport triplet, then append it to the coords.
// Appending extra coords after a 0 ensures that we still target the local router
// but lets us send extra data (which is otherwise ignored) to help separate
// traffic streams into independent queues
if flowkey != 0 {
coords = append(coords, 0) // First target the local switchport
coords = wire_put_uint64(flowkey, coords) // Then variable-length encoded flowkey
}
// Prepare the payload
payload, nonce := boxSeal(&sinfo.sharedSesKey, bs, &sinfo.myNonce)
defer util_putBytes(payload)
p := wire_trafficPacket{
Coords: sinfo.coords,
Coords: coords,
Handle: sinfo.theirHandle,
Nonce: *nonce,
Payload: payload,
@@ -437,43 +613,16 @@ func (sinfo *sessionInfo) doSend(bs []byte) {
// TODO? remove the MTU updating part? That should never happen with TCP peers, and the old UDP code that caused it was removed (and if replaced, should be replaced with something that can reliably send messages with an arbitrary size).
func (sinfo *sessionInfo) doRecv(p *wire_trafficPacket) {
defer util_putBytes(p.Payload)
payloadSize := uint16(len(p.Payload))
if !sinfo.nonceIsOK(&p.Nonce) {
return
}
bs, isOK := boxOpen(&sinfo.sharedSesKey, p.Payload, &p.Nonce)
if !isOK {
// We're going to guess that the session MTU is too large
// Set myMTU to the largest value we think we can receive
fixSessionMTU := func() {
// This clamps down to 1280 almost immediately over ipv4
// Over link-local ipv6, it seems to approach link MTU
// So maybe it's doing the right thing?...
//sinfo.core.log.Println("DEBUG got bad packet:", payloadSize)
newMTU := payloadSize - boxOverhead
if newMTU < 1280 {
newMTU = 1280
}
if newMTU < sinfo.myMTU {
sinfo.myMTU = newMTU
sinfo.core.sessions.sendPingPong(sinfo, false)
sinfo.mtuTime = time.Now()
sinfo.wasMTUFixed = true
}
}
go func() { sinfo.core.router.admin <- fixSessionMTU }()
util_putBytes(bs)
return
}
fixSessionMTU := func() {
if time.Since(sinfo.mtuTime) > time.Minute {
sinfo.myMTU = uint16(sinfo.core.tun.mtu)
sinfo.mtuTime = time.Now()
}
}
go func() { sinfo.core.router.admin <- fixSessionMTU }()
sinfo.updateNonce(&p.Nonce)
sinfo.time = time.Now()
sinfo.bytesRecvd += uint64(len(bs))
sinfo.core.router.recvPacket(bs, &sinfo.theirAddr, &sinfo.theirSubnet)
sinfo.core.router.toRecv <- router_recvPacket{bs, sinfo}
}

View File

@@ -1,86 +0,0 @@
package yggdrasil
// This is where we record which signatures we've previously checked
// It's so we can avoid needlessly checking them again
import (
"sync"
"time"
)
// This keeps track of what signatures have already been checked.
// It's used to skip expensive crypto operations, given that many signatures are likely to be the same for the average node's peers.
type sigManager struct {
mutex sync.RWMutex
checked map[sigBytes]knownSig
lastCleaned time.Time
}
// Represents a known signature.
// Includes the key, the signature bytes, the bytes that were signed, and the time it was last used.
type knownSig struct {
key sigPubKey
sig sigBytes
bs []byte
time time.Time
}
// Initializes the signature manager.
func (m *sigManager) init() {
m.checked = make(map[sigBytes]knownSig)
}
// Checks if a key and signature match the supplied bytes.
// If the same key/sig/bytes have been checked before, it returns true from the cached results.
// If not, it checks the key, updates it in the cache if successful, and returns the checked results.
func (m *sigManager) check(key *sigPubKey, sig *sigBytes, bs []byte) bool {
if m.isChecked(key, sig, bs) {
return true
}
verified := verify(key, bs, sig)
if verified {
m.putChecked(key, sig, bs)
}
return verified
}
// Checks the cache to see if this key/sig/bytes combination has already been verified.
// Returns true if it finds a match.
func (m *sigManager) isChecked(key *sigPubKey, sig *sigBytes, bs []byte) bool {
m.mutex.RLock()
defer m.mutex.RUnlock()
k, isIn := m.checked[*sig]
if !isIn {
return false
}
if k.key != *key || k.sig != *sig || len(bs) != len(k.bs) {
return false
}
for idx := 0; idx < len(bs); idx++ {
if bs[idx] != k.bs[idx] {
return false
}
}
k.time = time.Now()
return true
}
// Puts a new result into the cache.
// This result is then used by isChecked to skip the expensive crypto verification if it's needed again.
// This is useful because, for nodes with multiple peers, there is often a lot of overlap between the signatures provided by each peer.
func (m *sigManager) putChecked(key *sigPubKey, newsig *sigBytes, bs []byte) {
m.mutex.Lock()
defer m.mutex.Unlock()
now := time.Now()
if time.Since(m.lastCleaned) > 60*time.Second {
// Since we have the write lock anyway, do some cleanup
for s, k := range m.checked {
if time.Since(k.time) > 60*time.Second {
delete(m.checked, s)
}
}
m.lastCleaned = now
}
k := knownSig{key: *key, sig: *newsig, bs: bs, time: now}
m.checked[*newsig] = k
}

View File

@@ -12,15 +12,18 @@ package yggdrasil
// A little annoying to do with constant changes from backpressure
import (
"sort"
"math/rand"
"sync"
"sync/atomic"
"time"
)
const switch_timeout = time.Minute
const switch_updateInterval = switch_timeout / 2
const switch_throttle = switch_updateInterval / 2
const (
switch_timeout = time.Minute
switch_updateInterval = switch_timeout / 2
switch_throttle = switch_updateInterval / 2
switch_faster_threshold = 240 //Number of switch updates before switching to a faster parent
)
// The switch locator represents the topology and network state dependent info about a node, minus the signatures that go with it.
// Nodes will pick the best root they see, provided that the root continues to push out updates with new timestamps.
@@ -118,13 +121,13 @@ func (x *switchLocator) isAncestorOf(y *switchLocator) bool {
// Information about a peer, used by the switch to build the tree and eventually make routing decisions.
type peerInfo struct {
key sigPubKey // ID of this peer
locator switchLocator // Should be able to respond with signatures upon request
degree uint64 // Self-reported degree
time time.Time // Time this node was last seen
firstSeen time.Time
port switchPort // Interface number of this peer
msg switchMsg // The wire switchMsg used
key sigPubKey // ID of this peer
locator switchLocator // Should be able to respond with signatures upon request
degree uint64 // Self-reported degree
time time.Time // Time this node was last seen
faster map[switchPort]uint64 // Counter of how often a node is faster than the current parent, penalized extra if slower
port switchPort // Interface number of this peer
msg switchMsg // The wire switchMsg used
}
// This is just a uint64 with a named type for clarity reasons.
@@ -139,7 +142,7 @@ type tableElem struct {
// This is the subset of the information about all peers needed to make routing decisions, and it stored separately in an atomically accessed table, which gets hammered in the "hot loop" of the routing logic (see: peer.handleTraffic in peers.go).
type lookupTable struct {
self switchLocator
elems []tableElem
elems map[switchPort]tableElem
}
// This is switch information which is mutable and needs to be modified by other goroutines, but is not accessed atomically.
@@ -155,17 +158,25 @@ type switchData struct {
// All the information stored by the switch.
type switchTable struct {
core *Core
key sigPubKey // Our own key
time time.Time // Time when locator.tstamp was last updated
parent switchPort // Port of whatever peer is our parent, or self if we're root
drop map[sigPubKey]int64 // Tstamp associated with a dropped root
mutex sync.RWMutex // Lock for reads/writes of switchData
data switchData
updater atomic.Value //*sync.Once
table atomic.Value //lookupTable
core *Core
key sigPubKey // Our own key
time time.Time // Time when locator.tstamp was last updated
drop map[sigPubKey]int64 // Tstamp associated with a dropped root
mutex sync.RWMutex // Lock for reads/writes of switchData
parent switchPort // Port of whatever peer is our parent, or self if we're root
data switchData //
updater atomic.Value // *sync.Once
table atomic.Value // lookupTable
packetIn chan []byte // Incoming packets for the worker to handle
idleIn chan switchPort // Incoming idle notifications from peer links
admin chan func() // Pass a lambda for the admin socket to query stuff
queues switch_buffers // Queues - not atomic so ONLY use through admin chan
queueTotalMaxSize uint64 // Maximum combined size of queues
}
// Minimum allowed total size of switch queues.
const SwitchQueueTotalMinSize = 4 * 1024 * 1024
// Initializes the switchTable struct.
func (t *switchTable) init(core *Core, key sigPubKey) {
now := time.Now()
@@ -177,6 +188,10 @@ func (t *switchTable) init(core *Core, key sigPubKey) {
t.updater.Store(&sync.Once{})
t.table.Store(lookupTable{})
t.drop = make(map[sigPubKey]int64)
t.packetIn = make(chan []byte, 1024)
t.idleIn = make(chan switchPort, 1024)
t.admin = make(chan func())
t.queueTotalMaxSize = SwitchQueueTotalMinSize
}
// Safely gets a copy of this node's locator.
@@ -193,6 +208,7 @@ func (t *switchTable) doMaintenance() {
defer t.mutex.Unlock() // Release lock when we're done
t.cleanRoot()
t.cleanDropped()
t.cleanPeers()
}
// Updates the root periodically if it is ourself, or promotes ourself to root if we're better than the current root or if the current root has timed out.
@@ -235,14 +251,38 @@ func (t *switchTable) cleanRoot() {
// Removes a peer.
// Must be called by the router mainLoop goroutine, e.g. call router.doAdmin with a lambda that calls this.
// If the removed peer was this node's parent, it immediately tries to find a new parent.
func (t *switchTable) unlockedRemovePeer(port switchPort) {
func (t *switchTable) forgetPeer(port switchPort) {
t.mutex.Lock()
defer t.mutex.Unlock()
delete(t.data.peers, port)
t.updater.Store(&sync.Once{})
if port != t.parent {
return
}
t.parent = 0
for _, info := range t.data.peers {
t.unlockedHandleMsg(&info.msg, info.port)
t.unlockedHandleMsg(&info.msg, info.port, true)
}
}
// Clean all unresponsive peers from the table, needed in case a peer stops updating.
// Needed in case a non-parent peer keeps the connection open but stops sending updates.
// Also reclaims space from deleted peers by copying the map.
func (t *switchTable) cleanPeers() {
now := time.Now()
for port, peer := range t.data.peers {
if now.Sub(peer.time) > switch_timeout+switch_throttle {
// Longer than switch_timeout to make sure we don't remove a working peer because the root stopped responding.
delete(t.data.peers, port)
}
}
if _, isIn := t.data.peers[t.parent]; !isIn {
// The root timestamp would probably time out before this happens, but better safe than sorry.
// We removed the current parent, so find a new one.
t.parent = 0
for _, peer := range t.data.peers {
t.unlockedHandleMsg(&peer.msg, peer.port, true)
}
}
}
@@ -316,7 +356,7 @@ func (t *switchTable) checkRoot(msg *switchMsg) bool {
func (t *switchTable) handleMsg(msg *switchMsg, fromPort switchPort) {
t.mutex.Lock()
defer t.mutex.Unlock()
t.unlockedHandleMsg(msg, fromPort)
t.unlockedHandleMsg(msg, fromPort, false)
}
// This updates the switch with information about a peer.
@@ -324,7 +364,8 @@ func (t *switchTable) handleMsg(msg *switchMsg, fromPort switchPort) {
// That happens if this node is already our parent, or is advertising a better root, or is advertising a better path to the same root, etc...
// There are a lot of very delicate order sensitive checks here, so its' best to just read the code if you need to understand what it's doing.
// It's very important to not change the order of the statements in the case function unless you're absolutely sure that it's safe, including safe if used along side nodes that used the previous order.
func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
// Set the third arg to true if you're reprocessing an old message, e.g. to find a new parent after one disconnects, to avoid updating some timing related things.
func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort, reprocessing bool) {
// TODO directly use a switchMsg instead of switchMessage + sigs
now := time.Now()
// Set up the sender peerInfo
@@ -339,11 +380,6 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
prevKey = hop.Next
}
sender.msg = *msg
oldSender, isIn := t.data.peers[fromPort]
if !isIn {
oldSender.firstSeen = now
}
sender.firstSeen = oldSender.firstSeen
sender.port = fromPort
sender.time = now
// Decide what to do
@@ -362,11 +398,40 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
return true
}
doUpdate := false
oldSender := t.data.peers[fromPort]
if !equiv(&sender.locator, &oldSender.locator) {
// Reset faster info, we'll start refilling it right after this
sender.faster = nil
doUpdate = true
//sender.firstSeen = now // TODO? uncomment to prevent flapping?
}
// Update the matrix of peer "faster" thresholds
if reprocessing {
sender.faster = oldSender.faster
} else {
sender.faster = make(map[switchPort]uint64, len(oldSender.faster))
for port, peer := range t.data.peers {
if port == fromPort {
continue
} else if sender.locator.root != peer.locator.root || sender.locator.tstamp > peer.locator.tstamp {
// We were faster than this node, so increment, as long as we don't overflow because of it
if oldSender.faster[peer.port] < switch_faster_threshold {
sender.faster[port] = oldSender.faster[peer.port] + 1
} else {
sender.faster[port] = switch_faster_threshold
}
} else {
// Slower than this node, penalize (more than the reward amount)
if oldSender.faster[port] > 1 {
sender.faster[port] = oldSender.faster[peer.port] - 2
} else {
sender.faster[port] = 0
}
}
}
}
// Update sender
t.data.peers[fromPort] = sender
// Decide if we should also update our root info to make the sender our parent
updateRoot := false
oldParent, isIn := t.data.peers[t.parent]
noParent := !isIn
@@ -381,30 +446,49 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
}
return true
}()
sTime := now.Sub(sender.firstSeen)
pTime := oldParent.time.Sub(oldParent.firstSeen) + switch_timeout
// Really want to compare sLen/sTime and pLen/pTime
// Cross multiplied to avoid divide-by-zero
cost := len(sender.locator.coords) * int(pTime.Seconds())
pCost := len(t.data.locator.coords) * int(sTime.Seconds())
dropTstamp, isIn := t.drop[sender.locator.root]
// Here be dragons
// Decide if we need to update info about the root or change parents.
switch {
case !noLoop: // do nothing
case isIn && dropTstamp >= sender.locator.tstamp: // do nothing
case !noLoop:
// This route loops, so we can't use the sender as our parent.
case isIn && dropTstamp >= sender.locator.tstamp:
// This is a known root with a timestamp older than a known timeout, so we can't trust it to be a new announcement.
case firstIsBetter(&sender.locator.root, &t.data.locator.root):
// This is a better root than what we're currently using, so we should update.
updateRoot = true
case t.data.locator.root != sender.locator.root: // do nothing
case t.data.locator.tstamp > sender.locator.tstamp: // do nothing
case t.data.locator.root != sender.locator.root:
// This is not the same root, and it's apparently not better (from the above), so we should ignore it.
case t.data.locator.tstamp > sender.locator.tstamp:
// This timetsamp is older than the most recently seen one from this root, so we should ignore it.
case noParent:
// We currently have no working parent, and at this point in the switch statement, anything is better than nothing.
updateRoot = true
case cost < pCost:
case sender.faster[t.parent] >= switch_faster_threshold:
// The is reliably faster than the current parent.
updateRoot = true
case sender.port != t.parent: // do nothing
case !equiv(&sender.locator, &t.data.locator):
case reprocessing && sender.faster[t.parent] > oldParent.faster[sender.port]:
// The sender seems to be reliably faster than the current parent, so switch to them instead.
updateRoot = true
case now.Sub(t.time) < switch_throttle: // do nothing
case sender.port != t.parent:
// Ignore further cases if the sender isn't our parent.
case !reprocessing && !equiv(&sender.locator, &t.data.locator):
// Special case:
// If coords changed, then we need to penalize this node somehow, to prevent flapping.
// First, reset all faster-related info to 0.
// Then, de-parent the node and reprocess all messages to find a new parent.
t.parent = 0
for _, peer := range t.data.peers {
if peer.port == sender.port {
continue
}
t.unlockedHandleMsg(&peer.msg, peer.port, true)
}
// Process the sender last, to avoid keeping them as a parent if at all possible.
t.unlockedHandleMsg(&sender.msg, sender.port, true)
case now.Sub(t.time) < switch_throttle:
// We've already gotten an update from this root recently, so ignore this one to avoid flooding.
case sender.locator.tstamp > t.data.locator.tstamp:
// The timestamp was updated, so we need to update locally and send to our peers.
updateRoot = true
}
if updateRoot {
@@ -429,6 +513,10 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) {
return
}
////////////////////////////////////////////////////////////////////////////////
// The rest of these are related to the switch worker
// This is called via a sync.Once to update the atomically readable subset of switch information that gets used for routing decisions.
func (t *switchTable) updateTable() {
// WARNING this should only be called from within t.data.updater.Do()
@@ -443,7 +531,7 @@ func (t *switchTable) updateTable() {
defer t.mutex.RUnlock()
newTable := lookupTable{
self: t.data.locator.clone(),
elems: make([]tableElem, 0, len(t.data.peers)),
elems: make(map[switchPort]tableElem, len(t.data.peers)),
}
for _, pinfo := range t.data.peers {
//if !pinfo.forward { continue }
@@ -452,48 +540,285 @@ func (t *switchTable) updateTable() {
}
loc := pinfo.locator.clone()
loc.coords = loc.coords[:len(loc.coords)-1] // Remove the them->self link
newTable.elems = append(newTable.elems, tableElem{
newTable.elems[pinfo.port] = tableElem{
locator: loc,
port: pinfo.port,
})
}
}
sort.SliceStable(newTable.elems, func(i, j int) bool {
return t.data.peers[newTable.elems[i].port].firstSeen.Before(t.data.peers[newTable.elems[j].port].firstSeen)
})
t.table.Store(newTable)
}
// This does the switch layer lookups that decide how to route traffic.
// Traffic uses greedy routing in a metric space, where the metric distance between nodes is equal to the distance between them on the tree.
// Traffic must be routed to a node that is closer to the destination via the metric space distance.
// In the event that two nodes are equally close, it gets routed to the one with the longest uptime (due to the order that things are iterated over).
// The size of the outgoing packet queue is added to a node's tree distance when the cost of forwarding to a node, subject to the constraint that the real tree distance puts them closer to the destination than ourself.
// Doing so adds a limited form of backpressure routing, based on local information, which allows us to forward traffic around *local* bottlenecks, provided that another greedy path exists.
func (t *switchTable) lookup(dest []byte) switchPort {
// Returns a copy of the atomically-updated table used for switch lookups
func (t *switchTable) getTable() lookupTable {
t.updater.Load().(*sync.Once).Do(t.updateTable)
table := t.table.Load().(lookupTable)
return t.table.Load().(lookupTable)
}
// Starts the switch worker
func (t *switchTable) start() error {
t.core.log.Println("Starting switch")
go t.doWorker()
return nil
}
// Check if a packet should go to the self node
// This means there's no node closer to the destination than us
// This is mainly used to identify packets addressed to us, or that hit a blackhole
func (t *switchTable) selfIsClosest(dest []byte) bool {
table := t.getTable()
myDist := table.self.dist(dest)
if myDist == 0 {
return 0
// Skip the iteration step if it's impossible to be closer
return true
}
// cost is in units of (expected distance) + (expected queue size), where expected distance is used as an approximation of the minimum backpressure gradient needed for packets to flow
ports := t.core.peers.getPorts()
var best switchPort
bestCost := int64(^uint64(0) >> 1)
for _, info := range table.elems {
dist := info.locator.dist(dest)
if !(dist < myDist) {
if dist < myDist {
return false
}
}
return true
}
// Returns true if the peer is closer to the destination than ourself
func (t *switchTable) portIsCloser(dest []byte, port switchPort) bool {
table := t.getTable()
if info, isIn := table.elems[port]; isIn {
theirDist := info.locator.dist(dest)
myDist := table.self.dist(dest)
return theirDist < myDist
} else {
return false
}
}
// Get the coords of a packet without decoding
func switch_getPacketCoords(packet []byte) []byte {
_, pTypeLen := wire_decode_uint64(packet)
coords, _ := wire_decode_coords(packet[pTypeLen:])
return coords
}
// Returns a unique string for each stream of traffic
// Equal to coords
// The sender may append arbitrary info to the end of coords (as long as it's begins with a 0x00) to designate separate traffic streams
// Currently, it's the IPv6 next header type and the first 2 uint16 of the next header
// This is equivalent to the TCP/UDP protocol numbers and the source / dest ports
// TODO figure out if something else would make more sense (other transport protocols?)
func switch_getPacketStreamID(packet []byte) string {
return string(switch_getPacketCoords(packet))
}
// Find the best port for a given set of coords
func (t *switchTable) bestPortForCoords(coords []byte) switchPort {
table := t.getTable()
var best switchPort
bestDist := table.self.dist(coords)
for to, elem := range table.elems {
dist := elem.locator.dist(coords)
if !(dist < bestDist) {
continue
}
p, isIn := ports[info.port]
if !isIn {
continue
}
cost := int64(dist) + p.getQueueSize()
if cost < bestCost {
best = info.port
bestCost = cost
}
best = to
bestDist = dist
}
return best
}
// Handle an incoming packet
// Either send it to ourself, or to the first idle peer that's free
// Returns true if the packet has been handled somehow, false if it should be queued
func (t *switchTable) handleIn(packet []byte, idle map[switchPort]struct{}) bool {
coords := switch_getPacketCoords(packet)
ports := t.core.peers.getPorts()
if t.selfIsClosest(coords) {
// TODO? call the router directly, and remove the whole concept of a self peer?
ports[0].sendPacket(packet)
return true
}
table := t.getTable()
myDist := table.self.dist(coords)
var best *peer
bestDist := myDist
for port := range idle {
if to := ports[port]; to != nil {
if info, isIn := table.elems[to.port]; isIn {
dist := info.locator.dist(coords)
if !(dist < bestDist) {
continue
}
best = to
bestDist = dist
}
}
}
if best != nil {
// Send to the best idle next hop
delete(idle, best.port)
best.sendPacket(packet)
return true
} else {
// Didn't find anyone idle to send it to
return false
}
}
// Info about a buffered packet
type switch_packetInfo struct {
bytes []byte
time time.Time // Timestamp of when the packet arrived
}
// Used to keep track of buffered packets
type switch_buffer struct {
packets []switch_packetInfo // Currently buffered packets, which may be dropped if it grows too large
size uint64 // Total queue size in bytes
}
type switch_buffers struct {
switchTable *switchTable
bufs map[string]switch_buffer // Buffers indexed by StreamID
size uint64 // Total size of all buffers, in bytes
maxbufs int
maxsize uint64
}
func (b *switch_buffers) cleanup(t *switchTable) {
for streamID, buf := range b.bufs {
// Remove queues for which we have no next hop
packet := buf.packets[0]
coords := switch_getPacketCoords(packet.bytes)
if t.selfIsClosest(coords) {
for _, packet := range buf.packets {
util_putBytes(packet.bytes)
}
b.size -= buf.size
delete(b.bufs, streamID)
}
}
for b.size > b.switchTable.queueTotalMaxSize {
// Drop a random queue
target := rand.Uint64() % b.size
var size uint64 // running total
for streamID, buf := range b.bufs {
size += buf.size
if size < target {
continue
}
var packet switch_packetInfo
packet, buf.packets = buf.packets[0], buf.packets[1:]
buf.size -= uint64(len(packet.bytes))
b.size -= uint64(len(packet.bytes))
util_putBytes(packet.bytes)
if len(buf.packets) == 0 {
delete(b.bufs, streamID)
} else {
// Need to update the map, since buf was retrieved by value
b.bufs[streamID] = buf
}
break
}
}
}
// Handles incoming idle notifications
// Loops over packets and sends the newest one that's OK for this peer to send
// Returns true if the peer is no longer idle, false if it should be added to the idle list
func (t *switchTable) handleIdle(port switchPort) bool {
to := t.core.peers.getPorts()[port]
if to == nil {
return true
}
var best string
var bestPriority float64
t.queues.cleanup(t)
now := time.Now()
for streamID, buf := range t.queues.bufs {
// Filter over the streams that this node is closer to
// Keep the one with the smallest queue
packet := buf.packets[0]
coords := switch_getPacketCoords(packet.bytes)
priority := float64(now.Sub(packet.time)) / float64(buf.size)
if priority > bestPriority && t.portIsCloser(coords, port) {
best = streamID
bestPriority = priority
}
}
if bestPriority != 0 {
buf := t.queues.bufs[best]
var packet switch_packetInfo
// TODO decide if this should be LIFO or FIFO
packet, buf.packets = buf.packets[0], buf.packets[1:]
buf.size -= uint64(len(packet.bytes))
t.queues.size -= uint64(len(packet.bytes))
if len(buf.packets) == 0 {
delete(t.queues.bufs, best)
} else {
// Need to update the map, since buf was retrieved by value
t.queues.bufs[best] = buf
}
to.sendPacket(packet.bytes)
return true
} else {
return false
}
}
// The switch worker does routing lookups and sends packets to where they need to be
func (t *switchTable) doWorker() {
t.queues.switchTable = t
t.queues.bufs = make(map[string]switch_buffer) // Packets per PacketStreamID (string)
idle := make(map[switchPort]struct{}) // this is to deduplicate things
for {
select {
case bytes := <-t.packetIn:
// Try to send it somewhere (or drop it if it's corrupt or at a dead end)
if !t.handleIn(bytes, idle) {
// There's nobody free to take it right now, so queue it for later
packet := switch_packetInfo{bytes, time.Now()}
streamID := switch_getPacketStreamID(packet.bytes)
buf, bufExists := t.queues.bufs[streamID]
buf.packets = append(buf.packets, packet)
buf.size += uint64(len(packet.bytes))
t.queues.size += uint64(len(packet.bytes))
// Keep a track of the max total queue size
if t.queues.size > t.queues.maxsize {
t.queues.maxsize = t.queues.size
}
t.queues.bufs[streamID] = buf
if !bufExists {
// Keep a track of the max total queue count. Only recalculate this
// when the queue is new because otherwise repeating len(dict) might
// cause unnecessary processing overhead
if len(t.queues.bufs) > t.queues.maxbufs {
t.queues.maxbufs = len(t.queues.bufs)
}
}
t.queues.cleanup(t)
}
case port := <-t.idleIn:
// Try to find something to send to this peer
if !t.handleIdle(port) {
// Didn't find anything ready to send yet, so stay idle
idle[port] = struct{}{}
}
case f := <-t.admin:
f()
}
}
}
// Passed a function to call.
// This will send the function to t.admin and block until it finishes.
func (t *switchTable) doAdmin(f func()) {
// Pass this a function that needs to be run by the router's main goroutine
// It will pass the function to the router and wait for the router to finish
done := make(chan struct{})
newF := func() {
f()
close(done)
}
t.admin <- newF
<-done
}

View File

@@ -17,6 +17,8 @@ package yggdrasil
import (
"errors"
"fmt"
"io"
"math/rand"
"net"
"sync"
"sync/atomic"
@@ -26,6 +28,8 @@ import (
)
const tcp_msgSize = 2048 + 65535 // TODO figure out what makes sense
const default_tcp_timeout = 6 * time.Second
const tcp_ping_interval = (default_tcp_timeout * 2 / 3)
// Wrapper function for non tcp/ip connections.
func setNoDelay(c net.Conn, delay bool) {
@@ -37,11 +41,12 @@ func setNoDelay(c net.Conn, delay bool) {
// The TCP listener and information about active TCP connections, to avoid duplication.
type tcpInterface struct {
core *Core
serv net.Listener
mutex sync.Mutex // Protecting the below
calls map[string]struct{}
conns map[tcpInfo](chan struct{})
core *Core
serv net.Listener
tcp_timeout time.Duration
mutex sync.Mutex // Protecting the below
calls map[string]struct{}
conns map[tcpInfo](chan struct{})
}
// This is used as the key to a map that tracks existing connections, to prevent multiple connections to the same keys and local/remote address pair from occuring.
@@ -59,36 +64,24 @@ func (iface *tcpInterface) getAddr() *net.TCPAddr {
}
// Attempts to initiate a connection to the provided address.
func (iface *tcpInterface) connect(addr string) {
iface.call(addr)
func (iface *tcpInterface) connect(addr string, intf string) {
iface.call(addr, nil, intf)
}
// Attempst to initiate a connection to the provided address, viathe provided socks proxy address.
func (iface *tcpInterface) connectSOCKS(socksaddr, peeraddr string) {
// TODO make sure this doesn't keep attempting/killing connections when one is already active.
// I think some of the interaction between this and callWithConn needs work, so the dial isn't even attempted if there's already an outgoing call to peeraddr.
// Or maybe only if there's already an outgoing call to peeraddr via this socksaddr?
go func() {
dialer, err := proxy.SOCKS5("tcp", socksaddr, nil, proxy.Direct)
if err == nil {
conn, err := dialer.Dial("tcp", peeraddr)
if err == nil {
iface.callWithConn(&wrappedConn{
c: conn,
raddr: &wrappedAddr{
network: "tcp",
addr: peeraddr,
},
})
}
}
}()
iface.call(peeraddr, &socksaddr, "")
}
// Initializes the struct.
func (iface *tcpInterface) init(core *Core, addr string) (err error) {
func (iface *tcpInterface) init(core *Core, addr string, readTimeout int32) (err error) {
iface.core = core
iface.tcp_timeout = time.Duration(readTimeout) * time.Millisecond
if iface.tcp_timeout >= 0 && iface.tcp_timeout < default_tcp_timeout {
iface.tcp_timeout = default_tcp_timeout
}
iface.serv, err = net.Listen("tcp", addr)
if err == nil {
iface.calls = make(map[string]struct{})
@@ -112,54 +105,99 @@ func (iface *tcpInterface) listener() {
}
}
// Called by connectSOCKS, it's like call but with the connection already established.
func (iface *tcpInterface) callWithConn(conn net.Conn) {
go func() {
raddr := conn.RemoteAddr().String()
iface.mutex.Lock()
_, isIn := iface.calls[raddr]
iface.mutex.Unlock()
if !isIn {
iface.mutex.Lock()
iface.calls[raddr] = struct{}{}
iface.mutex.Unlock()
defer func() {
iface.mutex.Lock()
delete(iface.calls, raddr)
iface.mutex.Unlock()
}()
iface.handler(conn, false)
}
}()
}
// Checks if a connection already exists.
// If not, it adds it to the list of active outgoing calls (to block future attempts) and dials the address.
// If the dial is successful, it launches the handler.
// When finished, it removes the outgoing call, so reconnection attempts can be made later.
// This all happens in a separate goroutine that it spawns.
func (iface *tcpInterface) call(saddr string) {
func (iface *tcpInterface) call(saddr string, socksaddr *string, sintf string) {
go func() {
callname := saddr
if sintf != "" {
callname = fmt.Sprintf("%s/%s", saddr, sintf)
}
quit := false
iface.mutex.Lock()
if _, isIn := iface.calls[saddr]; isIn {
if _, isIn := iface.calls[callname]; isIn {
quit = true
} else {
iface.calls[saddr] = struct{}{}
iface.calls[callname] = struct{}{}
defer func() {
// Block new calls for a little while, to mitigate livelock scenarios
time.Sleep(default_tcp_timeout)
time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
iface.mutex.Lock()
delete(iface.calls, saddr)
delete(iface.calls, callname)
iface.mutex.Unlock()
}()
}
iface.mutex.Unlock()
if !quit {
conn, err := net.Dial("tcp", saddr)
if quit {
return
}
var conn net.Conn
var err error
if socksaddr != nil {
if sintf != "" {
return
}
var dialer proxy.Dialer
dialer, err = proxy.SOCKS5("tcp", *socksaddr, nil, proxy.Direct)
if err != nil {
return
}
conn, err = dialer.Dial("tcp", saddr)
if err != nil {
return
}
conn = &wrappedConn{
c: conn,
raddr: &wrappedAddr{
network: "tcp",
addr: saddr,
},
}
} else {
dialer := net.Dialer{}
if sintf != "" {
ief, err := net.InterfaceByName(sintf)
if err != nil {
return
} else {
if ief.Flags&net.FlagUp == 0 {
return
}
addrs, err := ief.Addrs()
if err == nil {
dst, err := net.ResolveTCPAddr("tcp", saddr)
if err != nil {
return
}
for _, addr := range addrs {
src, _, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if (src.To4() != nil) == (dst.IP.To4() != nil) && src.IsGlobalUnicast() {
dialer.LocalAddr = &net.TCPAddr{
IP: src,
Port: 0,
}
break
}
}
if dialer.LocalAddr == nil {
return
}
}
}
}
conn, err = dialer.Dial("tcp", saddr)
if err != nil {
return
}
iface.handler(conn, false)
}
iface.handler(conn, false)
}()
}
@@ -178,8 +216,9 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
if err != nil {
return
}
timeout := time.Now().Add(6 * time.Second)
sock.SetReadDeadline(timeout)
if iface.tcp_timeout > 0 {
sock.SetReadDeadline(time.Now().Add(iface.tcp_timeout))
}
_, err = sock.Read(metaBytes)
if err != nil {
return
@@ -248,24 +287,15 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
}()
// Note that multiple connections to the same node are allowed
// E.g. over different interfaces
p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link))
p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link), sock.RemoteAddr().String())
p.linkOut = make(chan []byte, 1)
in := func(bs []byte) {
p.handlePacket(bs)
}
out := make(chan []byte, 32) // TODO? what size makes sense
out := make(chan []byte, 1)
defer close(out)
go func() {
var shadow int64
var stack [][]byte
put := func(msg []byte) {
stack = append(stack, msg)
for len(stack) > 32 {
util_putBytes(stack[0])
stack = stack[1:]
shadow++
}
}
// This goroutine waits for outgoing packets, link protocol traffic, or sends idle keep-alive traffic
send := func(msg []byte) {
msgLen := wire_encode_uint64(uint64(len(msg)))
buf := net.Buffers{tcp_msg[:], msgLen, msg}
@@ -273,14 +303,18 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
atomic.AddUint64(&p.bytesSent, uint64(len(tcp_msg)+len(msgLen)+len(msg)))
util_putBytes(msg)
}
timerInterval := 4 * time.Second
timerInterval := tcp_ping_interval
timer := time.NewTimer(timerInterval)
defer timer.Stop()
for {
if shadow != 0 {
p.updateQueueSize(-shadow)
shadow = 0
select {
case msg := <-p.linkOut:
// Always send outgoing link traffic first, if needed
send(msg)
continue
default:
}
// Otherwise wait reset the timer and wait for something to do
timer.Stop()
select {
case <-timer.C:
@@ -296,34 +330,16 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
if !ok {
return
}
put(msg)
}
for len(stack) > 0 {
select {
case msg := <-p.linkOut:
send(msg)
case msg, ok := <-out:
if !ok {
return
}
put(msg)
default:
msg := stack[len(stack)-1]
stack = stack[:len(stack)-1]
send(msg)
p.updateQueueSize(-1)
}
send(msg) // Block until the socket write has finished
// Now inform the switch that we're ready for more traffic
p.core.switchTable.idleIn <- p.port
}
}
}()
p.core.switchTable.idleIn <- p.port // Start in the idle state
p.out = func(msg []byte) {
defer func() { recover() }()
select {
case out <- msg:
p.updateQueueSize(1)
default:
util_putBytes(msg)
}
out <- msg
}
p.close = func() { sock.Close() }
setNoDelay(sock, true)
@@ -332,44 +348,56 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) {
// Put all of our cleanup here...
p.core.peers.removePeer(p.port)
}()
us, _, _ := net.SplitHostPort(sock.LocalAddr().String())
them, _, _ := net.SplitHostPort(sock.RemoteAddr().String())
themNodeID := getNodeID(&info.box)
themAddr := address_addrForNodeID(themNodeID)
themAddrString := net.IP(themAddr[:]).String()
themString := fmt.Sprintf("%s@%s", themAddrString, them)
iface.core.log.Println("Connected:", themString)
iface.reader(sock, in) // In this goroutine, because of defers
iface.core.log.Println("Disconnected:", themString)
iface.core.log.Println("Connected:", themString, "source", us)
err = iface.reader(sock, in) // In this goroutine, because of defers
if err == nil {
iface.core.log.Println("Disconnected:", themString, "source", us)
} else {
iface.core.log.Println("Disconnected:", themString, "source", us, "with error:", err)
}
return
}
// This reads from the socket into a []byte buffer for incomping messages.
// It copies completed messages out of the cache into a new slice, and passes them to the peer struct via the provided `in func([]byte)` argument.
// Then it shifts the incomplete fragments of data forward so future reads won't overwrite it.
func (iface *tcpInterface) reader(sock net.Conn, in func([]byte)) {
func (iface *tcpInterface) reader(sock net.Conn, in func([]byte)) error {
bs := make([]byte, 2*tcp_msgSize)
frag := bs[:0]
for {
timeout := time.Now().Add(6 * time.Second)
sock.SetReadDeadline(timeout)
if iface.tcp_timeout > 0 {
sock.SetReadDeadline(time.Now().Add(iface.tcp_timeout))
}
n, err := sock.Read(bs[len(frag):])
if err != nil || n == 0 {
break
}
frag = bs[:len(frag)+n]
for {
msg, ok, err := tcp_chop_msg(&frag)
if err != nil {
return
if n > 0 {
frag = bs[:len(frag)+n]
for {
msg, ok, err2 := tcp_chop_msg(&frag)
if err2 != nil {
return fmt.Errorf("Message error: %v", err2)
}
if !ok {
// We didn't get the whole message yet
break
}
newMsg := append(util_getBytes(), msg...)
in(newMsg)
util_yield()
}
if !ok {
break
} // We didn't get the whole message yet
newMsg := append(util_getBytes(), msg...)
in(newMsg)
util_yield()
frag = append(bs[:0], frag...)
}
if err != nil || n == 0 {
if err != io.EOF {
return err
}
return nil
}
frag = append(bs[:0], frag...)
}
}

View File

@@ -3,8 +3,14 @@ package yggdrasil
// This manages the tun driver to send/recv packets to/from applications
import (
"bytes"
"errors"
"time"
"github.com/songgao/packets/ethernet"
"github.com/yggdrasil-network/water"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
)
const tun_IPv6_HEADER_LENGTH = 40
@@ -20,21 +26,11 @@ type tunDevice struct {
iface *water.Interface
}
// Defines which parameters are expected by default for a TUN/TAP adapter on a
// specific platform. These values are populated in the relevant tun_*.go for
// the platform being targeted. They must be set.
type tunDefaultParameters struct {
maximumIfMTU int
defaultIfMTU int
defaultIfName string
defaultIfTAPMode bool
}
// Gets the maximum supported MTU for the platform based on the defaults in
// getDefaults().
// defaults.GetDefaults().
func getSupportedMTU(mtu int) int {
if mtu > getDefaults().maximumIfMTU {
return getDefaults().maximumIfMTU
if mtu > defaults.GetDefaults().MaximumIfMTU {
return defaults.GetDefaults().MaximumIfMTU
}
return mtu
}
@@ -56,6 +52,23 @@ func (tun *tunDevice) start(ifname string, iftapmode bool, addr string, mtu int)
}
go func() { panic(tun.read()) }()
go func() { panic(tun.write()) }()
if iftapmode {
go func() {
for {
if _, ok := tun.icmpv6.peermacs[tun.core.router.addr]; ok {
break
}
request, err := tun.icmpv6.create_ndp_tap(tun.core.router.addr)
if err != nil {
panic(err)
}
if _, err := tun.iface.Write(request); err != nil {
panic(err)
}
time.Sleep(time.Second)
}
}()
}
return nil
}
@@ -69,16 +82,74 @@ func (tun *tunDevice) write() error {
continue
}
if tun.iface.IsTAP() {
var frame ethernet.Frame
frame.Prepare(
tun.icmpv6.peermac[:6], // Destination MAC address
tun.icmpv6.mymac[:6], // Source MAC address
ethernet.NotTagged, // VLAN tagging
ethernet.IPv6, // Ethertype
len(data)) // Payload length
copy(frame[tun_ETHER_HEADER_LENGTH:], data[:])
if _, err := tun.iface.Write(frame); err != nil {
panic(err)
var destAddr address
if data[0]&0xf0 == 0x60 {
if len(data) < 40 {
panic("Tried to send a packet shorter than an IPv6 header...")
}
copy(destAddr[:16], data[24:])
} else if data[0]&0xf0 == 0x40 {
if len(data) < 20 {
panic("Tried to send a packet shorter than an IPv4 header...")
}
copy(destAddr[:4], data[16:])
} else {
return errors.New("Invalid address family")
}
sendndp := func(destAddr address) {
neigh, known := tun.icmpv6.peermacs[destAddr]
known = known && (time.Since(neigh.lastsolicitation).Seconds() < 30)
if !known {
request, err := tun.icmpv6.create_ndp_tap(destAddr)
if err != nil {
panic(err)
}
if _, err := tun.iface.Write(request); err != nil {
panic(err)
}
tun.icmpv6.peermacs[destAddr] = neighbor{
lastsolicitation: time.Now(),
}
}
}
var peermac macAddress
var peerknown bool
if data[0]&0xf0 == 0x40 {
destAddr = tun.core.router.addr
} else if data[0]&0xf0 == 0x60 {
if !bytes.Equal(tun.core.router.addr[:16], destAddr[:16]) && !bytes.Equal(tun.core.router.subnet[:8], destAddr[:8]) {
destAddr = tun.core.router.addr
}
}
if neighbor, ok := tun.icmpv6.peermacs[destAddr]; ok && neighbor.learned {
peermac = neighbor.mac
peerknown = true
} else if neighbor, ok := tun.icmpv6.peermacs[tun.core.router.addr]; ok && neighbor.learned {
peermac = neighbor.mac
peerknown = true
sendndp(destAddr)
} else {
sendndp(tun.core.router.addr)
}
if peerknown {
var proto ethernet.Ethertype
switch {
case data[0]&0xf0 == 0x60:
proto = ethernet.IPv6
case data[0]&0xf0 == 0x40:
proto = ethernet.IPv4
}
var frame ethernet.Frame
frame.Prepare(
peermac[:6], // Destination MAC address
tun.icmpv6.mymac[:6], // Source MAC address
ethernet.NotTagged, // VLAN tagging
proto, // Ethertype
len(data)) // Payload length
copy(frame[tun_ETHER_HEADER_LENGTH:], data[:])
if _, err := tun.iface.Write(frame); err != nil {
panic(err)
}
}
} else {
if _, err := tun.iface.Write(data); err != nil {
@@ -109,10 +180,10 @@ func (tun *tunDevice) read() error {
if tun.iface.IsTAP() {
o = tun_ETHER_HEADER_LENGTH
}
if buf[o]&0xf0 != 0x60 ||
n != 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o {
// Either not an IPv6 packet or not the complete packet for some reason
//panic("Should not happen in testing")
switch {
case buf[o]&0xf0 == 0x60 && n == 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o:
case buf[o]&0xf0 == 0x40 && n == 256*int(buf[o+2])+int(buf[o+3])+o:
default:
continue
}
if buf[o+6] == 58 {

View File

@@ -13,17 +13,6 @@ import (
water "github.com/yggdrasil-network/water"
)
// Sane defaults for the Darwin/macOS platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() tunDefaultParameters {
return tunDefaultParameters{
maximumIfMTU: 65535,
defaultIfMTU: 65535,
defaultIfName: "auto",
defaultIfTAPMode: false,
}
}
// Configures the "utun" adapter with the correct IPv6 address and MTU.
func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int) error {
if iftapmode {
@@ -87,7 +76,7 @@ func (tun *tunDevice) setupAddress(addr string) error {
ar.ifra_prefixmask.sin6_len = uint8(unsafe.Sizeof(ar.ifra_prefixmask))
b := make([]byte, 16)
binary.LittleEndian.PutUint16(b, uint16(0xFF00))
binary.LittleEndian.PutUint16(b, uint16(0xFE00))
ar.ifra_prefixmask.sin6_addr[0] = uint16(binary.BigEndian.Uint16(b))
ar.ifra_addr.sin6_len = uint8(unsafe.Sizeof(ar.ifra_addr))

View File

@@ -1,12 +0,0 @@
package yggdrasil
// Sane defaults for the FreeBSD platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() tunDefaultParameters {
return tunDefaultParameters{
maximumIfMTU: 32767,
defaultIfMTU: 32767,
defaultIfName: "/dev/tap0",
defaultIfTAPMode: true,
}
}

View File

@@ -12,17 +12,6 @@ import (
water "github.com/yggdrasil-network/water"
)
// Sane defaults for the Linux platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() tunDefaultParameters {
return tunDefaultParameters{
maximumIfMTU: 65535,
defaultIfMTU: 65535,
defaultIfName: "auto",
defaultIfTAPMode: false,
}
}
// Configures the TAP adapter with the correct IPv6 address and MTU.
func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int) error {
var config water.Config
@@ -40,6 +29,18 @@ func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int)
}
tun.iface = iface
tun.mtu = getSupportedMTU(mtu)
// The following check is specific to Linux, as the TAP driver only supports
// an MTU of 65535-14 to make room for the ethernet headers. This makes sure
// that the MTU gets rounded down to 65521 instead of causing a panic.
if iftapmode {
if tun.mtu > 65535-tun_ETHER_HEADER_LENGTH {
tun.mtu = 65535 - tun_ETHER_HEADER_LENGTH
}
}
// Friendly output
tun.core.log.Printf("Interface name: %s", tun.iface.Name())
tun.core.log.Printf("Interface IPv6: %s", addr)
tun.core.log.Printf("Interface MTU: %d", tun.mtu)
return tun.setupAddress(addr)
}

View File

@@ -1,12 +0,0 @@
package yggdrasil
// Sane defaults for the NetBSD platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() tunDefaultParameters {
return tunDefaultParameters{
maximumIfMTU: 9000,
defaultIfMTU: 9000,
defaultIfName: "/dev/tap0",
defaultIfTAPMode: true,
}
}

View File

@@ -1,12 +0,0 @@
package yggdrasil
// Sane defaults for the OpenBSD platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() tunDefaultParameters {
return tunDefaultParameters{
maximumIfMTU: 16384,
defaultIfMTU: 16384,
defaultIfName: "/dev/tap0",
defaultIfTAPMode: true,
}
}

View File

@@ -7,17 +7,6 @@ import water "github.com/yggdrasil-network/water"
// This is to catch unsupported platforms
// If your platform supports tun devices, you could try configuring it manually
// These are sane defaults for any platform that has not been matched by one of
// the other tun_*.go files.
func getDefaults() tunDefaultParameters {
return tunDefaultParameters{
maximumIfMTU: 65535,
defaultIfMTU: 65535,
defaultIfName: "none",
defaultIfTAPMode: false,
}
}
// Creates the TUN/TAP adapter, if supported by the Water library. Note that
// no guarantees are made at this point on an unsupported platform.
func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int) error {

View File

@@ -10,17 +10,6 @@ import (
// This is to catch Windows platforms
// Sane defaults for the Windows platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() tunDefaultParameters {
return tunDefaultParameters{
maximumIfMTU: 65535,
defaultIfMTU: 65535,
defaultIfName: "auto",
defaultIfTAPMode: true,
}
}
// Configures the TAP adapter with the correct IPv6 address and MTU. On Windows
// we don't make use of a direct operating system API to do this - we instead
// delegate the hard work to "netsh".
@@ -68,6 +57,10 @@ func (tun *tunDevice) setup(ifname string, iftapmode bool, addr string, mtu int)
if err != nil {
panic(err)
}
// Friendly output
tun.core.log.Printf("Interface name: %s", tun.iface.Name())
tun.core.log.Printf("Interface IPv6: %s", addr)
tun.core.log.Printf("Interface MTU: %d", tun.mtu)
return tun.setupAddress(addr)
}

View File

@@ -25,19 +25,15 @@ func wire_encode_uint64(elem uint64) []byte {
// Encode uint64 using a variable length scheme.
// Similar to binary.Uvarint, but big-endian.
func wire_put_uint64(elem uint64, out []byte) []byte {
bs := make([]byte, 0, 10)
bs = append(bs, byte(elem&0x7f))
for e := elem >> 7; e > 0; e >>= 7 {
bs = append(bs, byte(e|0x80))
func wire_put_uint64(e uint64, out []byte) []byte {
var b [10]byte
i := len(b) - 1
b[i] = byte(e & 0x7f)
for e >>= 7; e != 0; e >>= 7 {
i--
b[i] = byte(e | 0x80)
}
// Now reverse bytes, because we set them in the wrong order
// TODO just put them in the right place the first time...
last := len(bs) - 1
for idx := 0; idx < len(bs)/2; idx++ {
bs[idx], bs[last-idx] = bs[last-idx], bs[idx]
}
return append(out, bs...)
return append(out, b[i:]...)
}
// Returns the length of a wire encoded uint64 of this value.

View File

@@ -1,222 +0,0 @@
package main
import "flag"
import "fmt"
import "strings"
import "net"
import "sort"
import "encoding/json"
import "strconv"
import "os"
type admin_info map[string]interface{}
func main() {
server := flag.String("endpoint", "localhost:9001", "Admin socket endpoint")
injson := flag.Bool("json", false, "Output in JSON format")
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Println("usage:", os.Args[0], "[-endpoint=localhost:9001] [-json] command [key=value] [...]")
fmt.Println("example:", os.Args[0], "getPeers")
fmt.Println("example:", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false")
fmt.Println("example:", os.Args[0], "-endpoint=localhost:9001 getDHT")
return
}
conn, err := net.Dial("tcp", *server)
if err != nil {
panic(err)
}
defer conn.Close()
decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn)
send := make(admin_info)
recv := make(admin_info)
for c, a := range args {
if c == 0 {
send["request"] = a
continue
}
tokens := strings.Split(a, "=")
if i, err := strconv.Atoi(tokens[1]); err == nil {
send[tokens[0]] = i
} else {
switch tokens[1] {
case "true":
send[tokens[0]] = true
case "false":
send[tokens[0]] = false
default:
send[tokens[0]] = tokens[1]
}
}
}
if err := encoder.Encode(&send); err != nil {
panic(err)
}
if err := decoder.Decode(&recv); err == nil {
if recv["status"] == "error" {
if err, ok := recv["error"]; ok {
fmt.Println("Error:", err)
} else {
fmt.Println("Unspecified error occured")
}
os.Exit(1)
}
if _, ok := recv["request"]; !ok {
fmt.Println("Missing request in response (malformed response?)")
return
}
if _, ok := recv["response"]; !ok {
fmt.Println("Missing response body (malformed response?)")
return
}
req := recv["request"].(map[string]interface{})
res := recv["response"].(map[string]interface{})
if *injson {
if json, err := json.MarshalIndent(res, "", " "); err == nil {
fmt.Println(string(json))
}
os.Exit(0)
}
switch req["request"] {
case "dot":
fmt.Println(res["dot"])
case "help", "getPeers", "getSwitchPeers", "getDHT", "getSessions":
maxWidths := make(map[string]int)
var keyOrder []string
keysOrdered := false
for _, tlv := range res {
for slk, slv := range tlv.(map[string]interface{}) {
if !keysOrdered {
for k := range slv.(map[string]interface{}) {
keyOrder = append(keyOrder, fmt.Sprint(k))
}
sort.Strings(keyOrder)
keysOrdered = true
}
for k, v := range slv.(map[string]interface{}) {
if len(fmt.Sprint(slk)) > maxWidths["key"] {
maxWidths["key"] = len(fmt.Sprint(slk))
}
if len(fmt.Sprint(v)) > maxWidths[k] {
maxWidths[k] = len(fmt.Sprint(v))
if maxWidths[k] < len(k) {
maxWidths[k] = len(k)
}
}
}
}
if len(keyOrder) > 0 {
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", "")
for _, v := range keyOrder {
fmt.Printf("%-"+fmt.Sprint(maxWidths[v])+"s ", v)
}
fmt.Println()
}
for slk, slv := range tlv.(map[string]interface{}) {
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", slk)
for _, k := range keyOrder {
preformatted := slv.(map[string]interface{})[k]
var formatted string
switch k {
case "bytes_sent", "bytes_recvd":
formatted = fmt.Sprintf("%d", uint(preformatted.(float64)))
case "uptime", "last_seen":
seconds := uint(preformatted.(float64)) % 60
minutes := uint(preformatted.(float64)/60) % 60
hours := uint(preformatted.(float64) / 60 / 60)
formatted = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
default:
formatted = fmt.Sprint(preformatted)
}
fmt.Printf("%-"+fmt.Sprint(maxWidths[k])+"s ", formatted)
}
fmt.Println()
}
}
case "getTunTap", "setTunTap":
for k, v := range res {
fmt.Println("Interface name:", k)
if mtu, ok := v.(map[string]interface{})["mtu"].(float64); ok {
fmt.Println("Interface MTU:", mtu)
}
if tap_mode, ok := v.(map[string]interface{})["tap_mode"].(bool); ok {
fmt.Println("TAP mode:", tap_mode)
}
}
case "getSelf":
for k, v := range res["self"].(map[string]interface{}) {
fmt.Println("IPv6 address:", k)
if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok {
fmt.Println("IPv6 subnet:", subnet)
}
if coords, ok := v.(map[string]interface{})["coords"].(string); ok {
fmt.Println("Coords:", coords)
}
}
case "addPeer", "removePeer", "addAllowedEncryptionPublicKey", "removeAllowedEncryptionPublicKey":
if _, ok := res["added"]; ok {
for _, v := range res["added"].([]interface{}) {
fmt.Println("Added:", fmt.Sprint(v))
}
}
if _, ok := res["not_added"]; ok {
for _, v := range res["not_added"].([]interface{}) {
fmt.Println("Not added:", fmt.Sprint(v))
}
}
if _, ok := res["removed"]; ok {
for _, v := range res["removed"].([]interface{}) {
fmt.Println("Removed:", fmt.Sprint(v))
}
}
if _, ok := res["not_removed"]; ok {
for _, v := range res["not_removed"].([]interface{}) {
fmt.Println("Not removed:", fmt.Sprint(v))
}
}
case "getAllowedEncryptionPublicKeys":
if _, ok := res["allowed_box_pubs"]; !ok {
fmt.Println("All connections are allowed")
} else if res["allowed_box_pubs"] == nil {
fmt.Println("All connections are allowed")
} else {
fmt.Println("Connections are allowed only from the following public box keys:")
for _, v := range res["allowed_box_pubs"].([]interface{}) {
fmt.Println("-", v)
}
}
case "getMulticastInterfaces":
if _, ok := res["multicast_interfaces"]; !ok {
fmt.Println("No multicast interfaces found")
} else if res["multicast_interfaces"] == nil {
fmt.Println("No multicast interfaces found")
} else {
fmt.Println("Multicast peer discovery is active on:")
for _, v := range res["multicast_interfaces"].([]interface{}) {
fmt.Println("-", v)
}
}
default:
if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil {
fmt.Println(string(json))
}
}
}
if v, ok := recv["status"]; ok && v == "error" {
os.Exit(1)
}
os.Exit(0)
}