From d7e6d814a01dbbdba51d666fcc33447123f8411b Mon Sep 17 00:00:00 2001 From: Arceliar Date: Thu, 28 Dec 2017 22:16:20 -0600 Subject: [PATCH] first code/readme/license commit --- LICENSE | 185 ++++ README.md | 92 +- build | 10 + clean | 2 + doc/README.md | 188 ++++ misc/genkeys.go | 77 ++ misc/run-conf2-netns | 23 + misc/run-conf3-netns | 28 + misc/run-conf4-netns | 28 + misc/run-schannel-netns | 69 ++ misc/sim/fc00-2017-08-12.txt | 1593 +++++++++++++++++++++++++++ misc/sim/merge-skitter.py | 60 + misc/sim/merge.py | 60 + misc/sim/test | 4 + misc/sim/treesim-basic.go | 193 ++++ misc/sim/treesim-forward.py | 902 +++++++++++++++ misc/sim/treesim.go | 410 +++++++ misc/tests/atomic-toy.go | 22 + misc/tests/bandwidth.go | 42 + misc/tests/channelbenchmark.go | 36 + misc/tests/gob-test.go | 52 + misc/tests/goroutine-test.go | 22 + misc/tests/multicast.go | 49 + misc/tests/packetbenchmark.go | 78 ++ misc/tests/pool.go | 83 ++ misc/tests/quic.go | 84 ++ misc/tests/socktest.go | 69 ++ misc/tests/socktest2.go | 77 ++ misc/tests/socktest_linklocal.go | 99 ++ misc/tests/socktest_tcp.go | 94 ++ misc/tests/socktest_udp.go | 72 ++ misc/tests/socktest_udp2.go | 72 ++ misc/tests/socktest_udp_nodial.go | 88 ++ misc/tests/socktest_udp_sendmmsg.go | 117 ++ misc/tests/tcptest.go | 96 ++ misc/tests/tunbench-client.go | 82 ++ misc/tests/tunbench-server.go | 127 +++ misc/tests/tunbench.go | 130 +++ misc/tests/tuntest.go | 41 + misc/tests/wire-test.go | 39 + misc/yggdrasil.go.tcp | 209 ++++ src/yggdrasil/address.go | 108 ++ src/yggdrasil/core.go | 64 ++ src/yggdrasil/crypto.go | 154 +++ src/yggdrasil/debug.go | 336 ++++++ src/yggdrasil/dht.go | 383 +++++++ src/yggdrasil/peer.go | 345 ++++++ src/yggdrasil/router.go | 220 ++++ src/yggdrasil/search.go | 168 +++ src/yggdrasil/session.go | 327 ++++++ src/yggdrasil/signature.go | 58 + src/yggdrasil/switch.go | 398 +++++++ src/yggdrasil/tcp.go | 246 +++++ src/yggdrasil/tun.go | 56 + src/yggdrasil/tun_linux.go | 36 + src/yggdrasil/tun_other.go | 12 + src/yggdrasil/udp.go | 275 +++++ src/yggdrasil/util.go | 79 ++ src/yggdrasil/wire.go | 492 +++++++++ yggdrasil.go | 209 ++++ 60 files changed, 9768 insertions(+), 2 deletions(-) create mode 100644 LICENSE create mode 100755 build create mode 100755 clean create mode 100644 doc/README.md create mode 100644 misc/genkeys.go create mode 100755 misc/run-conf2-netns create mode 100755 misc/run-conf3-netns create mode 100755 misc/run-conf4-netns create mode 100755 misc/run-schannel-netns create mode 100644 misc/sim/fc00-2017-08-12.txt create mode 100644 misc/sim/merge-skitter.py create mode 100644 misc/sim/merge.py create mode 100755 misc/sim/test create mode 100644 misc/sim/treesim-basic.go create mode 100644 misc/sim/treesim-forward.py create mode 100644 misc/sim/treesim.go create mode 100644 misc/tests/atomic-toy.go create mode 100644 misc/tests/bandwidth.go create mode 100644 misc/tests/channelbenchmark.go create mode 100644 misc/tests/gob-test.go create mode 100644 misc/tests/goroutine-test.go create mode 100644 misc/tests/multicast.go create mode 100644 misc/tests/packetbenchmark.go create mode 100644 misc/tests/pool.go create mode 100644 misc/tests/quic.go create mode 100644 misc/tests/socktest.go create mode 100644 misc/tests/socktest2.go create mode 100644 misc/tests/socktest_linklocal.go create mode 100644 misc/tests/socktest_tcp.go create mode 100644 misc/tests/socktest_udp.go create mode 100644 misc/tests/socktest_udp2.go create mode 100644 misc/tests/socktest_udp_nodial.go create mode 100644 misc/tests/socktest_udp_sendmmsg.go create mode 100644 misc/tests/tcptest.go create mode 100644 misc/tests/tunbench-client.go create mode 100644 misc/tests/tunbench-server.go create mode 100644 misc/tests/tunbench.go create mode 100644 misc/tests/tuntest.go create mode 100644 misc/tests/wire-test.go create mode 100644 misc/yggdrasil.go.tcp create mode 100644 src/yggdrasil/address.go create mode 100644 src/yggdrasil/core.go create mode 100644 src/yggdrasil/crypto.go create mode 100644 src/yggdrasil/debug.go create mode 100644 src/yggdrasil/dht.go create mode 100644 src/yggdrasil/peer.go create mode 100644 src/yggdrasil/router.go create mode 100644 src/yggdrasil/search.go create mode 100644 src/yggdrasil/session.go create mode 100644 src/yggdrasil/signature.go create mode 100644 src/yggdrasil/switch.go create mode 100644 src/yggdrasil/tcp.go create mode 100644 src/yggdrasil/tun.go create mode 100644 src/yggdrasil/tun_linux.go create mode 100644 src/yggdrasil/tun_other.go create mode 100644 src/yggdrasil/udp.go create mode 100644 src/yggdrasil/util.go create mode 100644 src/yggdrasil/wire.go create mode 100644 yggdrasil.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53320c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,185 @@ +This software is licensed under the LGPLv3, included below. + +As a special exception to the GNU Lesser General Public License version 3 +("LGPL3"), the copyright holders of this Library give you permission to +convey to a third party a Combined Work that links statically or dynamically +to this Library without providing any Minimal Corresponding Source or +Minimal Application Code as set out in 4d or providing the installation +information set out in section 4e, provided that you comply with the other +provisions of LGPL3 and provided that you meet, for the Application the +terms and conditions of the license(s) which apply to the Application. + +Except as stated in this special exception, the provisions of LGPL3 will +continue to comply in full to this Library. If you modify this Library, you +may apply this exception to your version of this Library, but you are not +obliged to do so. If you do not wish to do so, delete this exception +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. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/README.md b/README.md index ce903dc..766a0a2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,90 @@ -# yggdrasil-go -An experiment in scalable routing as an encrypted IPv6 overlay network +# Yggdrasil + +## What is it? + +This is a toy implementation of an encrypted IPv6 network, with many good ideas stolen from [cjdns](https://github.com/cjdelisle/cjdns), which was written to test a particular routing scheme that I cobbled together one random Wednesday afternoon. +It's notably not a shortest path routing scheme, with the goal of scalable name-independent routing on dynamic networks with an internet-like topology. +It's named Yggdrasil after the world tree from Norse mythology, because that seemed like the obvious name given how it works. +For a longer, rambling version of this readme with more information, see: [doc](doc/README.md). + +This is a toy / proof-of-principle, so it's not even alpha quality software--any nontrivial update is likely to break backwards compatibility with no possibility for a clean upgrade path. +You're encouraged to play with it, but I strongly advise against using it for anything mission critical. + +## Obligatory performance propaganda + +A [simplified model](misc/sim/treesim-forward.py) of this routing scheme has been tested in simulation on the 9204-node [skitter](https://www.caida.org/tools/measurement/skitter/) network topology dataset from [caida](https://www.caida.org/), and compared with results in [arxiv:0708.2309](https://arxiv.org/abs/0708.2309). +Using the routing scheme as implemented in this code, I observe an average multiplicative stretch of 1.08, with an average routing table size of 6 for a name-dependent scheme, and approximately 30 additional (but smaller) entries needed for the name-independent routing table. +The number of name-dependent routing table entries needed is proportional to node degree, so that 6 is the mean of a distribution with a long tail, but I believe this is an acceptable tradeoff. +The size of name-dependent routing table enties is relatively large, due to cryptographic signatures associated with routing table updates, but in the absence of cryptographic overhead I believe each entry is otherwise comparable to the BC routing scheme described in the above paper. +A modified version of this scheme, with the same resource requirements, achieves a multiplicative stretch of 1.02, which drops to 1.01 if source routing is used. +Both of these optimizations are not present in the current implementation, as the former depends on network state information that I haven't found a way to cryptographically secure, and the latter optimization is both tedious to implement and would make debugging other aspects of the implementation more difficult. + +## Building + +1. Install Go (tested on 1.9, I use [godeb](https://github.com/niemeyer/godeb)). +2. Clone this repository. +2. `./build` + +The build script sets its own `$GOPATH`, so the build environment is self-contained. +This code only works on linux, due to a few OS-specific parts that I haven't had an interest in rewriting, but see the optional example below for a way to share connectivity with the rest of a network. + +## 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 it will 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` + +This keeps a persistent set of keys (and by extension, IP address) and gives you the option of editing the configuration file. +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 remote devices domain/address and port (as a string, e.g. `"1.2.3.4:5678"`) to the list of `Peers` in the configuration file. + +## Optional: advertise a prefix locally + +Suppose a node has generated 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` changed to `fd80`, a separate `/9` 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 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 {}; +}; +``` + +This is enough to give unsupported devices on my LAN access to the network, with a few security and performance cautions outlined in the [doc](doc/README.md) file. + +## How does it work? + +I'd rather not try to explain in the readme, but I describe it further in the [doc](doc/README.md) file, so you can check there if you're interested. +Be warned that it's still not a very good explanation, but it at least gives a high-level overview and links to some relevant work by other people. +I may try to write another document at some point, to thoroughly explain how everything works, if the need arises. + +## License + +This code is released under the terms of the LGPLv3, but with an added exception that was shamelessly taken from [godeb](https://github.com/niemeyer/godeb). +Under certain circumstances, this exception permits distribution of binaries that are (statically or dynamically) linked with this code, without requiring the distribution of Minimal Corresponding Source or Minimal Application Code. +For more details, see: [LICENSE](LICENSE). + diff --git a/build b/build new file mode 100755 index 0000000..ddd386d --- /dev/null +++ b/build @@ -0,0 +1,10 @@ +#!/bin/bash +export GOPATH=$PWD +echo "Downloading..." +go get -d -v yggdrasil +for file in *.go ; do + echo "Building: $file" + go build -v $file + #go build -ldflags="-s -w" -v $file + #upx --brute ${file/.go/} +done diff --git a/clean b/clean new file mode 100755 index 0000000..4361a7b --- /dev/null +++ b/clean @@ -0,0 +1,2 @@ +#!/bin/bash +git clean -dxf diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..4310c5b --- /dev/null +++ b/doc/README.md @@ -0,0 +1,188 @@ +# 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). + diff --git a/misc/genkeys.go b/misc/genkeys.go new file mode 100644 index 0000000..f4701c7 --- /dev/null +++ b/misc/genkeys.go @@ -0,0 +1,77 @@ +/* + +This file generates crypto keys. +It prints out a new set of keys each time if finds a "better" one. +By default, "better" means a higher NodeID (-> higher IP address). +This is because the IP address format can compress leading 1s in the address, to incrase the number of ID bits in the address. + +If run with the "-sig" flag, it generates signing keys instead. +A "better" signing key means one with a higher TreeID. +This only matters if it's high enough to make you the root of the tree. + +*/ +package main + +import "encoding/hex" +import "flag" +import "fmt" +import . "yggdrasil" + +var doSig = flag.Bool("sig", false, "generate new signing keys instead") + +func main() { + flag.Parse() + switch { + case *doSig: doSigKeys() + default: doBoxKeys() + } +} + +func isBetter(oldID, newID []byte) bool { + for idx := range oldID { + if newID[idx] > oldID[idx] { return true } + if newID[idx] < oldID[idx] { return false } + } + return false +} + +func doBoxKeys() { + c := Core{} + pub, _ := c.DEBUG_newBoxKeys() + bestID := c.DEBUG_getNodeID(pub) + for idx := range bestID { + bestID[idx] = 0 + } + for { + pub, priv := c.DEBUG_newBoxKeys() + id := c.DEBUG_getNodeID(pub) + if !isBetter(bestID[:], id[:]) { continue } + bestID = id + ip := c.DEBUG_addrForNodeID(id) + fmt.Println("--------------------------------------------------------------------------------") + fmt.Println("boxPriv:", hex.EncodeToString(priv[:])) + fmt.Println("boxPub:", hex.EncodeToString(pub[:])) + fmt.Println("NodeID:", hex.EncodeToString(id[:])) + fmt.Println("IP:", ip) + } +} + +func doSigKeys() { + c := Core{} + pub, _ := c.DEBUG_newSigKeys() + bestID := c.DEBUG_getTreeID(pub) + for idx := range bestID { + bestID[idx] = 0 + } + for { + pub, priv := c.DEBUG_newSigKeys() + id := c.DEBUG_getTreeID(pub) + if !isBetter(bestID[:], id[:]) { continue } + bestID = id + fmt.Println("--------------------------------------------------------------------------------") + fmt.Println("sigPriv:", hex.EncodeToString(priv[:])) + fmt.Println("sigPub:", hex.EncodeToString(pub[:])) + fmt.Println("TreeID:", hex.EncodeToString(id[:])) + } +} + diff --git a/misc/run-conf2-netns b/misc/run-conf2-netns new file mode 100755 index 0000000..cedbb4d --- /dev/null +++ b/misc/run-conf2-netns @@ -0,0 +1,23 @@ +#!/bin/sh + +ip netns add peerns +ip link add veth0 type veth peer name veth1 +ifconfig veth0 192.168.2.1/24 up +echo "1" +#tc qdisc add dev veth0 root tbf rate 8mbit burst 8192 latency 1ms +#tc qdisc add dev veth0 root netem delay 50ms 5ms distribution normal +echo "2" +ip link set veth1 netns peerns +ip netns exec peerns ifconfig veth1 192.168.2.2/24 up +echo "3" +#ip netns exec peerns tc qdisc add dev veth1 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns tc qdisc add dev veth1 root netem delay 50ms 5ms distribution normal +echo "4" +ip netns exec peerns ip addr list +#ip netns exec peerns ./run -useconf=conf2.json +ip netns exec peerns ip link set dev lo up +ip netns exec peerns ./run -autoconf -pprof +#GODEBUG=gctrace=1 ip netns exec peerns ./run -autoconf +#ip netns exec peerns ./run -useconf=conf2.json -cpuprofile=cpu2.prof -memprofile=mem2.prof +#ip netns delete peerns + diff --git a/misc/run-conf3-netns b/misc/run-conf3-netns new file mode 100755 index 0000000..da5f494 --- /dev/null +++ b/misc/run-conf3-netns @@ -0,0 +1,28 @@ +#!/bin/sh + +ip netns add peerns3 +ip link add veth23 type veth peer name veth32 +ip link set veth23 netns peerns +ip netns exec peerns ifconfig veth23 192.168.3.1/24 up +#ip netns exec peerns tc qdisc add dev veth23 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns tc qdisc add dev veth23 root netem delay 50ms 5ms distribution normal +ip link set veth32 netns peerns3 +ip netns exec peerns3 ifconfig veth32 192.168.3.2/24 up +#ip netns exec peerns3 tc qdisc add dev veth32 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns3 tc qdisc add dev veth32 root netem delay 50ms 5ms distribution normal +ip netns exec peerns3 ip route add 192.168.2.0/24 via 192.168.3.1 + +#ip link add veth13 type veth peer name veth31 +#ifconfig veth13 192.168.4.1/24 up +#ip netns exec peerns tc qdisc add dev veth23 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns tc qdisc add dev veth23 root netem delay 50ms 5ms distribution normal +#ip link set veth31 netns peerns3 +#ip netns exec peerns3 ifconfig veth32 192.168.4.3/24 up +#ip netns exec peerns3 tc qdisc add dev veth32 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns3 tc qdisc add dev veth32 root netem delay 50ms 5ms distribution normal +#ip netns exec peerns3 ip route add 192.168.2.0/24 via 192.168.3.1 + +ip netns exec peerns3 ip addr list +#ip netns exec peerns3 ./run -useconf=conf3.json +ip netns exec peerns3 ./run -autoconf +#ip netns delete peerns3 diff --git a/misc/run-conf4-netns b/misc/run-conf4-netns new file mode 100755 index 0000000..77b9f85 --- /dev/null +++ b/misc/run-conf4-netns @@ -0,0 +1,28 @@ +#!/bin/sh + +ip netns add peerns4 +ip link add veth34 type veth peer name veth43 +ip link set veth34 netns peerns3 +ip netns exec peerns3 ifconfig veth34 192.168.4.3/24 up +#ip netns exec peerns tc qdisc add dev veth23 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns tc qdisc add dev veth23 root netem delay 50ms 5ms distribution normal +ip link set veth43 netns peerns4 +ip netns exec peerns4 ifconfig veth43 192.168.4.4/24 up +#ip netns exec peerns3 tc qdisc add dev veth32 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns3 tc qdisc add dev veth32 root netem delay 50ms 5ms distribution normal +#ip netns exec peerns4 ip route add 192.168.3.0/24 via 192.168.4.3 + +#ip link add veth13 type veth peer name veth31 +#ifconfig veth13 192.168.4.1/24 up +#ip netns exec peerns tc qdisc add dev veth23 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns tc qdisc add dev veth23 root netem delay 50ms 5ms distribution normal +#ip link set veth31 netns peerns3 +#ip netns exec peerns3 ifconfig veth32 192.168.4.3/24 up +#ip netns exec peerns3 tc qdisc add dev veth32 root tbf rate 8mbit burst 8192 latency 1ms +#ip netns exec peerns3 tc qdisc add dev veth32 root netem delay 50ms 5ms distribution normal +#ip netns exec peerns3 ip route add 192.168.2.0/24 via 192.168.3.1 + +ip netns exec peerns4 ip addr list +#ip netns exec peerns3 ./run -useconf=conf3.json +ip netns exec peerns4 ./run -autoconf +#ip netns delete peerns3 diff --git a/misc/run-schannel-netns b/misc/run-schannel-netns new file mode 100755 index 0000000..fe85f45 --- /dev/null +++ b/misc/run-schannel-netns @@ -0,0 +1,69 @@ +#!/bin/bash + +# Connects nodes in a network resembling an s-channel feynmann diagram. + +# 1 5 +# \ / +# 3--4 +# / \ +# 2 6 + +# Bandwidth constraints are applied to 4<->5 and 4<->6. +# The idea is to make sure that bottlenecks on one link don't affect the other. + +ip netns add node1 +ip netns add node2 +ip netns add node3 +ip netns add node4 +ip netns add node5 +ip netns add node6 + +ip link add veth13 type veth peer name veth31 +ip link set veth13 netns node1 up +ip link set veth31 netns node3 up + +ip link add veth23 type veth peer name veth32 +ip link set veth23 netns node2 up +ip link set veth32 netns node3 up + +ip link add veth34 type veth peer name veth43 +ip link set veth34 netns node3 up +ip link set veth43 netns node4 up + +ip link add veth45 type veth peer name veth54 +ip link set veth45 netns node4 up +ip link set veth54 netns node5 up + +ip link add veth46 type veth peer name veth64 +ip link set veth46 netns node4 up +ip link set veth64 netns node6 up + +ip netns exec node4 tc qdisc add dev veth45 root tbf rate 100mbit burst 8192 latency 1ms +ip netns exec node5 tc qdisc add dev veth54 root tbf rate 100mbit burst 8192 latency 1ms + +ip netns exec node4 tc qdisc add dev veth46 root tbf rate 10mbit burst 8192 latency 1ms +ip netns exec node6 tc qdisc add dev veth64 root tbf rate 10mbit burst 8192 latency 1ms + +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 & + +echo "Started, to continue you should (possibly w/ sudo):" +echo "kill" $(jobs -p) +wait + +ip netns delete node1 +ip netns delete node2 +ip netns delete node3 +ip netns delete node4 +ip netns delete node5 +ip netns delete node6 + +ip link delete veth13 +ip link delete veth23 +ip link delete veth34 +ip link delete veth45 +ip link delete veth46 diff --git a/misc/sim/fc00-2017-08-12.txt b/misc/sim/fc00-2017-08-12.txt new file mode 100644 index 0000000..13fa724 --- /dev/null +++ b/misc/sim/fc00-2017-08-12.txt @@ -0,0 +1,1593 @@ +0 1 +2 3 +2 4 +2 5 +3 6 +3 7 +3 8 +3 9 +3 10 +3 11 +4 11 +4 12 +4 13 +4 14 +4 15 +4 16 +4 17 +4 18 +4 19 +4 20 +4 21 +4 22 +4 23 +4 24 +4 25 +4 26 +4 27 +4 28 +4 29 +4 30 +4 31 +4 32 +4 33 +4 34 +4 35 +4 36 +4 37 +4 38 +4 39 +4 40 +4 41 +4 42 +4 43 +4 44 +4 45 +4 46 +4 47 +4 48 +5 10 +5 11 +5 14 +5 20 +5 22 +5 24 +5 25 +5 29 +5 31 +5 36 +5 39 +5 47 +5 49 +5 50 +5 51 +5 52 +5 53 +5 54 +5 55 +5 56 +5 57 +5 58 +5 59 +5 60 +5 61 +5 62 +5 63 +5 64 +5 65 +5 66 +5 67 +5 68 +5 69 +5 70 +6 71 +6 72 +7 0 +7 6 +7 19 +7 50 +7 73 +7 74 +7 75 +7 76 +7 77 +7 78 +7 79 +7 80 +7 81 +7 82 +7 83 +7 84 +7 85 +7 86 +7 87 +7 88 +7 89 +7 90 +7 91 +7 92 +7 93 +8 50 +9 8 +9 50 +9 94 +9 95 +10 96 +10 97 +10 98 +10 99 +12 5 +12 7 +12 100 +12 101 +12 102 +14 15 +14 26 +14 31 +14 32 +14 103 +14 104 +14 105 +16 5 +16 6 +16 71 +16 101 +16 102 +16 106 +16 107 +16 108 +16 109 +20 7 +20 8 +20 9 +20 110 +20 111 +20 101 +20 102 +21 7 +21 20 +24 8 +24 110 +24 101 +24 112 +24 113 +24 114 +24 115 +24 116 +28 37 +28 38 +28 117 +29 118 +30 119 +31 26 +31 32 +31 104 +32 26 +32 46 +32 105 +33 5 +33 101 +33 102 +34 5 +34 101 +35 7 +38 37 +40 101 +41 120 +41 121 +42 122 +42 123 +45 7 +45 124 +47 8 +52 54 +52 55 +52 56 +52 57 +52 58 +52 59 +52 125 +53 52 +53 54 +53 57 +53 60 +53 61 +53 126 +54 55 +56 55 +56 57 +56 58 +56 59 +56 125 +57 54 +57 55 +57 58 +57 59 +58 54 +58 55 +58 59 +59 54 +59 55 +61 60 +62 52 +62 54 +62 55 +62 56 +62 57 +62 58 +62 59 +62 125 +63 64 +63 127 +65 99 +66 100 +67 49 +70 128 +129 3 +129 6 +129 130 +129 131 +130 7 +130 8 +130 9 +130 10 +130 22 +73 74 +74 0 +74 1 +74 19 +74 75 +74 132 +74 133 +74 76 +135 7 +135 136 +135 137 +136 3 +136 20 +136 50 +136 110 +136 111 +136 138 +136 139 +136 140 +136 141 +136 142 +136 77 +136 78 +136 79 +136 143 +136 144 +136 80 +136 145 +136 146 +136 147 +136 148 +136 149 +136 150 +136 151 +136 152 +136 153 +136 154 +136 155 +136 156 +136 157 +136 158 +137 7 +137 19 +137 20 +137 22 +137 50 +137 73 +137 75 +137 110 +137 111 +137 78 +137 79 +137 143 +137 80 +137 147 +137 149 +137 150 +137 81 +137 86 +137 87 +137 88 +137 89 +137 91 +137 93 +137 159 +137 160 +137 161 +137 162 +137 163 +137 164 +110 7 +110 8 +110 9 +110 65 +110 130 +110 139 +110 79 +110 83 +110 120 +110 108 +110 165 +110 166 +110 114 +110 167 +110 168 +110 169 +110 170 +138 8 +138 9 +138 67 +138 137 +139 7 +140 7 +140 9 +140 110 +140 171 +140 172 +140 173 +140 174 +141 7 +141 137 +141 110 +141 175 +142 7 +142 176 +144 7 +144 137 +80 78 +145 7 +145 110 +146 137 +147 7 +148 7 +148 110 +151 7 +151 137 +153 111 +154 3 +154 6 +156 7 +156 137 +157 77 +158 137 +158 119 +159 7 +160 101 +160 96 +160 97 +161 7 +161 110 +162 7 +162 74 +163 7 +163 74 +164 7 +100 0 +100 3 +100 8 +100 9 +100 20 +100 22 +100 50 +100 69 +100 72 +100 74 +100 84 +100 161 +100 177 +100 178 +100 179 +100 180 +100 181 +100 182 +100 183 +100 94 +100 184 +100 185 +100 186 +100 187 +100 188 +100 189 +100 190 +101 0 +101 18 +101 19 +101 25 +101 49 +101 74 +101 106 +101 107 +102 0 +102 6 +102 19 +102 74 +102 191 +102 192 +102 193 +102 194 +178 7 +180 7 +181 6 +181 72 +182 23 +183 10 +184 7 +184 9 +185 7 +186 195 +187 177 +187 196 +187 197 +187 198 +189 7 +190 199 +106 200 +202 203 +203 8 +203 9 +203 28 +203 204 +204 3 +204 8 +204 9 +204 20 +204 50 +204 130 +204 110 +204 100 +204 94 +204 205 +204 176 +206 4 +206 13 +206 14 +206 15 +206 103 +206 207 +206 208 +206 209 +206 210 +103 13 +103 211 +103 120 +207 15 +207 210 +207 212 +208 15 +209 15 +210 13 +210 14 +210 15 +210 103 +210 213 +211 15 +211 120 +120 13 +212 120 +214 215 +214 216 +214 217 +214 218 +214 219 +217 4 +217 7 +217 136 +217 137 +217 110 +217 220 +220 3 +220 5 +220 12 +220 20 +220 21 +220 22 +220 23 +220 24 +220 49 +220 51 +220 52 +220 53 +220 54 +220 55 +220 56 +220 57 +220 58 +220 59 +220 60 +220 61 +220 62 +220 129 +220 130 +220 137 +220 138 +220 139 +220 82 +220 160 +220 178 +220 180 +220 221 +220 222 +220 223 +220 224 +220 225 +220 226 +220 227 +220 96 +220 228 +220 229 +220 230 +220 231 +220 97 +220 232 +220 233 +220 234 +235 236 +236 237 +236 238 +237 239 +237 240 +237 241 +243 103 +243 210 +243 211 +243 220 +243 126 +243 244 +126 52 +126 56 +126 57 +126 58 +126 59 +126 60 +126 61 +126 62 +244 5 +244 10 +244 179 +244 245 +244 246 +244 112 +244 247 +221 7 +221 201 +222 7 +222 71 +222 100 +222 178 +222 228 +223 23 +223 182 +224 5 +224 52 +224 54 +224 55 +224 56 +224 57 +224 58 +224 59 +224 62 +224 126 +224 232 +224 121 +224 125 +224 248 +226 16 +226 249 +226 250 +228 7 +228 71 +228 100 +228 178 +229 7 +229 233 +230 4 +230 18 +230 24 +230 67 +230 159 +230 121 +230 251 +231 7 +231 252 +231 253 +97 96 +232 5 +232 54 +232 55 +232 56 +232 57 +232 58 +232 59 +232 62 +232 121 +232 125 +232 248 +246 96 +246 97 +246 112 +246 254 +112 255 +112 170 +196 177 +257 4 +257 5 +257 16 +257 101 +257 102 +257 178 +257 258 +258 6 +258 16 +258 71 +258 222 +258 226 +258 113 +113 50 +113 65 +113 71 +113 259 +108 260 +109 101 +262 7 +263 137 +265 17 +128 7 +128 101 +266 100 +266 177 +266 267 +267 177 +268 18 +114 65 +114 165 +168 7 +272 7 +272 136 +272 273 +272 274 +273 274 +273 275 +276 277 +277 92 +278 7 +121 52 +121 53 +121 54 +121 55 +121 56 +121 57 +121 58 +121 59 +121 60 +121 61 +121 62 +121 126 +121 114 +125 55 +125 57 +125 58 +125 59 +248 52 +248 53 +248 55 +248 56 +248 57 +248 59 +248 62 +248 98 +251 116 +116 110 +116 160 +197 177 +279 136 +279 280 +280 136 +281 140 +281 171 +173 9 +283 7 +283 136 +283 137 +283 110 +283 141 +283 175 +284 182 +285 7 +285 101 +285 220 +205 5 +205 7 +205 8 +205 9 +205 136 +205 137 +205 110 +205 142 +205 100 +205 102 +205 113 +205 176 +176 7 +176 8 +176 9 +176 286 +288 5 +288 220 +289 290 +290 10 +290 137 +290 291 +290 292 +290 293 +290 294 +290 295 +291 237 +291 239 +291 296 +291 297 +291 298 +291 299 +293 292 +293 294 +295 110 +300 7 +300 301 +300 302 +300 303 +302 301 +303 301 +304 305 +305 306 +306 118 +307 102 +309 10 +309 310 +310 10 +311 312 +312 237 +313 111 +314 7 +314 137 +316 239 +317 318 +317 319 +318 237 +318 291 +318 239 +318 320 +318 321 +318 322 +320 237 +320 242 +320 291 +320 239 +320 323 +320 296 +320 324 +326 7 +326 136 +326 77 +329 29 +118 330 +331 180 +119 18 +332 333 +334 141 +335 4 +335 5 +335 101 +335 102 +335 336 +335 337 +336 2 +336 3 +336 6 +336 11 +336 12 +336 16 +336 18 +336 20 +336 29 +336 30 +336 33 +336 34 +336 36 +336 37 +336 38 +336 39 +336 63 +336 107 +336 223 +336 257 +336 268 +336 119 +336 338 +336 339 +336 340 +336 341 +336 342 +337 4 +337 5 +337 7 +337 9 +337 12 +337 13 +337 16 +337 20 +337 23 +337 28 +337 33 +337 34 +337 47 +337 50 +337 63 +337 65 +337 70 +337 129 +337 130 +337 136 +337 137 +337 110 +337 84 +337 90 +337 100 +337 101 +337 102 +337 177 +337 179 +337 106 +337 206 +337 220 +337 257 +337 113 +337 261 +337 259 +337 336 +337 343 +337 344 +337 345 +337 346 +337 347 +338 3 +338 4 +338 5 +338 11 +338 339 +339 3 +339 10 +339 11 +341 7 +341 18 +341 22 +341 130 +341 268 +341 348 +343 113 +345 101 +345 102 +347 261 +349 23 +349 100 +349 182 +349 220 +349 223 +349 284 +349 350 +351 5 +352 100 +354 7 +354 137 +355 102 +355 356 +355 357 +356 41 +357 3 +357 4 +357 6 +357 12 +357 14 +357 16 +357 20 +357 26 +357 29 +357 31 +357 32 +357 33 +357 46 +357 66 +357 79 +357 257 +357 171 +357 173 +357 335 +357 337 +357 358 +357 359 +359 4 +359 14 +359 32 +362 7 +362 100 +260 237 +260 363 +364 0 +364 1 +364 7 +364 19 +364 73 +364 74 +364 75 +364 132 +364 133 +364 162 +364 163 +364 100 +364 101 +364 102 +364 365 +364 366 +364 367 +364 368 +364 369 +364 370 +364 371 +364 372 +364 373 +364 374 +364 375 +364 376 +364 377 +365 74 +365 373 +366 74 +367 74 +368 74 +369 7 +369 74 +369 137 +370 74 +371 74 +372 7 +372 74 +372 137 +373 74 +373 132 +374 74 +375 74 +376 74 +377 74 +377 376 +378 323 +379 240 +380 140 +380 171 +381 382 +381 383 +381 384 +381 385 +382 383 +382 384 +383 102 +383 384 +384 386 +384 387 +385 102 +385 384 +386 193 +387 193 +389 390 +391 3 +391 71 +391 101 +391 258 +391 113 +391 339 +393 3 +393 110 +394 291 +395 282 +395 396 +396 390 +397 237 +397 291 +397 318 +397 320 +397 296 +398 160 +348 10 +399 82 +400 4 +400 5 +400 101 +400 102 +400 336 +400 337 +400 357 +400 401 +401 0 +401 6 +401 12 +401 16 +401 20 +401 33 +401 74 +401 102 +401 192 +401 193 +401 257 +401 205 +401 307 +401 335 +401 337 +401 345 +401 355 +401 364 +401 383 +401 385 +401 402 +401 403 +401 404 +401 405 +401 406 +402 102 +403 102 +403 121 +404 4 +404 5 +404 7 +404 16 +404 129 +404 137 +404 100 +404 101 +404 102 +404 220 +404 258 +404 336 +404 337 +405 4 +405 5 +405 102 +406 0 +406 1 +406 7 +406 19 +406 73 +406 74 +406 75 +406 132 +406 133 +406 76 +406 162 +406 163 +406 100 +406 101 +406 102 +406 364 +406 365 +406 366 +406 367 +406 368 +406 369 +406 371 +406 372 +406 373 +406 374 +406 375 +406 376 +406 377 +407 10 +409 237 +409 410 +409 411 +412 102 +413 12 +414 7 +415 16 +415 24 +415 113 +415 337 +416 241 +420 102 +420 402 +422 242 +422 325 +422 423 +424 7 +424 100 +426 136 +427 7 +427 137 +428 7 +428 110 +429 422 +430 131 +430 126 +430 431 +431 129 +432 177 +435 296 +437 7 +437 438 +441 226 +442 5 +443 7 +443 110 +443 168 +444 260 +445 394 +448 5 +449 5 +451 130 +451 328 +452 7 +452 137 +453 101 +453 308 +453 339 +453 341 +454 82 +455 7 +455 137 +456 301 +459 4 +459 5 +459 16 +459 101 +459 102 +459 178 +459 228 +459 257 +459 336 +459 337 +459 357 +459 401 +460 461 +461 237 +461 462 +463 318 +463 464 +465 454 +466 5 +466 10 +466 110 +466 121 +467 267 +468 7 +469 226 +469 250 +471 100 +472 119 +473 312 +474 137 +474 101 +478 110 +479 180 +480 100 +480 186 +480 195 +482 5 +482 53 +482 59 +482 60 +482 61 +482 220 +482 126 +482 483 +483 5 +483 52 +483 53 +483 55 +483 56 +483 59 +483 60 +483 61 +483 62 +483 220 +483 126 +483 224 +483 232 +483 121 +483 125 +484 7 +484 110 +484 408 +486 237 +487 68 +488 300 +488 301 +489 113 +492 209 +493 494 +494 32 +494 389 +494 395 +494 396 +495 15 +496 244 +496 327 +496 497 +497 485 +498 7 +498 137 +500 7 +500 136 +500 137 +501 110 +502 7 +502 137 +503 20 +503 110 +503 92 +504 23 +504 100 +504 182 +504 223 +504 349 +505 92 +505 277 +505 506 +507 7 +507 136 +507 137 +508 280 +510 3 +511 129 +511 357 +512 121 +514 100 +516 242 +517 30 +517 65 +517 114 +518 242 +519 489 +521 237 +522 320 +522 513 +522 523 +524 110 +525 7 +525 137 +526 100 +527 7 +527 77 +527 157 +527 528 +528 77 +528 157 +529 5 +529 110 +530 7 +531 242 +531 450 +532 429 +533 7 +533 136 +533 142 +533 205 +533 286 +534 126 +535 237 +535 536 +537 74 +537 364 +537 406 +538 7 +538 136 +540 148 +541 237 +542 0 +542 3 +542 6 +542 7 +542 8 +542 9 +542 12 +542 20 +542 21 +542 22 +542 42 +542 49 +542 130 +542 74 +542 137 +542 139 +542 140 +542 141 +542 77 +542 79 +542 143 +542 150 +542 84 +542 100 +542 178 +542 180 +542 183 +542 190 +542 204 +542 217 +542 222 +542 226 +542 227 +542 228 +542 229 +542 233 +542 271 +542 283 +542 285 +542 290 +542 291 +542 292 +542 293 +542 294 +542 300 +542 309 +542 326 +542 118 +542 337 +542 349 +542 352 +542 364 +542 404 +542 406 +542 407 +542 417 +542 474 +542 477 +542 504 +542 514 +542 519 +542 527 +542 530 +542 543 +542 544 +542 545 +542 546 +542 547 +542 548 +542 549 +542 550 +542 551 +542 552 +542 553 +542 554 +544 36 +545 190 +546 130 +546 110 +546 184 +546 544 +547 7 +547 8 +547 9 +547 136 +547 100 +547 204 +547 220 +547 244 +547 550 +547 551 +548 7 +548 153 +548 100 +548 220 +548 550 +549 7 +549 136 +549 137 +549 110 +549 141 +549 175 +550 3 +550 20 +550 50 +550 110 +550 111 +550 138 +550 141 +550 79 +550 143 +550 145 +550 146 +550 149 +550 150 +550 158 +550 184 +550 217 +550 229 +550 233 +550 283 +550 205 +550 295 +550 99 +550 337 +550 443 +550 458 +550 484 +550 503 +550 546 +550 549 +550 555 +550 556 +551 3 +551 8 +551 9 +551 20 +551 50 +551 130 +551 110 +551 138 +551 146 +551 100 +551 184 +551 203 +551 204 +551 205 +551 176 +551 337 +552 10 +553 7 +553 100 +553 440 +554 4 +554 7 +554 136 +554 137 +554 101 +554 214 +554 217 +554 218 +554 220 +554 332 +554 433 +554 491 +554 509 +554 550 +557 177 +557 196 +557 432 +558 318 +558 559 +560 4 +560 270 +560 360 +561 110 +562 14 +562 15 +562 563 +564 276 +564 505 +564 565 +565 92 +566 5 +566 220 +569 221 +570 141 +570 175 +573 160 +574 14 +574 31 +574 32 +575 47 +577 7 +577 136 +578 4 +578 5 +578 7 +579 461 +580 110 +581 291 +583 25 +584 237 +585 342 +586 7 +586 137 +587 4 +587 16 +587 101 +587 336 +587 337 +589 14 +589 137 +589 244 +589 96 +589 97 +589 246 +589 112 +589 248 +589 99 +589 254 +589 590 +589 591 +593 312 +595 291 +595 434 +596 169 +596 254 +597 5 +597 52 +597 55 +597 220 +597 126 +597 121 +598 312 +599 161 +600 472 +601 94 +602 7 +602 603 +603 7 +604 381 +604 382 +604 383 +604 384 +604 385 +605 96 +605 97 +606 7 +606 475 +608 99 +609 15 +610 237 +611 239 +612 5 +612 68 +612 487 +613 291 +614 7 +614 142 +614 205 +614 286 +615 7 +615 137 +616 291 +616 457 +617 170 +619 8 +619 9 +619 204 +619 220 +619 243 +619 476 +619 515 +619 551 +619 620 +621 137 +621 110 +621 220 +622 67 +622 121 +623 4 +623 7 +623 74 +623 137 +623 101 +623 102 +623 364 +623 406 +624 74 +624 364 +624 406 +625 63 +625 127 +626 5 +627 110 +627 100 +628 7 +629 461 +630 387 +632 336 +632 340 +632 342 +632 585 +633 244 +633 112 +633 247 +633 255 +633 606 \ No newline at end of file diff --git a/misc/sim/merge-skitter.py b/misc/sim/merge-skitter.py new file mode 100644 index 0000000..0a98f56 --- /dev/null +++ b/misc/sim/merge-skitter.py @@ -0,0 +1,60 @@ +import glob +inputDirPath = "out-skitter" + +inputFilePaths = glob.glob(inputDirPath+"/*") +inputFilePaths.sort() + +merged = dict() + +stretches = [] + +total = 0 +for inputFilePath in inputFilePaths: + print "Processing file {}".format(inputFilePath) + with open(inputFilePath, 'r') as f: + inData = f.readlines() + pathsChecked = 0. + avgStretch = 0. + for line in inData: + dat = line.rstrip('\n').split(' ') + eHops = int(dat[0]) + nHops = int(dat[1]) + count = int(dat[2]) + if eHops not in merged: merged[eHops] = dict() + if nHops not in merged[eHops]: merged[eHops][nHops] = 0 + merged[eHops][nHops] += count + total += count + pathsChecked += count + stretch = float(nHops)/eHops + avgStretch += stretch*count + finStretch = avgStretch / max(1, pathsChecked) + stretches.append(str(finStretch)) + +hopsUsed = 0. +hopsNeeded = 0. +avgStretch = 0. +results = [] +for eHops in sorted(merged.keys()): + for nHops in sorted(merged[eHops].keys()): + count = merged[eHops][nHops] + result = "{} {} {}".format(eHops, nHops, count) + results.append(result) + hopsUsed += nHops*count + hopsNeeded += eHops*count + stretch = float(nHops)/eHops + avgStretch += stretch*count + print result +bandwidthUsage = hopsUsed/max(1, hopsNeeded) +avgStretch /= max(1, total) + +with open("results.txt", "w") as f: + f.write('\n'.join(results)) + +with open("stretches.txt", "w") as f: + f.write('\n'.join(stretches)) + +print "Total files processed: {}".format(len(inputFilePaths)) +print "Total paths found: {}".format(total) +print "Bandwidth usage: {}".format(bandwidthUsage) +print "Average stretch: {}".format(avgStretch) + diff --git a/misc/sim/merge.py b/misc/sim/merge.py new file mode 100644 index 0000000..bd92661 --- /dev/null +++ b/misc/sim/merge.py @@ -0,0 +1,60 @@ +import glob +inputDirPath = "fc00" + +inputFilePaths = glob.glob(inputDirPath+"/*") +inputFilePaths.sort() + +merged = dict() + +stretches = [] + +total = 0 +for inputFilePath in inputFilePaths: + print "Processing file {}".format(inputFilePath) + with open(inputFilePath, 'r') as f: + inData = f.readlines() + pathsChecked = 0. + avgStretch = 0. + for line in inData: + dat = line.rstrip('\n').split(' ') + eHops = int(dat[0]) + nHops = int(dat[1]) + count = int(dat[2]) + if eHops not in merged: merged[eHops] = dict() + if nHops not in merged[eHops]: merged[eHops][nHops] = 0 + merged[eHops][nHops] += count + total += count + pathsChecked += count + stretch = float(nHops)/eHops + avgStretch += stretch*count + finStretch = avgStretch / max(1, pathsChecked) + stretches.append(str(finStretch)) + +hopsUsed = 0. +hopsNeeded = 0. +avgStretch = 0. +results = [] +for eHops in sorted(merged.keys()): + for nHops in sorted(merged[eHops].keys()): + count = merged[eHops][nHops] + result = "{} {} {}".format(eHops, nHops, count) + results.append(result) + hopsUsed += nHops*count + hopsNeeded += eHops*count + stretch = float(nHops)/eHops + avgStretch += stretch*count + print result +bandwidthUsage = hopsUsed/max(1, hopsNeeded) +avgStretch /= max(1, total) + +with open("results.txt", "w") as f: + f.write('\n'.join(results)) + +with open("stretches.txt", "w") as f: + f.write('\n'.join(stretches)) + +print "Total files processed: {}".format(len(inputFilePaths)) +print "Total paths found: {}".format(total) +print "Bandwidth usage: {}".format(bandwidthUsage) +print "Average stretch: {}".format(avgStretch) + diff --git a/misc/sim/test b/misc/sim/test new file mode 100755 index 0000000..a2ce2b6 --- /dev/null +++ b/misc/sim/test @@ -0,0 +1,4 @@ +#!/bin/bash +export GOPATH=$PWD +go get -d yggdrasil +go run misc/sim/treesim.go diff --git a/misc/sim/treesim-basic.go b/misc/sim/treesim-basic.go new file mode 100644 index 0000000..7c68cab --- /dev/null +++ b/misc/sim/treesim-basic.go @@ -0,0 +1,193 @@ +package main + +import "fmt" +import "bufio" +import "os" +import "strings" +import "strconv" +import "time" + +import "runtime/pprof" +import "flag" + +import "router" + +//////////////////////////////////////////////////////////////////////////////// + +type Node struct { + nodeID router.NodeID + table router.Table + links []*Node +} + +func (n *Node) init(nodeID router.NodeID) { + n.nodeID = nodeID + n.table.Init(nodeID) + n.links = append(n.links, n) +} + +func linkNodes(m, n *Node) { + for _, o := range m.links { + if o.nodeID == n.nodeID { + // Don't allow duplicates + return + } + } + m.links = append(m.links, n) + n.links = append(n.links, m) +} + +func makeStoreSquareGrid(sideLength int) map[router.NodeID]*Node { + store := make(map[router.NodeID]*Node) + nNodes := sideLength*sideLength + nodeIDs := make([]router.NodeID, 0, nNodes) + // TODO shuffle nodeIDs + for nodeID := 1 ; nodeID <= nNodes ; nodeID++ { + nodeIDs = append(nodeIDs, router.NodeID(nodeID)) + } + for _, nodeID := range nodeIDs { + node := &Node{} + node.init(nodeID) + store[nodeID] = node + } + for idx := 0 ; idx < nNodes ; idx++ { + if (idx % sideLength) != 0 { + linkNodes(store[nodeIDs[idx]], store[nodeIDs[idx-1]]) + } + if idx >= sideLength { + linkNodes(store[nodeIDs[idx]], store[nodeIDs[idx-sideLength]]) + } + } + return store +} + +func loadGraph(path string) map[router.NodeID]*Node { + f, err := os.Open(path) + if err != nil { panic(err) } + defer f.Close() + store := make(map[router.NodeID]*Node) + s := bufio.NewScanner(f) + for s.Scan() { + line := s.Text() + nodeIDstrs := strings.Split(line, " ") + nodeIDi0, _ := strconv.Atoi(nodeIDstrs[0]) + nodeIDi1, _ := strconv.Atoi(nodeIDstrs[1]) + nodeID0 := router.NodeID(nodeIDi0) + nodeID1 := router.NodeID(nodeIDi1) + if store[nodeID0] == nil { + node := &Node{} + node.init(nodeID0) + store[nodeID0] = node + } + if store[nodeID1] == nil { + node := &Node{} + node.init(nodeID1) + store[nodeID1] = node + } + linkNodes(store[nodeID0], store[nodeID1]) + } + return store +} + +//////////////////////////////////////////////////////////////////////////////// + +func idleUntilConverged(store map[router.NodeID]*Node) { + timeOfLastChange := 0 + step := 0 + // Idle untl the network has converged + for step - timeOfLastChange < 4*router.TIMEOUT { + step++ + fmt.Println("Step:", step, "--", "last change:", timeOfLastChange) + for _, node := range store { + node.table.Tick() + for idx, link := range node.links[1:] { + msg := node.table.CreateMessage(router.Iface(idx)) + for idx, fromNode := range link.links { + if fromNode == node { + //fmt.Println("Sending from node", node.nodeID, "to", link.nodeID) + link.table.HandleMessage(msg, router.Iface(idx)) + break + } + } + } + } + //for _, node := range store { + // if node.table.DEBUG_isDirty() { timeOfLastChange = step } + //} + //time.Sleep(10*time.Millisecond) + } +} + +func testPaths(store map[router.NodeID]*Node) { + nNodes := len(store) + nodeIDs := make([]router.NodeID, 0, nNodes) + for nodeID := range store { + nodeIDs = append(nodeIDs, nodeID) + } + lookups := 0 + count := 0 + start := time.Now() + for _, source := range store { + count++ + fmt.Printf("Testing paths from node %d / %d (%d)\n", count, nNodes, source.nodeID) + for _, dest := range store { + //if source == dest { continue } + destLoc := dest.table.GetLocator() + temp := 0 + for here := source ; here != dest ; { + temp++ + if temp > 16 { panic("Loop?") } + next := here.links[here.table.Lookup(destLoc)] + if next == here { + //for idx, link := range here.links { + // fmt.Println("DUMP:", idx, link.nodeID) + //} + panic(fmt.Sprintln("Routing Loop:", + source.nodeID, + here.nodeID, + dest.nodeID)) + } + //fmt.Println("DEBUG:", source.nodeID, here.nodeID, dest.nodeID) + here = next + lookups++ + } + } + } + timed := time.Since(start) + fmt.Printf("%f lookups per second\n", float64(lookups)/timed.Seconds()) +} + +func dumpStore(store map[router.NodeID]*Node) { + for _, node := range store { + fmt.Println("DUMPSTORE:", node.nodeID, node.table.GetLocator()) + node.table.DEBUG_dumpTable() + } +} + +//////////////////////////////////////////////////////////////////////////////// + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") + +func main() { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + fmt.Println("Test") + store := makeStoreSquareGrid(4) + idleUntilConverged(store) + dumpStore(store) + testPaths(store) + //panic("DYING") + store = loadGraph("hype-2016-09-19.list") + idleUntilConverged(store) + dumpStore(store) + testPaths(store) +} diff --git a/misc/sim/treesim-forward.py b/misc/sim/treesim-forward.py new file mode 100644 index 0000000..718a48b --- /dev/null +++ b/misc/sim/treesim-forward.py @@ -0,0 +1,902 @@ +# Tree routing scheme (named Yggdrasil, after the world tree from Norse mythology) +# Steps: +# 1: Pick any node, here I'm using highest nodeID +# 2: Build spanning tree, each node stores path back to root +# Optionally with weights for each hop +# Ties broken by preferring a parent with higher degree +# 3: Distance metric: self->peer + (via tree) peer->dest +# 4: Perform (modified) greedy lookup via this metric for each direction (A->B and B->A) +# 5: Source-route traffic using the better of those two paths + +# Note: This makes no attempt to simulate a dynamic network +# E.g. A node's peers cannot be disconnected + +# TODO: +# Make better use of drop? +# In particular, we should be ignoring *all* recently dropped *paths* to the root +# To minimize route flapping +# Not really an issue in the sim, but probably needed for a real network + +import array +import gc +import glob +import gzip +import heapq +import os +import random +import time + +############# +# Constants # +############# + +# Reminder of where link cost comes in +LINK_COST = 1 + +# Timeout before dropping something, in simulated seconds +TIMEOUT = 60 + +########### +# Classes # +########### + +class PathInfo: + def __init__(self, nodeID): + self.nodeID = nodeID # e.g. IP + self.coords = [] # Position in tree + self.tstamp = 0 # Timestamp from sender, to keep track of old vs new info + self.degree = 0 # Number of peers the sender has, used to break ties + # The above should be signed + self.path = [nodeID] # Path to node (in path-vector route) + self.time = 0 # Time info was updated, to keep track of e.g. timeouts + self.treeID = nodeID # Hack, let tree use different ID than IP, used so we can dijkstra once and test many roots + def clone(self): + # Return a deep-enough copy of the path + clone = PathInfo(None) + clone.nodeID = self.nodeID + clone.coords = self.coords[:] + clone.tstamp = self.tstamp + clone.degree = self.degree + clone.path = self.path[:] + clone.time = self.time + clone.treeID = self.treeID + return clone +# End class PathInfo + +class Node: + def __init__(self, nodeID): + self.info = PathInfo(nodeID) # Self NodeInfo + self.root = None # PathInfo to node at root of tree + self.drop = dict() # PathInfo to nodes from clus that have timed out + self.peers = dict() # PathInfo to peers + self.links = dict() # Links to peers (to pass messages) + self.msgs = [] # Said messages + self.table = dict() # Pre-computed lookup table of peer info + + def tick(self): + # Do periodic maintenance stuff, including push updates + self.info.time += 1 + if self.info.time > self.info.tstamp + TIMEOUT/4: + # Update timestamp at least once every 1/4 timeout period + # This should probably be randomized in a real implementation + self.info.tstamp = self.info.time + self.info.degree = 0# TODO decide if degree should be used, len(self.peers) + changed = False # Used to track when the network has converged + changed |= self.cleanRoot() + self.cleanDropped() + # Should probably send messages infrequently if there's nothing new to report + if self.info.tstamp == self.info.time: + msg = self.createMessage() + self.sendMessage(msg) + return changed + + def cleanRoot(self): + changed = False + if self.root and self.info.time - self.root.time > TIMEOUT: + print "DEBUG: clean root,", self.root.path + self.drop[self.root.treeID] = self.root + self.root = None + changed = True + if not self.root or self.root.treeID < self.info.treeID: + # No need to drop someone who'se worse than us + self.info.coords = [self.info.nodeID] + self.root = self.info.clone() + changed = True + elif self.root.treeID == self.info.treeID: + self.root = self.info.clone() + return changed + + def cleanDropped(self): + # May actually be a treeID... better to iterate over keys explicitly + nodeIDs = sorted(self.drop.keys()) + for nodeID in nodeIDs: + node = self.drop[nodeID] + if self.info.time - node.time > 4*TIMEOUT: + del self.drop[nodeID] + return None + + def createMessage(self): + # Message is just a tuple + # First element is the sender + # Second element is the root + # We will .clone() everything during the send operation + msg = (self.info, self.root) + return msg + + def sendMessage(self, msg): + for link in self.links.values(): + newMsg = (msg[0].clone(), msg[1].clone()) + link.msgs.append(newMsg) + return None + + def handleMessages(self): + changed = False + while self.msgs: + changed |= self.handleMessage(self.msgs.pop()) + return changed + + def handleMessage(self, msg): + changed = False + for node in msg: + # Update the path and timestamp for the sender and root info + node.path.append(self.info.nodeID) + node.time = self.info.time + # Update the sender's info in our list of peers + sender = msg[0] + self.peers[sender.nodeID] = sender + # Decide if we want to update the root + root = msg[1] + updateRoot = False + isSameParent = False + isBetterParent = False + if len(self.root.path) > 1 and len(root.path) > 1: + parent = self.peers[self.root.path[-2]] + if parent.nodeID == sender.nodeID: isSameParent = True + if sender.degree > parent.degree: + # This would also be where you check path uptime/reliability/whatever + # All else being equal, we prefer parents with high degree + # We are trusting peers to report degree correctly in this case + # So expect some performance reduction if your peers aren't trustworthy + # (Lies can increase average stretch by a few %) + isBetterParent = True + if self.info.nodeID in root.path[:-1]: pass # No loopy routes allowed + elif root.treeID in self.drop and self.drop[root.treeID].tstamp >= root.tstamp: pass + elif not self.root: updateRoot = True + elif self.root.treeID < root.treeID: updateRoot = True + elif self.root.treeID != root.treeID: pass + elif self.root.tstamp > root.tstamp: pass + elif len(root.path) < len(self.root.path): updateRoot = True + elif isBetterParent and len(root.path) == len(self.root.path): updateRoot = True + elif isSameParent and self.root.tstamp < root.tstamp: updateRoot = True + if updateRoot: + if not self.root or self.root.path != root.path: changed = True + self.root = root + self.info.coords = self.root.path + return changed + + def lookup_old(self, dest): + # Note: Can loop in an unconverged network + # The person looking up the route is responsible for checking for loops + best = None + bestDist = 0 + for node in self.peers.itervalues(): + # dist = distance to node + dist (on tree) from node to dest + dist = len(node.path)-1 + treeDist(node.coords, dest.coords) + if not best or dist < bestDist: + best = node + bestDist = dist + if best: + next = best.path[-2] + assert next in self.peers + return next + else: + # We failed to look something up + # TODO some way to signal this which doesn't crash + assert False + + def initTable(self): + # Pre-computes a lookup table for destination coords + # Insert parent first so you prefer them as a next-hop + self.table.clear() + parent = self.info.nodeID + if len(self.info.coords) >= 2: parent = self.info.coords[-2] + for peer in self.peers.itervalues(): + current = self.table + for coord in peer.coords: + if coord not in current: current[coord] = (peer.nodeID, dict()) + old = current[coord] + next = old[1] + oldPeer = self.peers[old[0]] + oldDist = len(oldPeer.coords) + oldDeg = oldPeer.degree + newDist = len(peer.coords) + newDeg = peer.degree + # Prefer parent + # Else prefer short distance from root + # If equal distance, prefer high degree + if peer.nodeID == parent: current[coord] = (peer.nodeID, next) + elif newDist < oldDist: current[coord] = (peer.nodeID, next) + elif newDist == oldDist and newDeg > oldDeg: current[coord] = (peer.nodeID, next) + current = next + return None + + def lookup(self, dest): + # Use pre-computed lookup table to look up next hop for dest coords + assert self.table + if len(self.info.coords) >= 2: parent = self.info.coords[-2] + else: parent = None + current = (parent, self.table) + c = None + for coord in dest.coords: + c = coord + if coord not in current[1]: break + current = current[1][coord] + next = current[0] + if c in self.peers: next = c + if next not in self.peers: + assert next == None + # You're the root of a different connected component + # You'd drop the packet in this case + # To make the path cache not die, need to return a valid next hop... + # Returning self for that reason + next = self.info.nodeID + return next +# End class Node + +#################### +# Helper Functions # +#################### + +def getIndexOfLCA(source, dest): + # Return index of last common ancestor in source/dest coords + # -1 if no common ancestor (e.g. different roots) + lcaIdx = -1 + minLen = min(len(source), len(dest)) + for idx in xrange(minLen): + if source[idx] == dest[idx]: lcaIdx = idx + else: break + return lcaIdx + +def treePath(source, dest): + # Return path with source at head and dest at tail + lastMatch = getIndexOfLCA(source, dest) + path = dest[-1:lastMatch:-1] + source[lastMatch:] + assert path[0] == dest[-1] + assert path[-1] == source[-1] + return path + +def treeDist(source, dest): + dist = len(source) + len(dest) + lcaIdx = getIndexOfLCA(source, dest) + dist -= 2*(lcaIdx+1) + return dist + +def dijkstra(nodestore, startingNodeID): + # Idea to use heapq and basic implementation taken from stackexchange post + # http://codereview.stackexchange.com/questions/79025/dijkstras-algorithm-in-python + nodeIDs = sorted(nodestore.keys()) + nNodes = len(nodeIDs) + idxs = dict() + for nodeIdx in xrange(nNodes): + nodeID = nodeIDs[nodeIdx] + idxs[nodeID] = nodeIdx + dists = array.array("H", [0]*nNodes) + queue = [(0, startingNodeID)] + while queue: + dist, nodeID = heapq.heappop(queue) + idx = idxs[nodeID] + if not dists[idx]: # Unvisited, otherwise we skip it + dists[idx] = dist + for peer in nodestore[nodeID].links: + if not dists[idxs[peer]]: + # Peer is also unvisited, so add to queue + heapq.heappush(queue, (dist+LINK_COST, peer)) + return dists + +def dijkstrall(nodestore): + # Idea to use heapq and basic implementation taken from stackexchange post + # http://codereview.stackexchange.com/questions/79025/dijkstras-algorithm-in-python + nodeIDs = sorted(nodestore.keys()) + nNodes = len(nodeIDs) + idxs = dict() + for nodeIdx in xrange(nNodes): + nodeID = nodeIDs[nodeIdx] + idxs[nodeID] = nodeIdx + dists = array.array("H", [0]*nNodes*nNodes) # use GetCacheIndex(nNodes, start, end) + for sourceIdx in xrange(nNodes): + print "Finding shortest paths for node {} / {} ({})".format(sourceIdx+1, nNodes, nodeIDs[sourceIdx]) + queue = [(0, sourceIdx)] + while queue: + dist, nodeIdx = heapq.heappop(queue) + distIdx = getCacheIndex(nNodes, sourceIdx, nodeIdx) + if not dists[distIdx]: # Unvisited, otherwise we skip it + dists[distIdx] = dist + for peer in nodestore[nodeIDs[nodeIdx]].links: + pIdx = idxs[peer] + pdIdx = getCacheIndex(nNodes, sourceIdx, pIdx) + if not dists[pdIdx]: + # Peer is also unvisited, so add to queue + heapq.heappush(queue, (dist+LINK_COST, pIdx)) + return dists + +def linkNodes(node1, node2): + node1.links[node2.info.nodeID] = node2 + node2.links[node1.info.nodeID] = node1 + +############################ +# Store topology functions # +############################ + +def makeStoreSquareGrid(sideLength, randomize=True): + # Simple grid in a sideLength*sideLength square + # Just used to validate that the code runs + store = dict() + nodeIDs = list(range(sideLength*sideLength)) + if randomize: random.shuffle(nodeIDs) + for nodeID in nodeIDs: + store[nodeID] = Node(nodeID) + for index in xrange(len(nodeIDs)): + if (index % sideLength != 0): linkNodes(store[nodeIDs[index]], store[nodeIDs[index-1]]) + if (index >= sideLength): linkNodes(store[nodeIDs[index]], store[nodeIDs[index-sideLength]]) + print "Grid store created, size {}".format(len(store)) + return store + +def makeStoreASRelGraph(pathToGraph): + #Existing network graphs, in caida.org's asrel format (ASx|ASy|z per line, z denotes relationship type) + with open(pathToGraph, "r") as f: + inData = f.readlines() + store = dict() + for line in inData: + if line.strip()[0] == "#": continue # Skip comment lines + line = line.replace('|'," ") + nodes = map(int, line.split()[0:2]) + if nodes[0] not in store: store[nodes[0]] = Node(nodes[0]) + if nodes[1] not in store: store[nodes[1]] = Node(nodes[1]) + linkNodes(store[nodes[0]], store[nodes[1]]) + print "CAIDA AS-relation graph successfully imported, size {}".format(len(store)) + return store + +def makeStoreASRelGraphMaxDeg(pathToGraph, degIdx=0): + with open(pathToGraph, "r") as f: + inData = f.readlines() + store = dict() + nodeDeg = dict() + for line in inData: + if line.strip()[0] == "#": continue # Skip comment lines + line = line.replace('|'," ") + nodes = map(int, line.split()[0:2]) + if nodes[0] not in nodeDeg: nodeDeg[nodes[0]] = 0 + if nodes[1] not in nodeDeg: nodeDeg[nodes[1]] = 0 + nodeDeg[nodes[0]] += 1 + nodeDeg[nodes[1]] += 1 + sortedNodes = sorted(nodeDeg.keys(), \ + key=lambda x: (nodeDeg[x], x), \ + reverse=True) + maxDegNodeID = sortedNodes[degIdx] + return makeStoreASRelGraphFixedRoot(pathToGraph, maxDegNodeID) + +def makeStoreASRelGraphFixedRoot(pathToGraph, rootNodeID): + with open(pathToGraph, "r") as f: + inData = f.readlines() + store = dict() + for line in inData: + if line.strip()[0] == "#": continue # Skip comment lines + line = line.replace('|'," ") + nodes = map(int, line.split()[0:2]) + if nodes[0] not in store: + store[nodes[0]] = Node(nodes[0]) + if nodes[0] == rootNodeID: store[nodes[0]].info.treeID += 1000000000 + if nodes[1] not in store: + store[nodes[1]] = Node(nodes[1]) + if nodes[1] == rootNodeID: store[nodes[1]].info.treeID += 1000000000 + linkNodes(store[nodes[0]], store[nodes[1]]) + print "CAIDA AS-relation graph successfully imported, size {}".format(len(store)) + return store + +def makeStoreDimesEdges(pathToGraph, rootNodeID=None): + # Read from a DIMES csv-formatted graph from a gzip file + store = dict() + with gzip.open(pathToGraph, "r") as f: + inData = f.readlines() + size = len(inData) + index = 0 + for edge in inData: + if not index % 1000: + pct = 100.0*index/size + print "Processing edge {}, {:.2f}%".format(index, pct) + index += 1 + dat = edge.rstrip().split(',') + node1 = "N" + str(dat[0].strip()) + node2 = "N" + str(dat[1].strip()) + if '?' in node1 or '?' in node2: continue #Unknown node + if node1 == rootNodeID: node1 = "R" + str(dat[0].strip()) + if node2 == rootNodeID: node2 = "R" + str(dat[1].strip()) + if node1 not in store: store[node1] = Node(node1) + if node2 not in store: store[node2] = Node(node2) + if node1 != node2: linkNodes(store[node1], store[node2]) + print "DIMES graph successfully imported, size {}".format(len(store)) + return store + +def makeStoreGeneratedGraph(pathToGraph, root=None): + with open(pathToGraph, "r") as f: + inData = f.readlines() + store = dict() + for line in inData: + if line.strip()[0] == "#": continue # Skip comment lines + nodes = map(int, line.strip().split(' ')[0:2]) + node1 = nodes[0] + node2 = nodes[1] + if node1 == root: node1 += 1000000 + if node2 == root: node2 += 1000000 + if node1 not in store: store[node1] = Node(node1) + if node2 not in store: store[node2] = Node(node2) + linkNodes(store[node1], store[node2]) + print "Generated graph successfully imported, size {}".format(len(store)) + return store + + +############################################ +# Functions used as parts of network tests # +############################################ + +def idleUntilConverged(store): + nodeIDs = sorted(store.keys()) + timeOfLastChange = 0 + step = 0 + # Idle until the network has converged + while step - timeOfLastChange < 4*TIMEOUT: + step += 1 + print "Step: {}, last change: {}".format(step, timeOfLastChange) + changed = False + for nodeID in nodeIDs: + # Update node status, send messages + changed |= store[nodeID].tick() + for nodeID in nodeIDs: + # Process messages + changed |= store[nodeID].handleMessages() + if changed: timeOfLastChange = step + initTables(store) + return store + +def getCacheIndex(nodes, sourceIndex, destIndex): + return sourceIndex*nodes + destIndex + +def initTables(store): + nodeIDs = sorted(store.keys()) + nNodes = len(nodeIDs) + print "Initializing routing tables for {} nodes".format(nNodes) + for idx in xrange(nNodes): + nodeID = nodeIDs[idx] + store[nodeID].initTable() + print "Routing tables initialized" + return None + +def getCache(store): + nodeIDs = sorted(store.keys()) + nNodes = len(nodeIDs) + nodeIdxs = dict() + for nodeIdx in xrange(nNodes): + nodeIdxs[nodeIDs[nodeIdx]] = nodeIdx + cache = array.array("H", [0]*nNodes*nNodes) + for sourceIdx in xrange(nNodes): + sourceID = nodeIDs[sourceIdx] + print "Building fast lookup table for node {} / {} ({})".format(sourceIdx+1, nNodes, sourceID) + for destIdx in xrange(nNodes): + destID = nodeIDs[destIdx] + if sourceID == destID: nextHop = destID # lookup would fail + else: nextHop = store[sourceID].lookup(store[destID].info) + nextHopIdx = nodeIdxs[nextHop] + cache[getCacheIndex(nNodes, sourceIdx, destIdx)] = nextHopIdx + return cache + +def testPaths(store, dists): + cache = getCache(store) + nodeIDs = sorted(store.keys()) + nNodes = len(nodeIDs) + idxs = dict() + for nodeIdx in xrange(nNodes): + nodeID = nodeIDs[nodeIdx] + idxs[nodeID] = nodeIdx + results = dict() + for sourceIdx in xrange(nNodes): + sourceID = nodeIDs[sourceIdx] + print "Testing paths from node {} / {} ({})".format(sourceIdx+1, len(nodeIDs), sourceID) + #dists = dijkstra(store, sourceID) + for destIdx in xrange(nNodes): + destID = nodeIDs[destIdx] + if destID == sourceID: continue # Skip self + distIdx = getCacheIndex(nNodes, sourceIdx, destIdx) + eHops = dists[distIdx] + if not eHops: continue # The network is split, no path exists + hops = 0 + for pair in ((sourceIdx, destIdx),): + nHops = 0 + locIdx = pair[0] + dIdx = pair[1] + while locIdx != dIdx: + locIdx = cache[getCacheIndex(nNodes, locIdx, dIdx)] + nHops += 1 + if not hops or nHops < hops: hops = nHops + if eHops not in results: results[eHops] = dict() + if hops not in results[eHops]: results[eHops][hops] = 0 + results[eHops][hops] += 1 + return results + +def getAvgStretch(pathMatrix): + avgStretch = 0. + checked = 0. + for eHops in sorted(pathMatrix.keys()): + for nHops in sorted(pathMatrix[eHops].keys()): + count = pathMatrix[eHops][nHops] + stretch = float(nHops)/float(max(1, eHops)) + avgStretch += stretch*count + checked += count + avgStretch /= max(1, checked) + return avgStretch + +def getMaxStretch(pathMatrix): + maxStretch = 0. + for eHops in sorted(pathMatrix.keys()): + for nHops in sorted(pathMatrix[eHops].keys()): + stretch = float(nHops)/float(max(1, eHops)) + maxStretch = max(maxStretch, stretch) + return maxStretch + +def getCertSizes(store): + # Returns nCerts frequency distribution + # De-duplicates common certs (for shared prefixes in the path) + sizes = dict() + for node in store.values(): + certs = set() + for peer in node.peers.values(): + pCerts = set() + assert len(peer.path) == 2 + assert peer.coords[-1] == peer.path[0] + hops = peer.coords + peer.path[1:] + for hopIdx in xrange(len(hops)-1): + send = hops[hopIdx] + if send == node.info.nodeID: continue # We created it, already have it + path = hops[0:hopIdx+2] + # Each cert is signed by the sender + # Includes information about the path from the sender to the next hop + # Next hop is at hopIdx+1, so the path to next hop is hops[0:hopIdx+2] + cert = "{}:{}".format(send, path) + certs.add(cert) + size = len(certs) + if size not in sizes: sizes[size] = 0 + sizes[size] += 1 + return sizes + +def getMinLinkCertSizes(store): + # Returns nCerts frequency distribution + # De-duplicates common certs (for shared prefixes in the path) + # Based on the minimum number of certs that must be traded through a particular link + # Handled per link + sizes = dict() + for node in store.values(): + peerCerts = dict() + for peer in node.peers.values(): + pCerts = set() + assert len(peer.path) == 2 + assert peer.coords[-1] == peer.path[0] + hops = peer.coords + peer.path[1:] + for hopIdx in xrange(len(hops)-1): + send = hops[hopIdx] + if send == node.info.nodeID: continue # We created it, already have it + path = hops[0:hopIdx+2] + # Each cert is signed by the sender + # Includes information about the path from the sender to the next hop + # Next hop is at hopIdx+1, so the path to next hop is hops[0:hopIdx+2] + cert = "{}:{}".format(send, path) + pCerts.add(cert) + peerCerts[peer.nodeID] = pCerts + for peer in peerCerts: + size = 0 + pCerts = peerCerts[peer] + for cert in pCerts: + required = True + for p2 in peerCerts: + if p2 == peer: continue + p2Certs = peerCerts[p2] + if cert in p2Certs: required = False + if required: size += 1 + if size not in sizes: sizes[size] = 0 + sizes[size] += 1 + return sizes + +def getPathSizes(store): + # Returns frequency distribution of the total number of hops the routing table + # I.e. a node with 3 peers, each with 5 hop coord+path, would count as 3x5=15 + sizes = dict() + for node in store.values(): + size = 0 + for peer in node.peers.values(): + assert len(peer.path) == 2 + assert peer.coords[-1] == peer.path[0] + peerSize = len(peer.coords) + len(peer.path) - 1 # double-counts peer, -1 + size += peerSize + if size not in sizes: sizes[size] = 0 + sizes[size] += 1 + return sizes + +def getPeerSizes(store): + # Returns frequency distribution of the number of peers each node has + sizes = dict() + for node in store.values(): + nPeers = len(node.peers) + if nPeers not in sizes: sizes[nPeers] = 0 + sizes[nPeers] += 1 + return sizes + +def getAvgSize(sizes): + sumSizes = 0 + nNodes = 0 + for size in sizes: + count = sizes[size] + sumSizes += size*count + nNodes += count + avgSize = float(sumSizes)/max(1, nNodes) + return avgSize + +def getMaxSize(sizes): + return max(sizes.keys()) + +def getMinSize(sizes): + return min(sizes.keys()) + +def getResults(pathMatrix): + results = [] + for eHops in sorted(pathMatrix.keys()): + for nHops in sorted(pathMatrix[eHops].keys()): + count = pathMatrix[eHops][nHops] + results.append("{} {} {}".format(eHops, nHops, count)) + return '\n'.join(results) + +#################################### +# Functions to run different tests # +#################################### + +def runTest(store): + # Runs the usual set of tests on the store + # Does not save results, so only meant for quick tests + # To e.g. check the code works, maybe warm up the pypy jit + for node in store.values(): + node.info.time = random.randint(0, TIMEOUT) + node.info.tstamp = TIMEOUT + print "Begin testing network" + dists = None + if not dists: dists = dijkstrall(store) + idleUntilConverged(store) + pathMatrix = testPaths(store, dists) + avgStretch = getAvgStretch(pathMatrix) + maxStretch = getMaxStretch(pathMatrix) + peers = getPeerSizes(store) + certs = getCertSizes(store) + paths = getPathSizes(store) + linkCerts = getMinLinkCertSizes(store) + avgPeerSize = getAvgSize(peers) + maxPeerSize = getMaxSize(peers) + avgCertSize = getAvgSize(certs) + maxCertSize = getMaxSize(certs) + avgPathSize = getAvgSize(paths) + maxPathSize = getMaxSize(paths) + avgLinkCert = getAvgSize(linkCerts) + maxLinkCert = getMaxSize(linkCerts) + totalCerts = sum(map(lambda x: x*certs[x], certs.keys())) + totalLinks = sum(map(lambda x: x*peers[x], peers.keys())) # one-way links + avgCertsPerLink = float(totalCerts)/max(1, totalLinks) + print "Finished testing network" + print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch) + print "Avg / Max nPeers size: {} / {}".format(avgPeerSize, maxPeerSize) + print "Avg / Max nCerts size: {} / {}".format(avgCertSize, maxCertSize) + print "Avg / Max total hops in any node's routing table: {} / {}".format(avgPathSize, maxPathSize) + print "Avg / Max lower bound cert requests per link (one-way): {} / {}".format(avgLinkCert, maxLinkCert) + print "Avg certs per link (one-way): {}".format(avgCertsPerLink) + return # End of function + +def rootNodeASTest(path, outDir="output-treesim-AS", dists=None, proc = 1): + # Checks performance for every possible choice of root node + # Saves output for each root node to a separate file on disk + # path = input path to some caida.org formatted AS-relationship graph + if not os.path.exists(outDir): os.makedirs(outDir) + assert os.path.exists(outDir) + store = makeStoreASRelGraph(path) + nodes = sorted(store.keys()) + for nodeIdx in xrange(len(nodes)): + if nodeIdx % proc != 0: continue # Work belongs to someone else + rootNodeID = nodes[nodeIdx] + outpath = outDir+"/{}".format(rootNodeID) + if os.path.exists(outpath): + print "Skipping {}, already processed".format(rootNodeID) + continue + store = makeStoreASRelGraphFixedRoot(path, rootNodeID) + for node in store.values(): + node.info.time = random.randint(0, TIMEOUT) + node.info.tstamp = TIMEOUT + print "Beginning {}, size {}".format(nodeIdx, len(store)) + if not dists: dists = dijkstrall(store) + idleUntilConverged(store) + pathMatrix = testPaths(store, dists) + avgStretch = getAvgStretch(pathMatrix) + maxStretch = getMaxStretch(pathMatrix) + results = getResults(pathMatrix) + with open(outpath, "w") as f: + f.write(results) + print "Finished test for root AS {} ({} / {})".format(rootNodeID, nodeIdx+1, len(store)) + print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch) + #break # Stop after 1, because they can take forever + return # End of function + +def timelineASTest(): + # Meant to study the performance of the network as a function of network size + # Loops over a set of AS-relationship graphs + # Runs a test on each graph, selecting highest-degree node as the root + # Saves results for each graph to a separate file on disk + outDir = "output-treesim-timeline-AS" + if not os.path.exists(outDir): os.makedirs(outDir) + assert os.path.exists(outDir) + paths = sorted(glob.glob("asrel/datasets/*")) + for path in paths: + date = os.path.basename(path).split(".")[0] + outpath = outDir+"/{}".format(date) + if os.path.exists(outpath): + print "Skipping {}, already processed".format(date) + continue + store = makeStoreASRelGraphMaxDeg(path) + dists = None + for node in store.values(): + node.info.time = random.randint(0, TIMEOUT) + node.info.tstamp = TIMEOUT + print "Beginning {}, size {}".format(date, len(store)) + if not dists: dists = dijkstrall(store) + idleUntilConverged(store) + pathMatrix = testPaths(store, dists) + avgStretch = getAvgStretch(pathMatrix) + maxStretch = getMaxStretch(pathMatrix) + results = getResults(pathMatrix) + with open(outpath, "w") as f: + f.write(results) + print "Finished {} with {} nodes".format(date, len(store)) + print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch) + #break # Stop after 1, because they can take forever + return # End of function + +def timelineDimesTest(): + # Meant to study the performance of the network as a function of network size + # Loops over a set of AS-relationship graphs + # Runs a test on each graph, selecting highest-degree node as the root + # Saves results for each graph to a separate file on disk + outDir = "output-treesim-timeline-dimes" + if not os.path.exists(outDir): os.makedirs(outDir) + assert os.path.exists(outDir) + # Input files are named ASEdgesX_Y where X = month (no leading 0), Y = year + paths = sorted(glob.glob("DIMES/ASEdges/*.gz")) + exists = set(glob.glob(outDir+"/*")) + for path in paths: + date = os.path.basename(path).split(".")[0] + outpath = outDir+"/{}".format(date) + if outpath in exists: + print "Skipping {}, already processed".format(date) + continue + store = makeStoreDimesEdges(path) + # Get the highest degree node and make it root + # Sorted by nodeID just to make it stable in the event of a tie + nodeIDs = sorted(store.keys()) + bestRoot = "" + bestDeg = 0 + for nodeID in nodeIDs: + node = store[nodeID] + if len(node.links) > bestDeg: + bestRoot = nodeID + bestDeg = len(node.links) + assert bestRoot + store = makeStoreDimesEdges(path, bestRoot) + rootID = "R" + bestRoot[1:] + assert rootID in store + # Don't forget to set random seed before setitng times + # To make results reproducible + nodeIDs = sorted(store.keys()) + random.seed(12345) + for nodeID in nodeIDs: + node = store[nodeID] + node.info.time = random.randint(0, TIMEOUT) + node.info.tstamp = TIMEOUT + print "Beginning {}, size {}".format(date, len(store)) + if not dists: dists = dijkstrall(store) + idleUntilConverged(store) + pathMatrix = testPaths(store, dists) + avgStretch = getAvgStretch(pathMatrix) + maxStretch = getMaxStretch(pathMatrix) + results = getResults(pathMatrix) + with open(outpath, "w") as f: + f.write(results) + print "Finished {} with {} nodes".format(date, len(store)) + print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch) + break # Stop after 1, because they can take forever + return # End of function + +def scalingTest(maxTests=None, inputDir="graphs"): + # Meant to study the performance of the network as a function of network size + # Loops over a set of nodes in a previously generated graph + # Runs a test on each graph, testing each node as the root + # if maxTests is set, tests only that number of roots (highest degree first) + # Saves results for each graph to a separate file on disk + outDir = "output-treesim-{}".format(inputDir) + if not os.path.exists(outDir): os.makedirs(outDir) + assert os.path.exists(outDir) + paths = sorted(glob.glob("{}/*".format(inputDir))) + exists = set(glob.glob(outDir+"/*")) + for path in paths: + gc.collect() # pypy waits for gc to close files + graph = os.path.basename(path).split(".")[0] + store = makeStoreGeneratedGraph(path) + # Get the highest degree node and make it root + # Sorted by nodeID just to make it stable in the event of a tie + nodeIDs = sorted(store.keys(), key=lambda x: len(store[x].links), reverse=True) + dists = None + if maxTests: nodeIDs = nodeIDs[:maxTests] + for nodeID in nodeIDs: + nodeIDStr = str(nodeID).zfill(len(str(len(store)-1))) + outpath = outDir+"/{}-{}".format(graph, nodeIDStr) + if outpath in exists: + print "Skipping {}-{}, already processed".format(graph, nodeIDStr) + continue + store = makeStoreGeneratedGraph(path, nodeID) + # Don't forget to set random seed before setting times + random.seed(12345) # To make results reproducible + nIDs = sorted(store.keys()) + for nID in nIDs: + node = store[nID] + node.info.time = random.randint(0, TIMEOUT) + node.info.tstamp = TIMEOUT + print "Beginning {}, size {}".format(graph, len(store)) + if not dists: dists = dijkstrall(store) + idleUntilConverged(store) + pathMatrix = testPaths(store, dists) + avgStretch = getAvgStretch(pathMatrix) + maxStretch = getMaxStretch(pathMatrix) + results = getResults(pathMatrix) + with open(outpath, "w") as f: + f.write(results) + print "Finished {} with {} nodes for root {}".format(graph, len(store), nodeID) + print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch) + return # End of function + +################## +# Main Execution # +################## + +if __name__ == "__main__": + if True: # Run a quick test + random.seed(12345) # DEBUG + store = makeStoreSquareGrid(4) + runTest(store) # Quick test + store = None + # Do some real work + #runTest(makeStoreDimesEdges("DIMES/ASEdges/ASEdges1_2007.csv.gz")) + #timelineDimesTest() + #rootNodeASTest("asrel/datasets/19980101.as-rel.txt") + #timelineASTest() + #rootNodeASTest("hype-2016-09-19.list", "output-treesim-hype") + #scalingTest(None, "graphs-20") # First argument 1 to only test 1 root per graph + #store = makeStoreGeneratedGraph("bgp_tables") + #store = makeStoreGeneratedGraph("skitter") + #store = makeStoreASRelGraphMaxDeg("hype-2016-09-19.list") #http://hia.cjdns.ca/watchlist/c/walk.peers.20160919 + #store = makeStoreGeneratedGraph("fc00-2017-08-12.txt") + if store: runTest(store) + #rootNodeASTest("skitter", "output-treesim-skitter", None, 0, 1) + #scalingTest(1, "graphs-20") # First argument 1 to only test 1 root per graph + #scalingTest(1, "graphs-21") # First argument 1 to only test 1 root per graph + #scalingTest(1, "graphs-22") # First argument 1 to only test 1 root per graph + #scalingTest(1, "graphs-23") # First argument 1 to only test 1 root per graph + if not store: + import sys + args = sys.argv + if len(args) == 2: + job_number = int(sys.argv[1]) + #rootNodeASTest("fc00-2017-08-12.txt", "fc00", None, job_number) + rootNodeASTest("skitter", "out-skitter", None, job_number) + else: + print "Usage: {} job_number".format(args[0]) + print "job_number = which job set to run on this node (1-indexed)" + diff --git a/misc/sim/treesim.go b/misc/sim/treesim.go new file mode 100644 index 0000000..0a6f948 --- /dev/null +++ b/misc/sim/treesim.go @@ -0,0 +1,410 @@ +package main + +import "fmt" +import "bufio" +import "os" +import "strings" +import "strconv" +import "time" + +import "runtime/pprof" +import "flag" + +import . "yggdrasil" + +//////////////////////////////////////////////////////////////////////////////// + +type Node struct { + index int + core Core + send chan<- []byte + recv <-chan []byte +} + +func (n *Node) init(index int) { + n.index = index + n.core.Init() + n.send = n.core.DEBUG_getSend() + n.recv = n.core.DEBUG_getRecv() +} + +func (n *Node) printTraffic() { + for { + packet := <-n.recv + fmt.Println(n.index, packet) + //panic("Got a packet") + } +} + +func (n *Node) startPeers() { + //for _, p := range n.core.Peers.Ports { + // go p.MainLoop() + //} + //go n.printTraffic() + //n.core.Peers.DEBUG_startPeers() +} + +func linkNodes(m, n *Node) { + // Don't allow duplicates + if m.core.DEBUG_getPeers().DEBUG_hasPeer(n.core.DEBUG_getSigPub()) { return } + // Create peers + // Buffering reduces packet loss in the sim + // This slightly speeds up testing (fewer delays before retrying a ping) + p := m.core.DEBUG_getPeers().DEBUG_newPeer(n.core.DEBUG_getBoxPub(), + n.core.DEBUG_getSigPub()) + q := n.core.DEBUG_getPeers().DEBUG_newPeer(m.core.DEBUG_getBoxPub(), + m.core.DEBUG_getSigPub()) + DEBUG_simLinkPeers(p, q) + return +} + +func makeStoreSquareGrid(sideLength int) map[int]*Node { + store := make(map[int]*Node) + nNodes := sideLength*sideLength + idxs := make([]int, 0, nNodes) + // TODO shuffle nodeIDs + for idx := 1 ; idx <= nNodes ; idx++ { + idxs = append(idxs, idx) + } + for _, idx := range idxs { + node := &Node{} + node.init(idx) + store[idx] = node + } + for idx := 0 ; idx < nNodes ; idx++ { + if (idx % sideLength) != 0 { + linkNodes(store[idxs[idx]], store[idxs[idx-1]]) + } + if idx >= sideLength { + linkNodes(store[idxs[idx]], store[idxs[idx-sideLength]]) + } + } + //for _, node := range store { node.initPorts() } + return store +} + +func makeStoreStar(nNodes int) map[int]*Node { + store := make(map[int]*Node) + center := &Node{} + center.init(0) + store[0] = center + for idx := 1 ; idx < nNodes ; idx++ { + node := &Node{} + node.init(idx) + store[idx] = node + linkNodes(center, node) + } + return store +} + +func loadGraph(path string) map[int]*Node { + f, err := os.Open(path) + if err != nil { panic(err) } + defer f.Close() + store := make(map[int]*Node) + s := bufio.NewScanner(f) + for s.Scan() { + line := s.Text() + nodeIdxstrs := strings.Split(line, " ") + nodeIdx0, _ := strconv.Atoi(nodeIdxstrs[0]) + nodeIdx1, _ := strconv.Atoi(nodeIdxstrs[1]) + if store[nodeIdx0] == nil { + node := &Node{} + node.init(nodeIdx0) + store[nodeIdx0] = node + } + if store[nodeIdx1] == nil { + node := &Node{} + node.init(nodeIdx1) + store[nodeIdx1] = node + } + linkNodes(store[nodeIdx0], store[nodeIdx1]) + } + //for _, node := range store { node.initPorts() } + return store +} + +//////////////////////////////////////////////////////////////////////////////// + +func startNetwork(store map[[32]byte]*Node) { + for _, node := range store { + node.startPeers() + } +} + +func getKeyedStore(store map[int]*Node) map[[32]byte]*Node { + newStore := make(map[[32]byte]*Node) + for _, node := range store { + newStore[node.core.DEBUG_getSigPub()] = node + } + return newStore +} + +func testPaths(store map[[32]byte]*Node) bool { + nNodes := len(store) + count := 0 + for _, source := range store { + count++ + fmt.Printf("Testing paths from node %d / %d (%d)\n", count, nNodes, source.index) + for _, dest := range store { + //if source == dest { continue } + destLoc := dest.core.DEBUG_getLocator() + coords := destLoc.DEBUG_getCoords() + temp := 0 + ttl := ^uint64(0) + oldTTL := ttl + for here := source ; here != dest ; { + if ttl == 0 { + fmt.Println("Drop:", source.index, here.index, dest.index, oldTTL) + return false + } + temp++ + if temp > 4096 { panic("Loop?") } + oldTTL = ttl + nextPort, newTTL := here.core.DEBUG_switchLookup(coords, ttl) + ttl = newTTL + // First check if "here" is accepting packets from the previous node + // TODO explain how this works + ports := here.core.DEBUG_getPeers().DEBUG_getPorts() + nextPeer := ports[nextPort] + if nextPeer == nil { + fmt.Println("Peer associated with next port is nil") + return false + } + next := store[nextPeer.DEBUG_getSigKey()] + /* + if next == here { + //for idx, link := range here.links { + // fmt.Println("DUMP:", idx, link.nodeID) + //} + if nextPort != 0 { panic("This should not be") } + fmt.Println("Failed to route:", source.index, here.index, dest.index, oldTTL, ttl) + //here.table.DEBUG_dumpTable() + //fmt.Println("Ports:", here.nodeID, here.ports) + return false + panic(fmt.Sprintln("Routing Loop:", + source.index, + here.index, + dest.index)) + } + */ + if temp > 4090 { + fmt.Println("DEBUG:", + source.index, source.core.DEBUG_getLocator(), + here.index, here.core.DEBUG_getLocator(), + dest.index, dest.core.DEBUG_getLocator()) + here.core.DEBUG_getSwitchTable().DEBUG_dumpTable() + } + if (here != source) { + // This is sufficient to check for routing loops or blackholes + //break + } + here = next + } + } + } + return true +} + +func stressTest(store map[[32]byte]*Node) { + fmt.Println("Stress testing network...") + nNodes := len(store) + dests := make([][]byte, 0, nNodes) + for _, dest := range store { + loc := dest.core.DEBUG_getLocator() + coords := loc.DEBUG_getCoords() + dests = append(dests, coords) + } + lookups := 0 + start := time.Now() + for _, source := range store { + for _, coords := range dests { + source.core.DEBUG_switchLookup(coords, ^uint64(0)) + lookups++ + } + } + timed := time.Since(start) + fmt.Printf("%d lookups in %s (%f lookups per second)\n", + lookups, + timed, + float64(lookups)/timed.Seconds()) +} + +func pingNodes(store map[[32]byte]*Node) { + fmt.Println("Sending pings...") + nNodes := len(store) + count := 0 + equiv := func (a []byte, b []byte) bool { + if len(a) != len(b) { return false } + for idx := 0 ; idx < len(a) ; idx++ { + if a[idx] != b[idx] { return false } + } + return true + } + for _, source := range store { + count++ + //if count > 16 { break } + fmt.Printf("Sending packets from node %d/%d (%d)\n", count, nNodes, source.index) + sourceKey := source.core.DEBUG_getBoxPub() + payload := sourceKey[:] + sourceAddr := source.core.DEBUG_getAddr()[:] + sendTo := func (bs []byte, destAddr []byte) { + packet := make([]byte, 40+len(bs)) + copy(packet[8:24], sourceAddr) + copy(packet[24:40], destAddr) + copy(packet[40:], bs) + source.send<-packet + } + destCount := 0 + for _, dest := range store { + destCount += 1 + fmt.Printf("%d Nodes, %d Send, %d Recv\n", nNodes, count, destCount) + if dest == source { + fmt.Println("Skipping self") + continue + } + 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() + for loop := true ; loop ; { + select { + case packet := <-dest.recv: { + if equiv(payload, packet[len(packet)-len(payload):]) { + loop = false + } + } + case <-ch: sendTo(payload, destAddr) + } + } + ticker.Stop() + } + //break // Only try sending pings from 1 node + // This is because, for some reason, stopTun() doesn't always close it + // And if two tuns are up, bad things happen (sends via wrong interface) + } + fmt.Println("Finished pinging nodes") +} + +func pingBench(store map[[32]byte]*Node) { + fmt.Println("Benchmarking pings...") + nPings := 0 + payload := make([]byte, 1280+40) // MTU + ipv6 header + var timed time.Duration + //nNodes := len(store) + count := 0 + for _, source := range store { + count++ + //fmt.Printf("Sending packets from node %d/%d (%d)\n", count, nNodes, source.index) + getPing := func (key [32]byte, decodedCoords []byte) []byte { + // TODO write some function to do this the right way, put... somewhere... + coords := DEBUG_wire_encode_coords(decodedCoords) + packet := make([]byte, 0, len(key)+len(coords)+len(payload)) + packet = append(packet, key[:]...) + packet = append(packet, coords...) + packet = append(packet, payload[:]...) + return packet + } + for _, dest := range store { + key := dest.core.DEBUG_getBoxPub() + loc := dest.core.DEBUG_getLocator() + coords := loc.DEBUG_getCoords() + ping := getPing(key, coords) + // TODO make sure the session is open first + start := time.Now() + for i := 0 ; i < 1000000 ; i++{ source.send<-ping ; nPings++ } + timed += time.Since(start) + break + } + break + } + fmt.Printf("Sent %d pings in %s (%f per second)\n", + nPings, + timed, + float64(nPings)/timed.Seconds()) +} + +func dumpStore(store map[NodeID]*Node) { + for _, node := range store { + fmt.Println("DUMPSTORE:", node.index, node.core.DEBUG_getLocator()) + node.core.DEBUG_getSwitchTable().DEBUG_dumpTable() + } +} + +func dumpDHTSize(store map[[32]byte]*Node) { + var min, max, sum int + for _, node := range store { + num := node.core.DEBUG_getDHTSize() + min = num + max = num + break + } + for _, node := range store { + num := node.core.DEBUG_getDHTSize() + if num < min { min = num } + if num > max { max = num } + sum += num + } + avg := float64(sum)/float64(len(store)) + fmt.Printf("DHT min %d / avg %f / max %d\n", min, avg, max) +} + +//////////////////////////////////////////////////////////////////////////////// + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main() { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + fmt.Println("Test") + Util_testAddrIDMask() + idxstore := makeStoreSquareGrid(4) + //idxstore := makeStoreStar(256) + //idxstore := loadGraph("misc/sim/hype-2016-09-19.list") + //idxstore := loadGraph("misc/sim/fc00-2017-08-12.txt") + //idxstore := loadGraph("skitter") + kstore := getKeyedStore(idxstore) + /* + for _, n := range kstore { + log := n.core.DEBUG_getLogger() + log.SetOutput(os.Stderr) + } + */ + startNetwork(kstore) + //time.Sleep(10*time.Second) + // Note that testPaths only works if pressure is turend off + // Otherwise congestion can lead to routing loops? + for finished := false; !finished ; { finished = testPaths(kstore) } + pingNodes(kstore) + //pingBench(kstore) // Only after disabling debug output + //stressTest(kstore) + //time.Sleep(120*time.Second) + dumpDHTSize(kstore) // note that this uses racey functions to read things... +} + diff --git a/misc/tests/atomic-toy.go b/misc/tests/atomic-toy.go new file mode 100644 index 0000000..14228b6 --- /dev/null +++ b/misc/tests/atomic-toy.go @@ -0,0 +1,22 @@ +package main + +import "fmt" +import "time" +import "sync/atomic" +import "runtime" + +func main() { + + var ops uint64 = 0 + for i := 0 ; i < 4 ; i++ { + go func () { + for { + atomic.AddUint64(&ops, 1) + runtime.Gosched() + } + }() + } + time.Sleep(1*time.Second) + opsFinal := atomic.LoadUint64(&ops) + fmt.Println("ops:", opsFinal) +} diff --git a/misc/tests/bandwidth.go b/misc/tests/bandwidth.go new file mode 100644 index 0000000..cdcb30d --- /dev/null +++ b/misc/tests/bandwidth.go @@ -0,0 +1,42 @@ +package main + +import "fmt" +import "net" +import "time" + +func main () { + addr, err := net.ResolveTCPAddr("tcp", "[::1]:9001") + if err != nil { panic(err) } + listener, err := net.ListenTCP("tcp", addr) + if err != nil { panic(err) } + defer listener.Close() + + packetSize := 65535 + numPackets := 65535 + + go func () { + send, err := net.DialTCP("tcp", nil, addr) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, packetSize) + for idx := 0 ; idx < numPackets ; idx++ { send.Write(msg) } + }() + + start := time.Now() + //msg := make([]byte, 1280) + sock, err := listener.AcceptTCP() + if err != nil { panic(err) } + defer sock.Close() + read := 0 + buf := make([]byte, packetSize) + for { + n, err := sock.Read(buf) + read += n + if err != nil { break } + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + fmt.Printf("%f bits/sec\n", 8*float64(read)/timed.Seconds()) +} + diff --git a/misc/tests/channelbenchmark.go b/misc/tests/channelbenchmark.go new file mode 100644 index 0000000..525a343 --- /dev/null +++ b/misc/tests/channelbenchmark.go @@ -0,0 +1,36 @@ +package main + +import "time" +import "fmt" +import "sync" + +func main() { + fmt.Println("Testing speed of recv+send loop") + const count = 10000000 + c := make(chan []byte, 1) + c<-[]byte{} + var wg sync.WaitGroup + worker := func () { + for idx := 0 ; idx < count ; idx++ { + p := <-c + select { + case c<-p: + default: + } + } + wg.Done() + } + nIter := 0 + start := time.Now() + for idx := 0 ; idx < 1 ; idx++ { + go worker() + nIter += count + wg.Add(1) + } + wg.Wait() + stop := time.Now() + timed := stop.Sub(start) + fmt.Printf("%d iterations in %s\n", nIter, timed) + fmt.Printf("%f iterations per second\n", float64(nIter)/timed.Seconds()) + fmt.Printf("%s per iteration\n", timed/time.Duration(nIter)) +} diff --git a/misc/tests/gob-test.go b/misc/tests/gob-test.go new file mode 100644 index 0000000..6bafb2d --- /dev/null +++ b/misc/tests/gob-test.go @@ -0,0 +1,52 @@ +package main + +import "bytes" +import "encoding/gob" +import "time" +import "fmt" + +type testStruct struct { + First uint64 + Second float64 + Third []byte +} + + +func testFunc(tickerDuration time.Duration) { + chn := make(chan []byte) + ticker := time.NewTicker(tickerDuration) + defer ticker.Stop() + send := testStruct{First: 1, Second: 2, Third: []byte{3, 4, 5}} + buf := bytes.NewBuffer(nil) + enc := gob.NewEncoder(buf) + dec := gob.NewDecoder(buf) + sendCall := func () { + err := enc.EncodeValue(&send) + if err != nil { panic(err) } + bs := make([]byte, buf.Len()) + buf.Read(bs) + fmt.Println("send:", bs) + go func() { chn<-bs }() + } + recvCall := func (bs []byte) { + buf.Write(bs) + recv := testStruct{} + err := dec.DecodeValue(&recv) + fmt.Println("recv:", bs) + if err != nil { panic(err) } + } + for { + select { + case bs := <-chn : recvCall(bs) + case <-ticker.C : sendCall() + } + } +} + +func main() { + go testFunc(100*time.Millisecond) // Does not crash + time.Sleep(time.Second) + go testFunc(time.Nanosecond) // Does crash + time.Sleep(time.Second) +} + diff --git a/misc/tests/goroutine-test.go b/misc/tests/goroutine-test.go new file mode 100644 index 0000000..d1b8dfe --- /dev/null +++ b/misc/tests/goroutine-test.go @@ -0,0 +1,22 @@ +package main + +import "sync" +import "time" +import "fmt" + +func main () { + const reqs = 1000000 + var wg sync.WaitGroup + start := time.Now() + for idx := 0 ; idx < reqs ; idx++ { + wg.Add(1) + go func () { wg.Done() } () + } + wg.Wait() + stop := time.Now() + timed := stop.Sub(start) + fmt.Printf("%d goroutines in %s (%f per second)\n", + reqs, + timed, + reqs/timed.Seconds()) +} diff --git a/misc/tests/multicast.go b/misc/tests/multicast.go new file mode 100644 index 0000000..008efaa --- /dev/null +++ b/misc/tests/multicast.go @@ -0,0 +1,49 @@ +package main + +import "fmt" +import "net" +import "time" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + //addr, err := net.ResolveUDPAddr("udp", "[ff02::1%veth0]:9001") + addr, err := net.ResolveUDPAddr("udp", "[ff02::1]:9001") + if err != nil { panic(err) } + sock, err := net.ListenMulticastUDP("udp", nil, addr) + if err != nil { panic(err) } + defer sock.Close() + + go func () { + saddr, err := net.ResolveUDPAddr("udp", "[::]:0") + if err != nil { panic(err) } + send, err := net.ListenUDP("udp", saddr) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, 1280) + for { + //fmt.Println("Sending...") + send.WriteTo(msg, addr) + } + }() + + numPackets := 1000 + start := time.Now() + msg := make([]byte, 2000) + for i := 0 ; i < numPackets ; i++ { + //fmt.Println("Reading:", i) + sock.ReadFromUDP(msg) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + +} + +func main () { + + basic_test() + +} diff --git a/misc/tests/packetbenchmark.go b/misc/tests/packetbenchmark.go new file mode 100644 index 0000000..a311461 --- /dev/null +++ b/misc/tests/packetbenchmark.go @@ -0,0 +1,78 @@ +package main + +import "fmt" +import "net" +import "time" + + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + var ip *net.IP + ifaces, err := net.Interfaces() + if err != nil { panic(err) } + var zone string + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { panic(err) } + for _, addr := range addrs { + addrIP, _, _ := net.ParseCIDR(addr.String()) + if addrIP.To4() != nil { continue } // IPv6 only + if !addrIP.IsLinkLocalUnicast() { continue } + zone = iface.Name + ip = &addrIP + } + addrs, err = iface.MulticastAddrs() + if err != nil { panic(err) } + for _, addr := range addrs { + fmt.Println(addr.String()) + } + } + if ip == nil { panic("No link-local IPv6 found") } + fmt.Println("Using address:", *ip) + + addr := net.UDPAddr{IP: *ip, Port: 9001, Zone: zone} + + saddr := net.UDPAddr{IP: *ip, Port: 9002, Zone: zone} + send, err := net.ListenUDP("udp", &saddr) + defer send.Close() + if err != nil { panic(err) } + sock, err := net.ListenUDP("udp", &addr) + defer sock.Close() + if err != nil { panic(err) } + + const buffSize = 1048576*100 + + send.SetWriteBuffer(buffSize) + sock.SetReadBuffer(buffSize) + sock.SetWriteBuffer(buffSize) + + + go func () { + msg := make([]byte, 1280) + for { + send.WriteTo(msg, &addr) + } + }() + + numPackets := 100000 + start := time.Now() + msg := make([]byte, 2000) + for i := 0 ; i < numPackets ; i++ { + _, addr, _ := sock.ReadFrom(msg) + sock.WriteTo(msg, addr) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + +} + +func main () { + + basic_test() + +} diff --git a/misc/tests/pool.go b/misc/tests/pool.go new file mode 100644 index 0000000..7b004ff --- /dev/null +++ b/misc/tests/pool.go @@ -0,0 +1,83 @@ +package main + +import "fmt" +//import "net" +import "time" +import "runtime" +import "sync/atomic" + +func poolbench() { + nWorkers := runtime.GOMAXPROCS(0) + work := make(chan func(), 1) + workers := make(chan chan<- func(), nWorkers) + makeWorker := func() chan<- func() { + ch := make(chan func()) + go func() { + for { + f := <-ch + f() + select { + case workers<-(ch): + default: return + } + } + }() + return ch + } + getWorker := func() chan<- func() { + select { + case ch := <-workers: return ch + default: return makeWorker() + } + } + dispatcher := func() { + for { + w := <-work + ch := getWorker() + ch<-w + } + } + go dispatcher() + var count uint64 + const nCounts = 1000000 + for idx := 0 ; idx < nCounts ; idx++ { + f := func() { atomic.AddUint64(&count, 1) } + work <- f + } + for atomic.LoadUint64(&count) < nCounts {} +} + +func normalbench() { + var count uint64 + const nCounts = 1000000 + ch := make(chan struct{}, 1) + ch<-struct{}{} + for idx := 0 ; idx < nCounts ; idx++ { + f := func() { atomic.AddUint64(&count, 1) } + f() + <-ch + ch<-struct{}{} + } +} + +func gobench() { + var count uint64 + const nCounts = 1000000 + for idx := 0 ; idx < nCounts ; idx++ { + f := func() { atomic.AddUint64(&count, 1) } + go f() + } + for atomic.LoadUint64(&count) < nCounts {} +} + +func main() { + start := time.Now() + poolbench() + fmt.Println(time.Since(start)) + start = time.Now() + normalbench() + fmt.Println(time.Since(start)) + start = time.Now() + gobench() + fmt.Println(time.Since(start)) +} diff --git a/misc/tests/quic.go b/misc/tests/quic.go new file mode 100644 index 0000000..6b1559d --- /dev/null +++ b/misc/tests/quic.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + "time" + "bytes" + "sync" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "math/big" + quic "github.com/lucas-clemente/quic-go" +) + +const addr = "[::1]:9001" + +func main () { + go run_server() + run_client() +} + +func run_server() { + listener, err := quic.ListenAddr(addr, generateTLSConfig(), nil) + if err != nil { panic(err) } + ses, err := listener.Accept() + if err != nil { panic(err) } + for { + stream, err := ses.AcceptStream() + if err != nil { panic(err) } + go func() { + defer stream.Close() + bs := bytes.Buffer{} + _, err := bs.ReadFrom(stream) + if err != nil { panic(err) } //<-- TooManyOpenStreams + }() + } +} + +func run_client() { + msgSize := 1048576 + msgCount := 128 + ses, err := quic.DialAddr(addr, &tls.Config{InsecureSkipVerify: true}, nil) + if err != nil { panic(err) } + bs := make([]byte, msgSize) + wg := sync.WaitGroup{} + start := time.Now() + for idx := 0 ; idx < msgCount ; idx++ { + wg.Add(1) + go func() { + defer wg.Done() + stream, err := ses.OpenStreamSync() + if err != nil { panic(err) } + defer stream.Close() + stream.Write(bs) + }() // "go" this later + } + wg.Wait() + timed := time.Since(start) + fmt.Println("Client finished", timed, fmt.Sprintf("%f Bits/sec", 8*float64(msgSize*msgCount)/timed.Seconds())) +} + +// Setup a bare-bones TLS config for the server +func generateTLSConfig() *tls.Config { + key, err := rsa.GenerateKey(rand.Reader, 1024) + if err != nil { + panic(err) + } + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key) + if err != nil { + panic(err) + } + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + panic(err) + } + return &tls.Config{Certificates: []tls.Certificate{tlsCert}} +} + diff --git a/misc/tests/socktest.go b/misc/tests/socktest.go new file mode 100644 index 0000000..4f85a75 --- /dev/null +++ b/misc/tests/socktest.go @@ -0,0 +1,69 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + addr, err := net.ResolveUDPAddr("udp", "[::1]:9001") + if err != nil { panic(err) } + sock, err := net.ListenUDP("udp", addr) + if err != nil { panic(err) } + defer sock.Close() + + go func () { + send, err := net.DialUDP("udp", nil, addr) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, 1280) + for { + send.Write(msg) + } + }() + + numPackets := 1000000 + start := time.Now() + msg := make([]byte, 2000) + for i := 0 ; i < numPackets ; i++ { + sock.ReadFrom(msg) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/socktest2.go b/misc/tests/socktest2.go new file mode 100644 index 0000000..f7ce212 --- /dev/null +++ b/misc/tests/socktest2.go @@ -0,0 +1,77 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + addr, err := net.ResolveUDPAddr("udp", "[::1]:9001") + if err != nil { panic(err) } + sock, err := net.ListenUDP("udp", addr) + if err != nil { panic(err) } + defer sock.Close() + + go func () { + send, err := net.DialUDP("udp", nil, addr) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, 1280) + bss := make(net.Buffers, 0, 1024) + for { + for len(bss) < 1024 { + bss = append(bss, msg) + } + bss.WriteTo(send) + //bss = bss[:0] + //send.Write(msg) + } + }() + + numPackets := 1000 + start := time.Now() + msg := make([]byte, 2000) + for i := 0 ; i < numPackets ; i++ { + n, err := sock.Read(msg) + if err != nil { panic(err) } + fmt.Println(n) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/socktest_linklocal.go b/misc/tests/socktest_linklocal.go new file mode 100644 index 0000000..0b50241 --- /dev/null +++ b/misc/tests/socktest_linklocal.go @@ -0,0 +1,99 @@ +package main + +import "flag" +import "fmt" +import "net" +import "os" +import "runtime/pprof" +import "time" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + var ip *net.IP + ifaces, err := net.Interfaces() + if err != nil { panic(err) } + var zone string + for _, iface := range ifaces { + addrs, err := iface.Addrs() + if err != nil { panic(err) } + for _, addr := range addrs { + addrIP, _, _ := net.ParseCIDR(addr.String()) + if addrIP.To4() != nil { continue } // IPv6 only + if !addrIP.IsLinkLocalUnicast() { continue } + fmt.Println(iface.Name, addrIP) + zone = iface.Name + ip = &addrIP + } + if ip != nil { break } + /* + addrs, err = iface.MulticastAddrs() + if err != nil { panic(err) } + for _, addr := range addrs { + fmt.Println(addr.String()) + } + */ + } + if ip == nil { panic("No link-local IPv6 found") } + fmt.Println("Using address:", *ip) + addr := net.UDPAddr{IP: *ip, Port: 9001, Zone: zone} + + laddr, err := net.ResolveUDPAddr("udp", "[::]:9001") + if err != nil { panic(err) } + sock, err := net.ListenUDP("udp", laddr) + if err != nil { panic(err) } + defer sock.Close() + + go func () { + send, err := net.DialUDP("udp", nil, &addr) + //send, err := net.ListenUDP("udp", nil) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, 1280) + for { + send.Write(msg) + //send.WriteToUDP(msg, &addr) + } + }() + + numPackets := 1000000 + start := time.Now() + msg := make([]byte, 2000) + for i := 0 ; i < numPackets ; i++ { + sock.ReadFromUDP(msg) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/socktest_tcp.go b/misc/tests/socktest_tcp.go new file mode 100644 index 0000000..959a92c --- /dev/null +++ b/misc/tests/socktest_tcp.go @@ -0,0 +1,94 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible? + +const buffSize = 32 + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + addr, err := net.ResolveTCPAddr("tcp", "[::1]:9001") + if err != nil { panic(err) } + listener, err := net.ListenTCP("tcp", addr) + if err != nil { panic(err) } + defer listener.Close() + + go func () { + send, err := net.DialTCP("tcp", nil, addr) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, 1280) + bss := make(net.Buffers, 0, 1024) + for { + for len(bss) < 1 { //buffSize { + bss = append(bss, msg) + } + bss := net.Buffers{[]byte{0,1,2,3}, []byte{0,1}, msg} + bss.WriteTo(send) + //send.Write(msg) + } + }() + + numPackets := 1000000 + start := time.Now() + //msg := make([]byte, 1280) + sock, err := listener.AcceptTCP() + if err != nil { panic(err) } + defer sock.Close() + for i := 0 ; i < numPackets ; i++ { + msg := make([]byte, 1280*buffSize) + n, err := sock.Read(msg) + if err != nil { panic(err) } + msg = msg[:n] + for len(msg) > 1286 { + // handle message + i++ + msg = msg[1286:] + } + // handle remaining fragment of message + //fmt.Println(n) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + + _ = func (in (chan<- int)) { + close(in) + } + +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/socktest_udp.go b/misc/tests/socktest_udp.go new file mode 100644 index 0000000..e0181ba --- /dev/null +++ b/misc/tests/socktest_udp.go @@ -0,0 +1,72 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + addr, err := net.ResolveUDPAddr("udp", "[::1]:0") + if err != nil { panic(err) } + sock, err := net.ListenUDP("udp", addr) + if err != nil { panic(err) } + defer sock.Close() + + go func () { + raddr := sock.LocalAddr().(*net.UDPAddr) + send, err := net.DialUDP("udp", nil, raddr) + //send, err := net.ListenUDP("udp", addr) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, 1280) + for { + send.Write(msg) + //send.WriteToUDP(msg, raddr) + } + }() + + numPackets := 1000000 + start := time.Now() + msg := make([]byte, 2000) + for i := 0 ; i < numPackets ; i++ { + sock.ReadFromUDP(msg) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/socktest_udp2.go b/misc/tests/socktest_udp2.go new file mode 100644 index 0000000..ed2db61 --- /dev/null +++ b/misc/tests/socktest_udp2.go @@ -0,0 +1,72 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + saddr, err := net.ResolveUDPAddr("udp", "[::1]:9001") + if err != nil { panic(err) } + raddr, err := net.ResolveUDPAddr("udp", "[::1]:9002") + if err != nil { panic(err) } + + send, err := net.DialUDP("udp", saddr, raddr) + if err != nil { panic(err) } + defer send.Close() + + recv, err := net.DialUDP("udp", raddr, saddr) + if err != nil { panic(err) } + defer recv.Close() + + go func () { + msg := make([]byte, 1280) + for { + send.Write(msg) + } + }() + + numPackets := 1000000 + start := time.Now() + msg := make([]byte, 2000) + for i := 0 ; i < numPackets ; i++ { + recv.Read(msg) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/socktest_udp_nodial.go b/misc/tests/socktest_udp_nodial.go new file mode 100644 index 0000000..7641f0a --- /dev/null +++ b/misc/tests/socktest_udp_nodial.go @@ -0,0 +1,88 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + sock, err := net.ListenUDP("udp", nil) + if err != nil { panic(err) } + defer sock.Close() + + ch := make(chan []byte, 1) + + writer := func () { + raddr := sock.LocalAddr().(*net.UDPAddr) + //send, err := net.ListenUDP("udp", nil) + //if err != nil { panic(err) } + //defer send.Close() + for { + select { + case <-ch: + default: + } + msg := make([]byte, 1280) + sock.WriteToUDP(msg, raddr) + //send.WriteToUDP(msg, raddr) + } + } + go writer() + //go writer() + //go writer() + //go writer() + + numPackets := 65536 + size := 0 + start := time.Now() + success := 0 + for i := 0 ; i < numPackets ; i++ { + msg := make([]byte, 2048) + n, _, err := sock.ReadFromUDP(msg) + if err != nil { panic(err) } + size += n + select { + case ch <- msg: success += 1 + default: + } + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + fmt.Printf("%f bits per second\n", 8*float64(size)/timed.Seconds()) + fmt.Println("Success:", success, "/", numPackets) +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/socktest_udp_sendmmsg.go b/misc/tests/socktest_udp_sendmmsg.go new file mode 100644 index 0000000..1986c9d --- /dev/null +++ b/misc/tests/socktest_udp_sendmmsg.go @@ -0,0 +1,117 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +import "golang.org/x/net/ipv6" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") + if err != nil { panic(err) } + sock, err := net.ListenUDP("udp", udpAddr) + if err != nil { panic(err) } + defer sock.Close() + + writer := func () { + raddr := sock.LocalAddr().(*net.UDPAddr) + send, err := net.ListenUDP("udp", nil) + if err != nil { panic(err) } + defer send.Close() + conn := ipv6.NewPacketConn(send) + defer conn.Close() + var msgs []ipv6.Message + for idx := 0 ; idx < 1024 ; idx++ { + msg := ipv6.Message{Addr: raddr, Buffers: [][]byte{make([]byte, 1280)}} + msgs = append(msgs, msg) + } + for { + /* + var msgs []ipv6.Message + for idx := 0 ; idx < 1024 ; idx++ { + msg := ipv6.Message{Addr: raddr, Buffers: [][]byte{make([]byte, 1280)}} + msgs = append(msgs, msg) + } + */ + conn.WriteBatch(msgs, 0) + } + + } + go writer() + //go writer() + //go writer() + //go writer() + + numPackets := 65536 + size := 0 + count := 0 + start := time.Now() + /* + conn := ipv6.NewPacketConn(sock) + defer conn.Close() + for ; count < numPackets ; count++ { + msgs := make([]ipv6.Message, 1024) + for _, msg := range msgs { + msg.Buffers = append(msg.Buffers, make([]byte, 2048)) + } + n, err := conn.ReadBatch(msgs, 0) + if err != nil { panic(err) } + fmt.Println("DEBUG: n", n) + for _, msg := range msgs[:n] { + fmt.Println("DEBUG: msg", msg) + size += msg.N + //for _, bs := range msg.Buffers { + // size += len(bs) + //} + count++ + } + } + //*/ + //* + for ; count < numPackets ; count++ { + msg := make([]byte, 2048) + n, _, err := sock.ReadFromUDP(msg) + if err != nil { panic(err) } + size += n + } + //*/ + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(count)/timed.Seconds()) + fmt.Printf("%f bits/second\n", float64(8*size)/timed.Seconds()) +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/tcptest.go b/misc/tests/tcptest.go new file mode 100644 index 0000000..6600452 --- /dev/null +++ b/misc/tests/tcptest.go @@ -0,0 +1,96 @@ +package main + +import "fmt" +import "net" +import "time" +import "flag" +import "os" +import "runtime/pprof" + +// TODO look into netmap + libpcap to bypass the kernel as much as possible? + +const buffSize = 32 + +func basic_test() { + + // TODO need a way to look up who our link-local neighbors are for each iface! + + addr, err := net.ResolveTCPAddr("tcp", "[::1]:9001") + if err != nil { panic(err) } + listener, err := net.ListenTCP("tcp", addr) + if err != nil { panic(err) } + defer listener.Close() + + go func () { + send, err := net.DialTCP("tcp", nil, addr) + if err != nil { panic(err) } + defer send.Close() + msg := make([]byte, 1280) + bss := make(net.Buffers, 0, 1024) + count := 0 + for { + time.Sleep(100*time.Millisecond) + for len(bss) < count { + bss = append(bss, msg) + } + bss.WriteTo(send) + count++ + //send.Write(msg) + } + }() + + numPackets := 1000000 + start := time.Now() + //msg := make([]byte, 1280) + sock, err := listener.AcceptTCP() + if err != nil { panic(err) } + defer sock.Close() + for { + msg := make([]byte, 1280*buffSize) + n, err := sock.Read(msg) + if err != nil { panic(err) } + msg = msg[:n] + fmt.Println("Read:", n) + for len(msg) > 1280 { + // handle message + msg = msg[1280:] + } + // handle remaining fragment of message + //fmt.Println(n) + } + timed := time.Since(start) + + fmt.Printf("%f packets per second\n", float64(numPackets)/timed.Seconds()) + + _ = func (in (chan<- int)) { + close(in) + } + +} + +var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`") +var memprofile = flag.String("memprofile", "", "write memory profile to this file") + +func main () { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + panic(fmt.Sprintf("could not create CPU profile: ", err)) + } + if err := pprof.StartCPUProfile(f); err != nil { + panic(fmt.Sprintf("could not start CPU profile: ", err)) + } + defer pprof.StopCPUProfile() + } + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + panic(fmt.Sprintf("could not create memory profile: ", err)) + } + defer func () { pprof.WriteHeapProfile(f) ; f.Close() }() + } + basic_test() + +} + diff --git a/misc/tests/tunbench-client.go b/misc/tests/tunbench-client.go new file mode 100644 index 0000000..3beb8d6 --- /dev/null +++ b/misc/tests/tunbench-client.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "log" + "net" + "os/exec" + "time" + + "github.com/songgao/water" +) + +const mtu = 65535 + +func setup_dev() *water.Interface { + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + panic(err) + } + return ifce +} + +func setup_dev1() *water.Interface { + ifce := setup_dev() + cmd := exec.Command("ip", "-f", "inet6", + "addr", "add", "fc00::2/8", + "dev", ifce.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to assign address") + } + cmd = exec.Command("ip", "link", "set", + "dev", ifce.Name(), + "mtu", fmt.Sprintf("%d", mtu), + "up") + out, err = cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to bring up interface") + } + return ifce +} + +func connect(ifce *water.Interface) { + conn, err := net.DialTimeout("tcp", "192.168.2.2:9001", time.Second) + if err != nil { panic(err) } + sock := conn.(*net.TCPConn) + // TODO go a worker to move packets to/from the tun +} + +func bench() { +} + +func main() { + ifce := setup_dev1() + connect(ifce) + bench() + fmt.Println("Done?") + return + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + panic(err) + } + + log.Printf("Interface Name: %s\n", ifce.Name()) + + packet := make([]byte, 2000) + for { + n, err := ifce.Read(packet) + if err != nil { + panic(err) + log.Fatal(err) + } + log.Printf("Packet Received: % x\n", packet[:n]) + } +} + diff --git a/misc/tests/tunbench-server.go b/misc/tests/tunbench-server.go new file mode 100644 index 0000000..382d771 --- /dev/null +++ b/misc/tests/tunbench-server.go @@ -0,0 +1,127 @@ +package main + +import ( + "fmt" + "log" + "net" + "os/exec" + + "github.com/songgao/water" +) + +const mtu = 65535 +const netnsName = "tunbenchns" + +func setup_dev() *water.Interface { + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + panic(err) + } + return ifce +} + +func setup_dev1() *water.Interface { + ifce := setup_dev() + cmd := exec.Command("ip", "-f", "inet6", + "addr", "add", "fc00::1/8", + "dev", ifce.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + fmt.Println(string(err)) + panic("Failed to assign address") + } + cmd = exec.Command("ip", "link", "set", + "dev", tun.name, + "mtu", fmt.Sprintf("%d", mtu), + "up") + out, err = cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to bring up interface") + } + return ifce +} + +func addNS(name string) { + cmd := exec.COmmand("ip", "netns", "add", name) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to setup netns") + } +} + +func delNS(name string) { + cmd := exec.COmmand("ip", "netns", "delete", name) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to setup netns") + } +} + +func doInNetNS(comm ...string) *exec.Cmd { + return exec.Command("ip", "netns", "exec", netnsName, comm...) +} + +func setup_dev2() *water.Interface { + ifce := setup_dev() + addNS(netnsName) + cmd := exec.Command("ip", "link", "set", ifce.Name(), "netns", netnsName) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to move tun to netns") + } + cmd = doInNetNS("ip", "-f", "inet6", + "addr", "add", "fc00::2/8", + "dev", ifce.Name()) + out, err = cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to assign address") + } + cmd = doInNetNS("ip", "link", "set", + "dev", tun.name, + "mtu", fmt.Sprintf("%d", mtu), + "up") + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + fmt.Println(string(err)) + panic("Failed to bring up interface") + } + return ifce +} + +func connect() { + +} + +func bench() { +} + +func main() { + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + panic(err) + } + + log.Printf("Interface Name: %s\n", ifce.Name()) + + packet := make([]byte, 2000) + for { + n, err := ifce.Read(packet) + if err != nil { + panic(err) + log.Fatal(err) + } + log.Printf("Packet Received: % x\n", packet[:n]) + } +} + diff --git a/misc/tests/tunbench.go b/misc/tests/tunbench.go new file mode 100644 index 0000000..f4b72df --- /dev/null +++ b/misc/tests/tunbench.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "log" + "net" + "os/exec" + + "github.com/songgao/water" +) + +const mtu = 65535 +const netnsName = "tunbenchns" + +func setup_dev() *water.Interface { + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + panic(err) + } + return ifce +} + +func setup_dev1() *water.Interface { + ifce := setup_dev() + cmd := exec.Command("ip", "-f", "inet6", + "addr", "add", "fc00::1/8", + "dev", ifce.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + fmt.Println(string(err)) + panic("Failed to assign address") + } + cmd = exec.Command("ip", "link", "set", + "dev", tun.name, + "mtu", fmt.Sprintf("%d", mtu), + "up") + out, err = cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to bring up interface") + } + return ifce +} + +func addNS(name string) { + cmd := exec.COmmand("ip", "netns", "add", name) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to setup netns") + } +} + +func delNS(name string) { + cmd := exec.COmmand("ip", "netns", "delete", name) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to setup netns") + } +} + +func doInNetNS(comm ...string) *exec.Cmd { + return exec.Command("ip", "netns", "exec", netnsName, comm...) +} + +func setup_dev2() *water.Interface { + ifce := setup_dev() + addNS(netnsName) + cmd := exec.Command("ip", "link", "set", ifce.Name(), "netns", netnsName) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to move tun to netns") + } + cmd = + cmd = exec.Command( + "ip", "-f", "inet6", + "addr", "add", "fc00::2/8", + "dev", ifce.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + panic("Failed to assign address") + } + cmd = exec.Command( + "ip", "link", "set", + "dev", tun.name, + "mtu", fmt.Sprintf("%d", mtu), + "up") + out, err := cmd.CombinedOutput() + if err != nil { + fmt.Println(string(out)) + fmt.Println(string(err)) + panic("Failed to bring up interface") + } + return ifce +} + +func connect() { + +} + +func bench() { +} + +func main() { + ifce, err := water.New(water.Config{ + DeviceType: water.TUN, + }) + if err != nil { + panic(err) + } + + log.Printf("Interface Name: %s\n", ifce.Name()) + + packet := make([]byte, 2000) + for { + n, err := ifce.Read(packet) + if err != nil { + panic(err) + log.Fatal(err) + } + log.Printf("Packet Received: % x\n", packet[:n]) + } +} + diff --git a/misc/tests/tuntest.go b/misc/tests/tuntest.go new file mode 100644 index 0000000..3cb06db --- /dev/null +++ b/misc/tests/tuntest.go @@ -0,0 +1,41 @@ +package main + +import ( + "log" + "net" + "sync" + + "github.com/FlexibleBroadband/tun-go" +) + +// first start server tun server. +func main() { + wg := sync.WaitGroup{} + // local tun interface read and write channel. + rCh := make(chan []byte, 1024) + // read from local tun interface channel, and write into remote udp channel. + wg.Add(1) + go func() { + wg.Done() + for { + data := <-rCh + // if data[0]&0xf0 == 0x40 + // write into udp conn. + log.Println("tun->conn:", len(data)) + log.Println("read!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + log.Println("src:", net.IP(data[8:24]), "dst:", net.IP(data[24:40])) + } + }() + + address := net.ParseIP("fc00::1") + tuntap, err := tun.OpenTun(address) + if err != nil { panic(err) } + defer tuntap.Close() + // read data from tun into rCh channel. + wg.Add(1) + go func() { + if err := tuntap.Read(rCh); err != nil { panic(err) } + wg.Done() + }() + wg.Wait() +} diff --git a/misc/tests/wire-test.go b/misc/tests/wire-test.go new file mode 100644 index 0000000..b8bb244 --- /dev/null +++ b/misc/tests/wire-test.go @@ -0,0 +1,39 @@ +package main + +import "wire" +import "fmt" + +import "time" + +func main() { + for idx := 0 ; idx < 64 ; idx++ { + num := uint64(1) << uint(idx) + encoded := make([]byte, 10) + length := wire.Encode_uint64(num, encoded) + decoded, _ := wire.Decode_uint64(encoded[:length]) + if decoded != num { panic(fmt.Sprintf("%d != %d", decoded, num)) } + } + const count = 1000000 + start := time.Now() + encoded := make([]byte, 10) + //num := ^uint64(0) // Longest possible value for full uint64 range + num := ^uint64(0) >> 1 // Largest positive int64 (real use case) + //num := uint64(0) // Shortest possible value, most will be of this length + length := wire.Encode_uint64(num, encoded) + for idx := 0 ; idx < count ; idx++ { + wire.Encode_uint64(num, encoded) + } + timed := time.Since(start) + fmt.Println("Ops:", count/timed.Seconds()) + fmt.Println("Time:", timed.Nanoseconds()/count) + + encoded = encoded[:length] + start = time.Now() + for idx := 0 ; idx < count ; idx++ { + wire.Decode_uint64(encoded) + } + timed = time.Since(start) + fmt.Println("Ops:", count/timed.Seconds()) + fmt.Println("Time:", timed.Nanoseconds()/count) +} + diff --git a/misc/yggdrasil.go.tcp b/misc/yggdrasil.go.tcp new file mode 100644 index 0000000..a2b174d --- /dev/null +++ b/misc/yggdrasil.go.tcp @@ -0,0 +1,209 @@ +package main + +import "bytes" +import "encoding/hex" +import "encoding/json" +import "flag" +import "fmt" +import "io/ioutil" +import "net" +import "os" +import "os/signal" +import "time" + +import _ "net/http/pprof" +import "net/http" +import "log" +import "runtime" + +import "golang.org/x/net/ipv6" + +import . "yggdrasil" + +/** +* This is a very crude wrapper around src/yggdrasil +* It can generate a new config (--genconf) +* It can read a config from stdin (--useconf) +* It can run with an automatic config (--autoconf) +*/ + +type nodeConfig struct { + Listen string + Peers []string + BoxPub string + BoxPriv string + SigPub string + SigPriv string + Multicast bool +} + +type node struct { + core Core + sock *ipv6.PacketConn +} + +func (n *node) init(cfg *nodeConfig, logger *log.Logger) { + boxPub, err := hex.DecodeString(cfg.BoxPub) + if err != nil { panic(err) } + boxPriv, err := hex.DecodeString(cfg.BoxPriv) + if err != nil { panic(err) } + sigPub, err := hex.DecodeString(cfg.SigPub) + if err != nil { panic(err) } + sigPriv, err := hex.DecodeString(cfg.SigPriv) + if err != nil { panic(err) } + n.core.DEBUG_init(boxPub, boxPriv, sigPub, sigPriv) + n.core.DEBUG_setLogger(logger) + logger.Println("Starting interface...") + n.core.DEBUG_setupAndStartGlobalTCPInterface(cfg.Listen) + logger.Println("Started interface") + go func () { + if len(cfg.Peers) == 0 { return } + for { + for _, p := range cfg.Peers { + n.core.DEBUG_addTCPConn(p) + time.Sleep(time.Second) + } + } + }() +} + +func generateConfig() *nodeConfig { + core := Core{} + bpub, bpriv := core.DEBUG_newBoxKeys() + spub, spriv := core.DEBUG_newSigKeys() + cfg := nodeConfig{} + cfg.Listen = "[::]:0" + cfg.BoxPub = hex.EncodeToString(bpub[:]) + cfg.BoxPriv = hex.EncodeToString(bpriv[:]) + cfg.SigPub = hex.EncodeToString(spub[:]) + cfg.SigPriv = hex.EncodeToString(spriv[:]) + cfg.Peers = []string{} + cfg.Multicast = true + return &cfg +} + +func doGenconf() string { + cfg := generateConfig() + bs, err := json.MarshalIndent(cfg, "", " ") + if err != nil { panic(err) } + return string(bs) +} + +var multicastAddr = "[ff02::114]:9001" + +func (n *node) listen() { + groupAddr, err := net.ResolveUDPAddr("udp", multicastAddr) + if err != nil { panic(err) } + bs := make([]byte, 2048) + for { + nBytes, rcm, fromAddr, err := n.sock.ReadFrom(bs) + if err != nil { panic(err) } + //if rcm == nil { continue } // wat + //fmt.Println("DEBUG:", "packet from:", fromAddr.String()) + if !rcm.Dst.IsLinkLocalMulticast() { continue } + if !rcm.Dst.Equal(groupAddr.IP) { continue } + anAddr := string(bs[:nBytes]) + addr, err := net.ResolveTCPAddr("tcp", anAddr) + if err != nil { panic(err) ; continue } // Panic for testing, remove later + from := fromAddr.(*net.UDPAddr) + //fmt.Println("DEBUG:", "heard:", addr.IP.String(), "from:", from.IP.String()) + if addr.IP.String() != from.IP.String() { continue } + addr.Zone = from.Zone + saddr := addr.String() + //if _, isIn := n.peers[saddr]; isIn { continue } + //n.peers[saddr] = struct{}{} + n.core.DEBUG_addTCPConn(saddr) + //fmt.Println("DEBUG:", "added multicast peer:", saddr) + } +} + +func (n *node) announce() { + groupAddr, err := net.ResolveUDPAddr("udp", multicastAddr) + if err != nil { panic(err) } + tcpaddr := n.core.DEBUG_getGlobalTCPAddr() + anAddr, err := net.ResolveTCPAddr("tcp", tcpaddr.String()) + if err != nil { panic(err) } + destAddr, err := net.ResolveUDPAddr("udp6", multicastAddr) + if err != nil { panic(err) } + for { + ifaces, err := net.Interfaces() + if err != nil { panic(err) } + for _, iface := range ifaces { + n.sock.JoinGroup(&iface, groupAddr) + //err := n.sock.JoinGroup(&iface, groupAddr) + //if err != nil { panic(err) } + addrs, err := iface.Addrs() + if err != nil { panic(err) } + for _, addr := range addrs { + addrIP, _, _ := net.ParseCIDR(addr.String()) + if addrIP.To4() != nil { continue } // IPv6 only + if !addrIP.IsLinkLocalUnicast() { continue } + anAddr.IP = addrIP + anAddr.Zone = iface.Name + destAddr.Zone = iface.Name + msg := []byte(anAddr.String()) + n.sock.WriteTo(msg, nil, destAddr) + break + } + time.Sleep(time.Second) + } + time.Sleep(time.Second) + } +} + +var pprof = flag.Bool("pprof", false, "Run pprof, see http://localhost:6060/debug/pprof/") +var genconf = flag.Bool("genconf", false, "print a new config to stdout") +var useconf = flag.Bool("useconf", false, "read config from stdin") +var autoconf = flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)") + +func main() { + flag.Parse() + var cfg *nodeConfig + switch { + case *autoconf: cfg = generateConfig() + case *useconf: + config, err := ioutil.ReadAll(os.Stdin) + if err != nil { panic(err) } + decoder := json.NewDecoder(bytes.NewReader(config)) + err = decoder.Decode(&cfg) + if err != nil { panic(err) } + case *genconf: fmt.Println(doGenconf()) + default: flag.PrintDefaults() + } + if cfg == nil { return } + logger := log.New(os.Stdout, "", log.Flags()) + if *pprof { + runtime.SetBlockProfileRate(1) + go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() + } + // Setup + logger.Println("Initializing...") + n := node{} + n.init(cfg, logger) + logger.Println("Starting tun...") + //n.core.DEBUG_startTun() // 1280, the smallest supported MTU + n.core.DEBUG_startTunWithMTU(65535) // Largest supported MTU + defer func() { + logger.Println("Closing...") + n.core.DEBUG_stopTun() + }() + logger.Println("Started...") + if cfg.Multicast { + addr, err := net.ResolveUDPAddr("udp", multicastAddr) + if err != nil { panic(err) } + listenString := fmt.Sprintf("[::]:%v", addr.Port) + conn, err := net.ListenPacket("udp6", listenString) + if err != nil { panic(err) } + //defer conn.Close() // Let it close on its own when the application exits + n.sock = ipv6.NewPacketConn(conn) + if err = n.sock.SetControlMessage(ipv6.FlagDst, true) ; err != nil { panic(err) } + go n.listen() + go n.announce() + } + // Catch interrupt to exit gracefully + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + logger.Println("Stopping...") +} + diff --git a/src/yggdrasil/address.go b/src/yggdrasil/address.go new file mode 100644 index 0000000..8a1273d --- /dev/null +++ b/src/yggdrasil/address.go @@ -0,0 +1,108 @@ +package yggdrasil + +type address [16]byte // IPv6 address within the network +type subnet [8]byte // It's a /64 + +var address_prefix = [...]byte{0xfd} // For node addresses + local subnets + +func (a *address) isValid() bool { + for idx := range address_prefix { + if (*a)[idx] != address_prefix[idx] { return false } + } + return (*a)[len(address_prefix)] & 0x80 == 0 +} + +func (s *subnet) isValid() bool { + for idx := range address_prefix { + if (*s)[idx] != address_prefix[idx] { return false } + } + return (*s)[len(address_prefix)] & 0x80 != 0 +} + +func address_addrForNodeID(nid *NodeID) *address { + // 128 bit address + // Begins with prefix + // Next bit is a 0 + // Next 7 bits, interpreted as a uint, are # of leading 1s in the NodeID + // Leading 1s and first leading 0 of the NodeID are truncated off + // The rest is appended to the IPv6 address (truncated to 128 bits total) + var addr address + var temp []byte + done := false + ones := byte(0) + bits := byte(0) + nBits := 0 + for idx := 0 ; idx < 8*len(nid) ; idx++ { + bit := (nid[idx/8] & (0x80 >> byte(idx % 8))) >> byte(7 - (idx % 8)) + if !done && bit != 0 { + ones++ + continue + } + if !done && bit == 0 { + done = true + continue // FIXME this assumes that ones <= 127 + } + bits = (bits << 1) | bit + nBits++ + if nBits == 8 { + nBits = 0 + temp = append(temp, bits) + } + } + copy(addr[:], address_prefix[:]) + addr[len(address_prefix)] = ones & 0x7f + copy(addr[len(address_prefix)+1:], temp) + return &addr +} + +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 + // 2) It's truncated to a subnet prefix length instead of 128 bits + addr := *address_addrForNodeID(nid) + var snet subnet + copy(snet[:], addr[:]) + snet[len(address_prefix)] |= 0x80 + return &snet +} + +func (a *address) getNodeIDandMask() (*NodeID, *NodeID) { + // Mask is a bitmask to mark the bits visible from the address + // 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) + for idx := 0 ; idx < ones ; idx++ { nid[idx/8] |= 0x80 >> byte(idx % 8) } + nidOffset := ones+1 + addrOffset := 8*len(address_prefix)+8 + for idx := addrOffset ; idx < 8*len(a) ; idx++ { + bits := a[idx/8] & (0x80 >> byte(idx % 8)) + bits <<= byte(idx % 8) + nidIdx := nidOffset + (idx - addrOffset) + bits >>= byte(nidIdx % 8) + nid[nidIdx/8] |= bits + } + maxMask := 8*(len(a) - len(address_prefix) - 1) + ones + 1 + for idx := 0 ; idx < maxMask ; idx++ { mask[idx/8] |= 0x80 >> byte(idx % 8) } + return &nid, &mask +} + +func (s *subnet) getNodeIDandMask() (*NodeID, *NodeID) { + // As witht he address version, but visible parts of the subnet prefix instead + var nid NodeID + var mask NodeID + ones := int(s[len(address_prefix)] & 0x7f) + for idx := 0 ; idx < ones ; idx++ { nid[idx/8] |= 0x80 >> byte(idx % 8) } + nidOffset := ones+1 + addrOffset := 8*len(address_prefix)+8 + for idx := addrOffset ; idx < 8*len(s) ; idx++ { + bits := s[idx/8] & (0x80 >> byte(idx % 8)) + bits <<= byte(idx % 8) + nidIdx := nidOffset + (idx - addrOffset) + bits >>= byte(nidIdx % 8) + nid[nidIdx/8] |= bits + } + maxMask := 8*(len(s) - len(address_prefix) - 1) + ones + 1 + for idx := 0 ; idx < maxMask ; idx++ { mask[idx/8] |= 0x80 >> byte(idx % 8) } + return &nid, &mask +} diff --git a/src/yggdrasil/core.go b/src/yggdrasil/core.go new file mode 100644 index 0000000..a9672df --- /dev/null +++ b/src/yggdrasil/core.go @@ -0,0 +1,64 @@ +package yggdrasil + +import "io/ioutil" +import "log" + +type Core struct { + // This is the main data structure that holds everything else for a node + // TODO? move keys out of core and into something more appropriate + // e.g. box keys live in sessions + // sig keys live in peers or sigs (or wherever signing/validating logic is) + boxPub boxPubKey + boxPriv boxPrivKey + sigPub sigPubKey + sigPriv sigPrivKey + switchTable switchTable + peers peers + sigs sigManager + sessions sessions + router router + dht dht + tun tunDevice + searches searches + tcp *tcpInterface + udp *udpInterface + log *log.Logger +} + +func (c *Core) Init() { + // Only called by the simulator, to set up nodes with random keys + bpub, bpriv := newBoxKeys() + spub, spriv := newSigKeys() + c.init(bpub, bpriv, spub, spriv) +} + +func (c *Core) init(bpub *boxPubKey, + bpriv *boxPrivKey, + spub *sigPubKey, + spriv *sigPrivKey) { + // TODO separate init and start functions + // Init sets up structs + // Start launches goroutines that depend on structs being set up + // This is pretty much required to avoid race conditions + util_initByteStore() + c.log = log.New(ioutil.Discard, "", 0) + c.boxPub, c.boxPriv = *bpub, *bpriv + c.sigPub, c.sigPriv = *spub, *spriv + c.sigs.init() + c.searches.init(c) + c.dht.init(c) + c.sessions.init(c) + c.peers.init(c) + c.router.init(c) + c.switchTable.init(c, c.sigPub) // TODO move before peers? before router? + c.tun.init(c) +} + +func (c *Core) GetNodeID() *NodeID { + return getNodeID(&c.boxPub) +} + +func (c *Core) GetTreeID() *TreeID { + return getTreeID(&c.sigPub) +} + diff --git a/src/yggdrasil/crypto.go b/src/yggdrasil/crypto.go new file mode 100644 index 0000000..5fbab65 --- /dev/null +++ b/src/yggdrasil/crypto.go @@ -0,0 +1,154 @@ +package yggdrasil + +/* + +This part of the package wraps crypto operations needed elsewhere + +In particular, it exposes key generation for ed25519 and nacl box + +It also defines NodeID and TreeID as hashes of keys, and wraps hash functions + +*/ + +import "crypto/rand" +import "crypto/sha512" +import "golang.org/x/crypto/ed25519" +import "golang.org/x/crypto/nacl/box" + +//////////////////////////////////////////////////////////////////////////////// + +// NodeID and TreeID + +const NodeIDLen = sha512.Size +const TreeIDLen = sha512.Size +const handleLen = 8 + +type NodeID [NodeIDLen]byte +type TreeID [TreeIDLen]byte +type handle [handleLen]byte + +func getNodeID(pub *boxPubKey) *NodeID { + h := sha512.Sum512(pub[:]) + return (*NodeID)(&h) +} + +func getTreeID(pub *sigPubKey) *TreeID { + h := sha512.Sum512(pub[:]) + return (*TreeID)(&h) +} + +func newHandle() *handle { + var h handle + _, err := rand.Read(h[:]) + if err != nil { panic(err) } + return &h +} + +//////////////////////////////////////////////////////////////////////////////// + +// Signatures + +const sigPubKeyLen = ed25519.PublicKeySize +const sigPrivKeyLen = ed25519.PrivateKeySize +const sigLen = ed25519.SignatureSize + +type sigPubKey [sigPubKeyLen]byte +type sigPrivKey [sigPrivKeyLen]byte +type sigBytes [sigLen]byte + +func newSigKeys() (*sigPubKey, *sigPrivKey) { + var pub sigPubKey + var priv sigPrivKey + pubSlice, privSlice, err := ed25519.GenerateKey(rand.Reader) + if err != nil { panic(err) } + copy(pub[:], pubSlice) + copy(priv[:], privSlice) + return &pub, &priv +} + +func sign(priv *sigPrivKey, msg []byte) *sigBytes { + var sig sigBytes + sigSlice := ed25519.Sign(priv[:], msg) + copy(sig[:], sigSlice) + return &sig +} + +func verify(pub *sigPubKey, msg []byte, sig *sigBytes) bool { + // Should sig be an array instead of a slice?... + // It's fixed size, but + return ed25519.Verify(pub[:], msg, sig[:]) +} + +//////////////////////////////////////////////////////////////////////////////// + +// NaCl-like crypto "box" (curve25519+xsalsa20+poly1305) + +const boxPubKeyLen = 32 +const boxPrivKeyLen = 32 +const boxSharedKeyLen = 32 +const boxNonceLen = 24 + +type boxPubKey [boxPubKeyLen]byte +type boxPrivKey [boxPrivKeyLen]byte +type boxSharedKey [boxSharedKeyLen]byte +type boxNonce [boxNonceLen]byte + +func newBoxKeys() (*boxPubKey, *boxPrivKey) { + pubBytes, privBytes, err := box.GenerateKey(rand.Reader) + if err != nil { panic(err) } + pub := (*boxPubKey)(pubBytes) + priv := (*boxPrivKey)(privBytes) + return pub, priv +} + +func getSharedKey(myPrivKey *boxPrivKey, + othersPubKey *boxPubKey) *boxSharedKey { + var shared [boxSharedKeyLen]byte + priv := (*[boxPrivKeyLen]byte)(myPrivKey) + pub := (*[boxPubKeyLen]byte)(othersPubKey) + box.Precompute(&shared, pub, priv) + return (*boxSharedKey)(&shared) +} + +func boxOpen(shared *boxSharedKey, + boxed []byte, + nonce *boxNonce) ([]byte, bool) { + out := util_getBytes() + //return append(out, boxed...), true // XXX HACK to test without encryption + s := (*[boxSharedKeyLen]byte)(shared) + n := (*[boxNonceLen]byte)(nonce) + unboxed, success := box.OpenAfterPrecomputation(out, boxed, n, s) + return unboxed, success +} + +func boxSeal(shared *boxSharedKey, unboxed []byte, nonce *boxNonce) ([]byte, *boxNonce) { + if nonce == nil { nonce = newBoxNonce() } + nonce.update() + out := util_getBytes() + //return append(out, unboxed...), nonce // XXX HACK to test without encryption + s := (*[boxSharedKeyLen]byte)(shared) + n := (*[boxNonceLen]byte)(nonce) + boxed := box.SealAfterPrecomputation(out, unboxed, n, s) + return boxed, nonce +} + +func newBoxNonce() *boxNonce { + var nonce boxNonce + _, err := rand.Read(nonce[:]) + for ; err == nil && nonce[0] == 0xff ; _, err = rand.Read(nonce[:]){ + // Make sure nonce isn't too high + // This is just to make rollover unlikely to happen + // Rollover is fine, but it may kill the session and force it to reopen + } + if err != nil { panic(err) } + return &nonce +} + +func (n *boxNonce) update() { + oldNonce := *n + n[len(n)-1] += 2 + for i := len(n)-2 ; i >= 0 ; i-- { + if n[i+1] < oldNonce[i+1] { n[i] += 1 } + } +} + diff --git a/src/yggdrasil/debug.go b/src/yggdrasil/debug.go new file mode 100644 index 0000000..46bbd98 --- /dev/null +++ b/src/yggdrasil/debug.go @@ -0,0 +1,336 @@ +package yggdrasil + +// These are functions that should not exist +// They are (or were) used during development, to work around missing features +// They're also used to configure things from the outside +// It would be better to define and export a few config functions elsewhere +// Or define some remote API and call it to send/request configuration info + +import _ "golang.org/x/net/ipv6" // TODO put this somewhere better + +import "fmt" +import "net" +import "log" + +// Core + +func (c *Core) DEBUG_getSigPub() sigPubKey { + return (sigPubKey)(c.sigPub) +} + +func (c *Core) DEBUG_getBoxPub() boxPubKey { + return (boxPubKey)(c.boxPub) +} + +func (c *Core) DEBUG_getSend() (chan<- []byte) { + return c.tun.send +} + +func (c *Core) DEBUG_getRecv() (<-chan []byte) { + return c.tun.recv +} + +// Peer + +func (c *Core) DEBUG_getPeers() *peers { + return &c.peers +} + +func (ps *peers) DEBUG_newPeer(box boxPubKey, + sig sigPubKey) *peer { + //in <-chan []byte, + //out chan<- []byte) *peer { + return ps.newPeer(&box, &sig)//, in, out) +} + +/* +func (ps *peers) DEBUG_startPeers() { + ps.mutex.RLock() + defer ps.mutex.RUnlock() + for _, p := range ps.ports { + if p == nil { continue } + go p.MainLoop() + } +} +*/ + +func (ps *peers) DEBUG_hasPeer(key sigPubKey) bool { + ports := ps.ports.Load().(map[switchPort]*peer) + for _, p := range ports { + if p == nil { continue } + if p.sig == key { return true } + } + return false +} + +func (ps *peers) DEBUG_getPorts() map[switchPort]*peer { + ports := ps.ports.Load().(map[switchPort]*peer) + newPeers := make(map[switchPort]*peer) + for port, p := range ports{ + newPeers[port] = p + } + return newPeers +} + +func (p *peer) DEBUG_getSigKey() sigPubKey { + return p.sig +} + +func (p *peer) DEEBUG_getPort() switchPort { + return p.port +} + +// Router + +func (c *Core) DEBUG_getSwitchTable() *switchTable { + return &c.switchTable +} + +func (c *Core) DEBUG_getLocator() switchLocator { + return c.switchTable.getLocator() +} + +func (l *switchLocator) DEBUG_getCoords() []byte { + return l.getCoords() +} + +func (c *Core) DEBUG_switchLookup(dest []byte, ttl uint64) (switchPort, uint64) { + return c.switchTable.lookup(dest, ttl) +} + +/* +func (t *switchTable) DEBUG_isDirty() bool { + //data := t.data.Load().(*tabledata) + t.mutex.RLock() + defer t.mutex.RUnlock() + data := t.data + return data.dirty +} +*/ + +func (t *switchTable) DEBUG_dumpTable() { + //data := t.data.Load().(*tabledata) + t.mutex.RLock() + defer t.mutex.RUnlock() + data := t.data + for _, peer := range data.peers { + //fmt.Println("DUMPTABLE:", t.treeID, peer.treeID, peer.port, + // peer.locator.Root, peer.coords, + // peer.reverse.Root, peer.reverse.Coords, peer.forward) + fmt.Println("DUMPTABLE:", t.key, peer.key, peer.locator.coords, peer.port/*, peer.forward*/) + } +} + +func (t *switchTable) DEBUG_getReversePort(port switchPort) switchPort { + // Returns Port(0) if it cannot get the reverse peer for any reason + //data := t.data.Load().(*tabledata) + t.mutex.RLock() + defer t.mutex.RUnlock() + data := t.data + if port >= switchPort(len(data.peers)) { return switchPort(0) } + pinfo := data.peers[port] + if len(pinfo.locator.coords) < 1 { return switchPort(0) } + return pinfo.locator.coords[len(pinfo.locator.coords)-1] +} + +// Wire + +func DEBUG_wire_encode_coords(coords []byte) []byte { + return wire_encode_coords(coords) +} + +// 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.infos) + } + return total +} + +// udpInterface +// FIXME udpInterface isn't exported +// So debug functions need to work differently... + +/* +func (c *Core) DEBUG_setupLoopbackUDPInterface() { + iface := udpInterface{} + iface.init(c, "[::1]:0") + c.ifaces = append(c.ifaces[:0], &iface) +} +*/ + +/* +func (c *Core) DEBUG_getLoopbackAddr() net.Addr { + iface := c.ifaces[0] + return iface.sock.LocalAddr() +} +*/ + +/* +func (c *Core) DEBUG_addLoopbackPeer(addr *net.UDPAddr, + in (chan<- []byte), + out (<-chan []byte)) { + iface := c.ifaces[0] + iface.addPeer(addr, in, out) +} +*/ + +/* +func (c *Core) DEBUG_startLoopbackUDPInterface() { + iface := c.ifaces[0] + go iface.reader() + for addr, chs := range iface.peers { + udpAddr, err := net.ResolveUDPAddr("udp6", addr) + if err != nil { panic(err) } + go iface.writer(udpAddr, chs.out) + } +} +*/ + +//////////////////////////////////////////////////////////////////////////////// + +func (c *Core) DEBUG_getAddr() *address { + return address_addrForNodeID(&c.dht.nodeID) +} + +func (c *Core) DEBUG_startTun() { + c.DEBUG_startTunWithMTU(1280) +} + +func (c *Core) DEBUG_startTunWithMTU(mtu int) { + addr := c.DEBUG_getAddr() + straddr := fmt.Sprintf("%s/%v", net.IP(addr[:]).String(), 8*len(address_prefix)) + err := c.tun.setup(straddr, mtu) + if err != nil { panic(err) } + go c.tun.read() + go c.tun.write() +} + +func (c *Core) DEBUG_stopTun() { + c.tun.close() +} + +//////////////////////////////////////////////////////////////////////////////// + +func (c *Core) DEBUG_newBoxKeys() (*boxPubKey, *boxPrivKey) { + return newBoxKeys() +} + +func (c *Core) DEBUG_newSigKeys() (*sigPubKey, *sigPrivKey) { + return newSigKeys() +} + +func (c *Core) DEBUG_getNodeID(pub *boxPubKey) *NodeID { + return getNodeID(pub) +} + +func (c *Core) DEBUG_getTreeID(pub *sigPubKey) *TreeID { + return getTreeID(pub) +} + +func (c *Core) DEBUG_addrForNodeID(nodeID *NodeID) string { + return net.IP(address_addrForNodeID(nodeID)[:]).String() +} + +func (c *Core) DEBUG_init(bpub []byte, + bpriv []byte, + spub []byte, + spriv []byte) { + var boxPub boxPubKey + var boxPriv boxPrivKey + var sigPub sigPubKey + var sigPriv sigPrivKey + copy(boxPub[:], bpub) + copy(boxPriv[:], bpriv) + copy(sigPub[:], spub) + copy(sigPriv[:], spriv) + c.init(&boxPub, &boxPriv, &sigPub, &sigPriv) +} + +//////////////////////////////////////////////////////////////////////////////// + +func (c *Core) DEBUG_setupAndStartGlobalUDPInterface(addrport string) { + iface := udpInterface{} + iface.init(c, addrport) + c.udp = &iface +} + +func (c *Core) DEBUG_getGlobalUDPAddr() net.Addr { + return c.udp.sock.LocalAddr() +} + +func (c *Core) DEBUG_sendUDPKeys(saddr string) { + addr := connAddr(saddr) + c.udp.sendKeys(addr) +} + +//////////////////////////////////////////////////////////////////////////////// + +//* +func (c *Core) DEBUG_setupAndStartGlobalTCPInterface(addrport string) { + iface := tcpInterface{} + iface.init(c, addrport) + c.tcp = &iface +} + +func (c *Core) DEBUG_getGlobalTCPAddr() *net.TCPAddr { + return c.tcp.serv.Addr().(*net.TCPAddr) +} + +func (c *Core) DEBUG_addTCPConn(saddr string) { + c.tcp.call(saddr) +} +//*/ + +/* +func (c *Core) DEBUG_startSelfPeer() { + c.Peers.mutex.RLock() + defer c.Peers.mutex.RUnlock() + p := c.Peers.ports[0] + go p.MainLoop() +} +*/ + +//////////////////////////////////////////////////////////////////////////////// + +/* +func (c *Core) DEBUG_setupAndStartGlobalKCPInterface(addrport string) { + iface := kcpInterface{} + iface.init(c, addrport) + c.kcp = &iface +} + +func (c *Core) DEBUG_getGlobalKCPAddr() net.Addr { + return c.kcp.serv.Addr() +} + +func (c *Core) DEBUG_addKCPConn(saddr string) { + c.kcp.call(saddr) +} +*/ + +//////////////////////////////////////////////////////////////////////////////// + +func (c *Core) DEBUG_setLogger(log *log.Logger) { + c.log = log +} + +//////////////////////////////////////////////////////////////////////////////// + +func DEBUG_simLinkPeers(p, q *peer) { + // Sets q.out() to point to p and starts p.linkLoop() + plinkIn := make(chan []byte, 1) + qlinkIn := make(chan []byte, 1) + p.out = func(bs []byte) { + go q.handlePacket(bs, qlinkIn) + } + q.out = func(bs []byte) { + go p.handlePacket(bs, plinkIn) + } + go p.linkLoop(plinkIn) + go q.linkLoop(qlinkIn) +} + diff --git a/src/yggdrasil/dht.go b/src/yggdrasil/dht.go new file mode 100644 index 0000000..30b5df4 --- /dev/null +++ b/src/yggdrasil/dht.go @@ -0,0 +1,383 @@ +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 + +*/ + +// TODO handle the case where we try to look ourself up +// Ends up at bucket index NodeIDLen +// That's 1 too many + +import "sort" +import "time" +//import "fmt" + +// Maximum size for buckets and lookups +// Exception for buckets if the next one is non-full +const dht_bucket_size = 2 // This should be at least 2 +const dht_lookup_size = 2 // This should be at least 1, below 2 is impractical +const dht_bucket_number = 8*NodeIDLen // This shouldn't be changed + +type dhtInfo struct { + // TODO save their nodeID so we don't need to rehash if we need it again + 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 +} + +func (info *dhtInfo) getNodeID() *NodeID { + if info.nodeID_hidden == nil { + info.nodeID_hidden = getNodeID(&info.key) + } + return info.nodeID_hidden +} + +type bucket struct { + infos []*dhtInfo +} + +type dhtReq struct { + key boxPubKey // Key of whoever asked + coords []byte // Coords of whoever asked + dest NodeID // NodeID they're asking about +} + +type dhtRes struct { + key boxPubKey // key to respond to + coords []byte // coords to respond to + dest NodeID + infos []*dhtInfo // response +} + +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 +} + +func (t *dht) init(c *Core) { + t.core = c + t.nodeID = *t.core.GetNodeID() + t.peers = make(chan *dhtInfo, 1) + t.reqs = make(map[boxPubKey]map[NodeID]time.Time) +} + +func (t *dht) handleReq(req *dhtReq) { + // Send them what they asked for + loc := t.core.switchTable.getLocator() + coords := loc.getCoords() + res := dhtRes{ + key: t.core.boxPub, + coords: coords, + dest: req.dest, + infos: t.lookup(&req.dest), + } + t.sendRes(&res, req) + // Also (possibly) add them to our DHT + info := dhtInfo{ + key: req.key, + coords: req.coords, + } + t.insertIfNew(&info) // This seems DoSable (we just trust their coords...) + //if req.dest != t.nodeID { t.ping(&info, info.getNodeID()) } // Or spam... +} + +func (t *dht) handleRes(res *dhtRes) { + reqs, isIn := t.reqs[res.key] + if !isIn { return } + _, isIn = reqs[res.dest] + if !isIn { return } + rinfo := dhtInfo{ + key: res.key, + coords: res.coords, + send: time.Now(), // Technically wrong but should be OK... FIXME or not + recv: time.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.infos { + if oldinfo.key == rinfo.key {rinfo.send = oldinfo.send } + } + // Insert into table + t.insert(&rinfo) + if res.dest == *rinfo.getNodeID() { return } // No infinite recursions + // ping the nodes we were told about + if len(res.infos) > dht_lookup_size { + // Ignore any "extra" lookup results + res.infos = res.infos[:dht_lookup_size] + } + for _, info := range res.infos { + bidx, isOK := t.getBucketIndex(info.getNodeID()) + if !isOK { continue } + b := t.getBucket(bidx) + if b.contains(info) { continue } // wait for maintenance cycle to get them + t.ping(info, info.getNodeID()) + } +} + +func (t *dht) lookup(nodeID *NodeID) []*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 true || 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.infos) + } + 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 +} + +func (t *dht) getBucket(bidx int) *bucket { + return &t.buckets_hidden[bidx] +} + +func (t *dht) nBuckets() int { + return len(t.buckets_hidden) +} + +func (t *dht) insertIfNew(info *dhtInfo) { + //fmt.Println("DEBUG: dht insertIfNew:", info.getNodeID(), info.coords) + // Insert a peer if and only if the bucket doesn't already contain it + nodeID := info.getNodeID() + bidx, isOK := t.getBucketIndex(nodeID) + if !isOK { return } + b := t.getBucket(bidx) + if !b.contains(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) + } +} + +func (t *dht) insert(info *dhtInfo) { + //fmt.Println("DEBUG: dht insert:", info.getNodeID(), info.coords) + // 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) + // First drop any existing entry from the bucket + b.drop(&info.key) + // Now add to the *end* of the bucket + b.infos = append(b.infos, info) + // Check if the next bucket is non-full and return early if it is + if bidx+1 == t.nBuckets() { return } + bnext := t.getBucket(bidx+1) + if len(bnext.infos) < dht_bucket_size { return } + // Shrink from the *front* to requied size + for len(b.infos) > dht_bucket_size { b.infos = b.infos[1:] } +} + +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 +} + +func (b *bucket) contains(ninfo *dhtInfo) bool { + // Compares if key and coords match + for _, info := range b.infos { + if info == nil { panic("Should never happen") } + if info.key == ninfo.key { + if len(info.coords) != len(ninfo.coords) { return false } + for idx := 0 ; idx < len(info.coords) ; idx++ { + if info.coords[idx] != ninfo.coords[idx] { return false } + } + return true + } + } + return false +} + +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.infos = clean(b.infos) +} + +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{ + ttl: ^uint64(0), + 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() +} + +func (t *dht) sendRes(res *dhtRes, req *dhtReq) { + // Send a reply for a dhtReq + bs := res.encode() + shared := t.core.sessions.getSharedKey(&t.core.boxPriv, &req.key) + payload, nonce := boxSeal(shared, bs, nil) + p := wire_protoTrafficPacket{ + ttl: ^uint64(0), + coords: req.coords, + toKey: req.key, + fromKey: t.core.boxPub, + nonce: *nonce, + payload: payload, + } + packet := p.encode() + t.core.router.out(packet) +} + +func (b *bucket) isEmpty() bool { + return len(b.infos) == 0 +} + +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 + for _, next := range b.infos { + if time.Since(next.send) < 6*time.Second { continue } + if toPing == nil || next.recv.Before(toPing.recv) { toPing = next } + } + return toPing +} + +func (t *dht) getTarget(bidx int) *NodeID { + targetID := t.nodeID + targetID[bidx/8] ^= 0x80 >> byte(bidx % 8) + return &targetID +} + +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) + return + } + if target == nil { target = &t.nodeID } + loc := t.core.switchTable.getLocator() + coords := loc.getCoords() + req := dhtReq{ + key: t.core.boxPub, + coords: coords, + dest: *target, + } + info.pings++ + info.send = time.Now() + t.sendReq(&req, info) +} + +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) } + } + // 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 + } + } + } + if oldest != nil { t.ping(oldest, nil) } // if the DHT isn't empty + // Refresh buckets + if t.offset > last { t.offset = 0 } + target := t.getTarget(t.offset) + for _, info := range t.lookup(target) { + t.ping(info, target) + break + } + t.offset++ +} + +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 +} + diff --git a/src/yggdrasil/peer.go b/src/yggdrasil/peer.go new file mode 100644 index 0000000..3477568 --- /dev/null +++ b/src/yggdrasil/peer.go @@ -0,0 +1,345 @@ +package yggdrasil + +// TODO cleanup, this file is kind of a mess + +// FIXME? this part may be at least sligtly vulnerable to replay attacks +// The switch message part should catch / drop old tstamps +// So the damage is limited +// But you could still mess up msgAnc / msgHops and break some things there + +import "time" +import "sync" +import "sync/atomic" +import "math" +//import "fmt" + +type peers struct { + core *Core + mutex sync.Mutex // Synchronize writes to atomic + ports atomic.Value //map[Port]*peer, use CoW semantics + //ports map[Port]*peer +} + +func (ps *peers) init(c *Core) { + ps.mutex.Lock() + defer ps.mutex.Unlock() + ps.putPorts(make(map[switchPort]*peer)) + ps.core = c +} + +func (ps *peers) getPorts() map[switchPort]*peer { + return ps.ports.Load().(map[switchPort]*peer) +} + +func (ps *peers) putPorts(ports map[switchPort]*peer) { + ps.ports.Store(ports) +} + +type peer struct { + // Rolling approximation of bandwidth, in bps, used by switch, updated by tcp + // use get/update methods only! (atomic accessors as float64) + bandwidth uint64 + // BUG: sync/atomic, 32 bit platforms need the above to be the first element + box boxPubKey + sig sigPubKey + shared boxSharedKey + //in <-chan []byte + //out chan<- []byte + //in func([]byte) + out func([]byte) + core *Core + port switchPort + msgAnc *msgAnnounce + msgHops []*msgHop + myMsg *switchMessage + mySigs []sigInfo + // This is used to limit how often we perform expensive operations + // Specifically, processing switch messages, signing, and verifying sigs + // Resets at the start of each tick + throttle uint8 +} +const peer_Throttle = 1 + +func (p *peer) getBandwidth() float64 { + bits := atomic.LoadUint64(&p.bandwidth) + return math.Float64frombits(bits) +} + +func (p *peer) updateBandwidth(bytes int, duration time.Duration) { + if p == nil { return } + for ok := false ; !ok ; { + oldBits := atomic.LoadUint64(&p.bandwidth) + oldBandwidth := math.Float64frombits(oldBits) + bandwidth := oldBandwidth * 7 / 8 + float64(bytes)/duration.Seconds() + bits := math.Float64bits(bandwidth) + ok = atomic.CompareAndSwapUint64(&p.bandwidth, oldBits, bits) + } +} + +func (ps *peers) newPeer(box *boxPubKey, + sig *sigPubKey) *peer { + //in <-chan []byte, + //out chan<- []byte) *peer { + p := peer{box: *box, + sig: *sig, + shared: *getSharedKey(&ps.core.boxPriv, box), + //in: in, + //out: out, + core: ps.core} + ps.mutex.Lock() + defer ps.mutex.Unlock() + oldPorts := ps.getPorts() + newPorts := make(map[switchPort]*peer) + for k,v := range oldPorts{ newPorts[k] = v } + for idx := switchPort(0) ; true ; idx++ { + if _, isIn := newPorts[idx]; !isIn { + p.port = switchPort(idx) + newPorts[p.port] = &p + break + } + } + ps.putPorts(newPorts) + return &p +} + +func (p *peer) linkLoop(in <-chan []byte) { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case packet, ok := <-in: + if !ok { return } + p.handleLinkTraffic(packet) + case <-ticker.C: { + p.throttle = 0 + if p.port == 0 { continue } // Don't send announces on selfInterface + // Maybe we shouldn't time out, and instead wait for a kill signal? + p.myMsg, p.mySigs = p.core.switchTable.createMessage(p.port) + p.sendSwitchAnnounce() + } + } + } +} + +func (p *peer) handlePacket(packet []byte, linkIn (chan<- []byte)) { + pType, pTypeLen := wire_decode_uint64(packet) + if pTypeLen==0 { return } + switch (pType) { + case wire_Traffic: p.handleTraffic(packet, pTypeLen) + case wire_ProtocolTraffic: p.handleTraffic(packet, pTypeLen) + case wire_LinkProtocolTraffic: { + select { + case linkIn<-packet: + default: + } + } + default: /*panic(pType) ;*/ return + } +} + +func (p *peer) handleTraffic(packet []byte, pTypeLen int) { + ttl, ttlLen := wire_decode_uint64(packet[pTypeLen:]) + ttlBegin := pTypeLen + ttlEnd := pTypeLen+ttlLen + coords, coordLen := wire_decode_coords(packet[ttlEnd:]) + coordEnd := ttlEnd+coordLen + if coordEnd == len(packet) { return } // No payload + toPort, newTTL := p.core.switchTable.lookup(coords, ttl) + if toPort == p.port { return } // FIXME? shouldn't happen, does it? would loop + to := p.core.peers.getPorts()[toPort] + if to == nil { return } + newTTLSlice := wire_encode_uint64(newTTL) + // This mutates the packet in-place if the length of the TTL changes! + shift := ttlLen - len(newTTLSlice) + copy(packet[ttlBegin+shift:], newTTLSlice) + copy(packet[shift:], packet[:pTypeLen]) + packet = packet[shift:] + to.sendPacket(packet) +} + +func (p *peer) sendPacket(packet []byte) { + // Is there ever a case where something more complicated is needed? + // What if p.out blocks? + p.out(packet) +} + +func (p *peer) sendLinkPacket(packet []byte) { + bs, nonce := boxSeal(&p.shared, packet, nil) + linkPacket := wire_linkProtoTrafficPacket{ + toKey: p.box, + fromKey: p.core.boxPub, + nonce: *nonce, + payload: bs, + } + packet = linkPacket.encode() + p.sendPacket(packet) +} + +func (p *peer) handleLinkTraffic(bs []byte) { + packet := wire_linkProtoTrafficPacket{} + // TODO throttle on returns? + if !packet.decode(bs) { return } + if packet.toKey != p.core.boxPub { return } + if packet.fromKey != p.box { return } + payload, isOK := boxOpen(&p.shared, packet.payload, &packet.nonce) + if !isOK { return } + pType, pTypeLen := wire_decode_uint64(payload) + if pTypeLen == 0 { return } + switch pType { + case wire_SwitchAnnounce: p.handleSwitchAnnounce(payload) + case wire_SwitchHopRequest: p.handleSwitchHopRequest(payload) + case wire_SwitchHop: p.handleSwitchHop(payload) + } +} + +func (p *peer) handleSwitchAnnounce(packet []byte) { + //p.core.log.Println("DEBUG: handleSwitchAnnounce") + anc := msgAnnounce{} + //err := wire_decode_struct(packet, &anc) + //if err != nil { return } + if !anc.decode(packet) { return } + //if p.msgAnc != nil && anc.Seq != p.msgAnc.Seq { p.msgHops = nil } + if p.msgAnc == nil || + anc.root != p.msgAnc.root || + anc.tstamp != p.msgAnc.tstamp || + anc.seq != p.msgAnc.seq { p.msgHops = nil } + p.msgAnc = &anc + p.processSwitchMessage() +} + +func (p *peer) requestHop(hop uint64) { + //p.core.log.Println("DEBUG requestHop") + req := msgHopReq{} + req.root = p.msgAnc.root + req.tstamp = p.msgAnc.tstamp + req.seq = p.msgAnc.seq + req.hop = hop + packet := req.encode() + p.sendLinkPacket(packet) +} + +func (p *peer) handleSwitchHopRequest(packet []byte) { + //p.core.log.Println("DEBUG: handleSwitchHopRequest") + if p.throttle > peer_Throttle { return } + if p.myMsg == nil { return } + req := msgHopReq{} + if !req.decode(packet) { return } + if req.root != p.myMsg.locator.root { return } + if req.tstamp != p.myMsg.locator.tstamp { return } + if req.seq != p.myMsg.seq { return } + if uint64(len(p.myMsg.locator.coords)) <= req.hop { return } + res := msgHop{} + res.root = p.myMsg.locator.root + res.tstamp = p.myMsg.locator.tstamp + res.seq = p.myMsg.seq + res.hop = req.hop + res.port = p.myMsg.locator.coords[res.hop] + sinfo := p.getSig(res.hop) + //p.core.log.Println("DEBUG sig:", sinfo) + res.next = sinfo.next + res.sig = sinfo.sig + packet = res.encode() + p.sendLinkPacket(packet) +} + +func (p *peer) handleSwitchHop(packet []byte) { + //p.core.log.Println("DEBUG: handleSwitchHop") + if p.throttle > peer_Throttle { return } + if p.msgAnc == nil { return } + res := msgHop{} + if !res.decode(packet) { return } + if res.root != p.msgAnc.root { return } + if res.tstamp != p.msgAnc.tstamp { return } + if res.seq != p.msgAnc.seq { return } + if res.hop != uint64(len(p.msgHops)) { return } // always process in order + loc := switchLocator{coords: make([]switchPort, 0, len(p.msgHops)+1)} + loc.root = res.root + loc.tstamp = res.tstamp + for _, hop := range p.msgHops { loc.coords = append(loc.coords, hop.port) } + loc.coords = append(loc.coords, res.port) + thisHopKey := &res.root + if res.hop != 0 { thisHopKey = &p.msgHops[res.hop-1].next } + bs := getBytesForSig(&res.next, &loc) + if p.core.sigs.check(thisHopKey, &res.sig, bs) { + p.msgHops = append(p.msgHops, &res) + p.processSwitchMessage() + } else { + p.throttle++ + } +} + +func (p *peer) processSwitchMessage() { + //p.core.log.Println("DEBUG: processSwitchMessage") + if p.throttle > peer_Throttle { return } + if p.msgAnc == nil { return } + if uint64(len(p.msgHops)) < p.msgAnc.len { + p.requestHop(uint64(len(p.msgHops))) + return + } + p.throttle++ + if p.msgAnc.len != uint64(len(p.msgHops)) { return } + msg := switchMessage{} + coords := make([]switchPort, 0, len(p.msgHops)) + sigs := make([]sigInfo, 0, len(p.msgHops)) + for idx, hop := range p.msgHops { + // Consistency checks, should be redundant (already checked these...) + if hop.root != p.msgAnc.root { return } + if hop.tstamp != p.msgAnc.tstamp { return } + if hop.seq != p.msgAnc.seq { return } + if hop.hop != uint64(idx) { return } + coords = append(coords, hop.port) + sigs = append(sigs, sigInfo{next: hop.next, sig: hop.sig}) + } + msg.from = p.sig + msg.locator.root = p.msgAnc.root + msg.locator.tstamp = p.msgAnc.tstamp + msg.locator.coords = coords + msg.seq = p.msgAnc.seq + //msg.RSeq = p.msgAnc.RSeq + //msg.Degree = p.msgAnc.Deg + p.core.switchTable.handleMessage(&msg, p.port, sigs) + if len(coords) == 0 { return } + // Reuse locator, set the coords to the peer's coords, to use in dht + msg.locator.coords = coords[:len(coords)-1] + // Pass a mesage to the dht informing it that this peer (still) exists + dinfo := dhtInfo{ + key: p.box, + coords: msg.locator.getCoords(), + } + p.core.dht.peers<-&dinfo +} + +func (p *peer) sendSwitchAnnounce() { + anc := msgAnnounce{} + anc.root = p.myMsg.locator.root + anc.tstamp = p.myMsg.locator.tstamp + anc.seq = p.myMsg.seq + anc.len = uint64(len(p.myMsg.locator.coords)) + //anc.Deg = p.myMsg.Degree + //anc.RSeq = p.myMsg.RSeq + packet := anc.encode() + p.sendLinkPacket(packet) +} + +func (p *peer) getSig(hop uint64) sigInfo { + //p.core.log.Println("DEBUG getSig:", len(p.mySigs), hop) + if hop < uint64(len(p.mySigs)) { return p.mySigs[hop] } + bs := getBytesForSig(&p.sig, &p.myMsg.locator) + sig := sigInfo{} + sig.next = p.sig + sig.sig = *sign(&p.core.sigPriv, bs) + p.mySigs = append(p.mySigs, sig) + //p.core.log.Println("DEBUG sig bs:", bs) + return sig +} + +func getBytesForSig(next *sigPubKey, loc *switchLocator) []byte { + //bs, err := wire_encode_locator(loc) + //if err != nil { panic(err) } + bs := append([]byte(nil), next[:]...) + bs = append(bs, wire_encode_locator(loc)...) + //bs := wire_encode_locator(loc) + //bs = append(next[:], bs...) + return bs +} + diff --git a/src/yggdrasil/router.go b/src/yggdrasil/router.go new file mode 100644 index 0000000..a451327 --- /dev/null +++ b/src/yggdrasil/router.go @@ -0,0 +1,220 @@ +package yggdrasil + +// This part does most of the work to handle packets to/from yourself +// It also manages crypto and dht info +// TODO? move dht stuff into another goroutine? + +// Send: +// Receive a packet from the tun +// Look up session (if none exists, trigger a search) +// Hand off to session (which encrypts, etc) +// Session will pass it back to router.out, which hands it off to the self peer +// The self peer triggers a lookup to find which peer to send to next +// And then passes it to that's peer's peer.out function +// The peer.out function sends it over the wire to the matching peer + +// Recv: +// A packet comes in off the wire, and goes to a peer.handlePacket +// The peer does a lookup, sees no better peer than the self +// Hands it to the self peer.out, which passes it to router.in +// If it's dht/seach/etc. traffic, the router passes it to that part +// If it's an encapsulated IPv6 packet, the router looks up the session for it +// The packet is passed to the session, which decrypts it, router.recvPacket +// The router then runs some sanity checks before passing it to the tun + +import "time" +//import "fmt" +//import "net" + +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) +} + +func (r *router) init(core *Core) { + r.core = core + r.addr = *address_addrForNodeID(&r.core.dht.nodeID) + in := make(chan []byte, 1) // TODO something better than this... + p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub)//, out, in) + // TODO set in/out functions on the new peer... + p.out = func(packet []byte) { in<-packet } // FIXME in theory it blocks... + r.in = in + // TODO? make caller responsible for go-ing if it needs to not block + r.out = func(packet []byte) { p.handlePacket(packet, nil) } + // TODO attach these to the tun + // Maybe that's the core's job... + // It creates tun, creates the router, creates channels, sets them? + recv := make(chan []byte, 1) + send := make(chan []byte, 1) + r.recv = recv + r.send = send + r.core.tun.recv = recv + r.core.tun.send = send + r.reset = make(chan struct{}, 1) + go r.mainLoop() +} + +func (r *router) mainLoop() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + select { + case p := <-r.in: r.handleIn(p) + case p := <-r.send: r.sendPacket(p) + case info := <-r.core.dht.peers: r.core.dht.insert(info) //r.core.dht.insertIfNew(info) + case <-r.reset: r.core.sessions.resetInits() + case <-ticker.C: { + // Any periodic maintenance stuff goes here + r.core.dht.doMaintenance() + util_getBytes() // To slowly drain things + } + } + } +} + +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() { return } + var dest address + copy(dest[:], bs[24:]) + var snet subnet + copy(snet[:], bs[24:]) + if !dest.isValid() && !snet.isValid() { return } + doSearch := func (packet []byte) { + var nodeID, mask *NodeID + if dest.isValid() { nodeID, mask = dest.getNodeIDandMask() } + if snet.isValid() { nodeID, mask = snet.getNodeIDandMask() } + sinfo, isIn := r.core.searches.searches[*nodeID] + if !isIn { sinfo = r.core.searches.createSearch(nodeID, mask) } + if packet != nil { sinfo.packet = packet } + r.core.searches.sendSearch(sinfo) + } + var sinfo *sessionInfo + var isIn bool + if dest.isValid() { sinfo, isIn = r.core.sessions.getByTheirAddr(&dest) } + if snet.isValid() { sinfo, isIn = r.core.sessions.getByTheirSubnet(&snet) } + switch { + case !isIn || !sinfo.init: + // No or unintiialized session, so we need to search first + doSearch(bs) + case time.Since(sinfo.time) > 6*time.Second: + // We haven't heard from the dest in a while; they may have changed coords + // Maybe the connection is idle, or maybe one of us changed coords + // Try searching to either ping them (a little overhead) or fix the coords + doSearch(nil) + fallthrough + //default: go func() { sinfo.send<-bs }() + default: sinfo.send<-bs + } +} + +func (r *router) recvPacket(bs []byte, theirAddr *address) { + // TODO pass their NodeID, check *that* instead + // Or store their address in the session?... + //fmt.Println("Recv packet") + if theirAddr == nil { panic("Should not happen ever") } + if len(bs) < 24 { return } + var source address + copy(source[:], bs[8:]) + var snet subnet + copy(snet[:], bs[8:]) + if !source.isValid() && !snet.isValid() { return } + //go func() { r.recv<-bs }() + r.recv<-bs +} + +func (r *router) handleIn(packet []byte) { + pType, pTypeLen := wire_decode_uint64(packet) + if pTypeLen == 0 { return } + switch pType { + case wire_Traffic: r.handleTraffic(packet) + case wire_ProtocolTraffic: r.handleProto(packet) + default: /*panic("Should not happen in testing") ;*/ return + } +} + +func (r *router) handleTraffic(packet []byte) { + defer util_putBytes(packet) + p := wire_trafficPacket{} + if !p.decode(packet) { return } + sinfo, isIn := r.core.sessions.getSessionForHandle(&p.handle) + if !isIn { return } + //go func () { sinfo.recv<-&p }() + sinfo.recv<-&p +} + +func (r *router) handleProto(packet []byte) { + // First parse the packet + p := wire_protoTrafficPacket{} + if !p.decode(packet) { return } + // Now try to open the payload + var sharedKey *boxSharedKey + //var theirPermPub *boxPubKey + if p.toKey == r.core.boxPub { + // Try to open using our permanent key + sharedKey = r.core.sessions.getSharedKey(&r.core.boxPriv, &p.fromKey) + } else { return } + bs, isOK := boxOpen(sharedKey, p.payload, &p.nonce) + if !isOK { return } + // Now do something with the bytes in bs... + // send dht messages to dht, sessionRefresh to sessions, data to tun... + // For data, should check that key and IP match... + bsType, bsTypeLen := wire_decode_uint64(bs) + if bsTypeLen == 0 { return } + //fmt.Println("RECV bytes:", bs) + switch bsType { + case wire_SessionPing: r.handlePing(bs, &p.fromKey) + case wire_SessionPong: r.handlePong(bs, &p.fromKey) + case wire_DHTLookupRequest: r.handleDHTReq(bs, &p.fromKey) + case wire_DHTLookupResponse: r.handleDHTRes(bs, &p.fromKey) + case wire_SearchRequest: r.handleSearchReq(bs) + case wire_SearchResponse: r.handleSearchRes(bs) + default: /*panic("Should not happen in testing") ;*/ return + } +} + +func (r *router) handlePing(bs []byte, fromKey *boxPubKey) { + ping := sessionPing{} + if !ping.decode(bs) { return } + ping.sendPermPub = *fromKey + r.core.sessions.handlePing(&ping) +} + +func (r *router) handlePong(bs []byte, fromKey *boxPubKey) { + r.handlePing(bs, fromKey) +} + +func (r *router) handleDHTReq(bs []byte, fromKey *boxPubKey) { + req := dhtReq{} + if !req.decode(bs) { return } + if req.key != *fromKey { return } + r.core.dht.handleReq(&req) +} + +func (r *router) handleDHTRes(bs []byte, fromKey *boxPubKey) { + res := dhtRes{} + if !res.decode(bs) { return } + if res.key != *fromKey { return } + r.core.dht.handleRes(&res) +} + +func (r *router) handleSearchReq(bs []byte) { + req := searchReq{} + if !req.decode(bs) { return } + r.core.searches.handleSearchReq(&req) +} + +func (r *router) handleSearchRes(bs []byte) { + res := searchRes{} + if !res.decode(bs) { return } + r.core.searches.handleSearchRes(&res) +} diff --git a/src/yggdrasil/search.go b/src/yggdrasil/search.go new file mode 100644 index 0000000..5d61b6a --- /dev/null +++ b/src/yggdrasil/search.go @@ -0,0 +1,168 @@ +package yggdrasil + +// This thing manages search packets + +// The basic idea is as follows: +// We may know a NodeID (with a mask) and want to connect +// We forward a searchReq packet through the dht +// The last person in the dht will respond with a searchRes +// If the responders nodeID is close enough to the requested key, it matches +// The "close enough" is handled by a bitmask, set when the request is sent +// For testing in the sim, it must match exactly +// For the real world, the mask would need to map it to the desired IPv6 +// This is also where we store the temporary keys used to send a request +// Would go in sessions, but can't open one without knowing perm key +// This is largely to avoid using an iterative DHT lookup approach +// The iterative parallel lookups from kad can skip over some DHT blackholes +// This hides bugs, which I don't want to do right now + +import "time" +//import "fmt" + +type searchInfo struct { + dest *NodeID + mask *NodeID + time time.Time + packet []byte +} + +type searches struct { + core *Core + searches map[NodeID]*searchInfo +} + +func (s *searches) init(core *Core) { + s.core = core + s.searches = make(map[NodeID]*searchInfo) +} + +func (s *searches) createSearch(dest *NodeID, mask *NodeID) *searchInfo { + now := time.Now() + for dest, sinfo := range s.searches { + if now.Sub(sinfo.time) > time.Minute { + delete(s.searches, dest) + } + } + info := searchInfo{ + dest: dest, + mask: mask, + time: now.Add(-time.Second), + } + s.searches[*dest] = &info + return &info +} + +//////////////////////////////////////////////////////////////////////////////// + +type searchReq struct { + key boxPubKey // Who I am + coords []byte // Where I am + dest NodeID // Who I'm trying to connect to +} + +type searchRes struct { + key boxPubKey // Who I am + coords []byte // Where I am + dest NodeID // Who I was asked about +} + +func (s *searches) sendSearch(info *searchInfo) { + now := time.Now() + if now.Sub(info.time) < time.Second { return } + loc := s.core.switchTable.getLocator() + coords := loc.getCoords() + req := searchReq{ + key: s.core.boxPub, + coords: coords, + dest: *info.dest, + } + info.time = time.Now() + s.handleSearchReq(&req) +} + +func (s *searches) handleSearchReq(req *searchReq) { + lookup := s.core.dht.lookup(&req.dest) + sent := false + //fmt.Println("DEBUG len:", len(lookup)) + for _, info := range lookup { + //fmt.Println("DEBUG lup:", info.getNodeID()) + if dht_firstCloserThanThird(info.getNodeID(), + &req.dest, + &s.core.dht.nodeID) { + s.forwardSearch(req, info) + sent = true + break + } + } + if !sent { s.sendSearchRes(req) } +} + +func (s *searches) forwardSearch(req *searchReq, next *dhtInfo) { + //fmt.Println("DEBUG fwd:", req.dest, next.getNodeID()) + bs := req.encode() + shared := s.core.sessions.getSharedKey(&s.core.boxPriv, &next.key) + payload, nonce := boxSeal(shared, bs, nil) + p := wire_protoTrafficPacket{ + ttl: ^uint64(0), + coords: next.coords, + toKey: next.key, + fromKey: s.core.boxPub, + nonce: *nonce, + payload: payload, + } + packet := p.encode() + s.core.router.out(packet) +} + +func (s *searches) sendSearchRes(req *searchReq) { + //fmt.Println("DEBUG res:", req.dest, s.core.dht.nodeID) + loc := s.core.switchTable.getLocator() + coords := loc.getCoords() + res := searchRes{ + key: s.core.boxPub, + coords: coords, + dest: req.dest, + } + bs := res.encode() + shared := s.core.sessions.getSharedKey(&s.core.boxPriv, &req.key) + payload, nonce := boxSeal(shared, bs, nil) + p := wire_protoTrafficPacket{ + ttl: ^uint64(0), + coords: req.coords, + toKey: req.key, + fromKey: s.core.boxPub, + nonce: *nonce, + payload: payload, + } + packet := p.encode() + s.core.router.out(packet) +} + +func (s *searches) handleSearchRes(res *searchRes) { + info, isIn := s.searches[res.dest] + if !isIn { return } + them := getNodeID(&res.key) + var destMasked NodeID + var themMasked NodeID + for idx := 0 ; idx < NodeIDLen ; idx++ { + destMasked[idx] = info.dest[idx] & info.mask[idx] + themMasked[idx] = them[idx] & info.mask[idx] + } + //fmt.Println("DEBUG search res1:", themMasked, destMasked) + //fmt.Println("DEBUG search res2:", *them, *info.dest, *info.mask) + if themMasked != destMasked { return } + // They match, so create a session and send a sessionRequest + sinfo, isIn := s.core.sessions.getByTheirPerm(&res.key) + if !isIn { + sinfo = s.core.sessions.createSession(&res.key) + _, isIn := s.core.sessions.getByTheirPerm(&res.key) + if !isIn { panic("This should never happen") } + } + // FIXME replay attacks could mess with coords? + sinfo.coords = res.coords + sinfo.packet = info.packet + s.core.sessions.ping(sinfo) + // Cleanup + delete(s.searches, res.dest) +} + diff --git a/src/yggdrasil/session.go b/src/yggdrasil/session.go new file mode 100644 index 0000000..4f78e76 --- /dev/null +++ b/src/yggdrasil/session.go @@ -0,0 +1,327 @@ +package yggdrasil + +// This is the session manager +// It's responsible for keeping track of open sessions to other nodes +// The session information consists of crypto keys and coords + +import "time" + +type sessionInfo struct { + core *Core + theirAddr address + theirSubnet subnet + theirPermPub boxPubKey + theirSesPub boxPubKey + mySesPub boxPubKey + mySesPriv boxPrivKey + sharedSesKey boxSharedKey // derived from session keys + theirHandle handle + myHandle handle + theirNonce boxNonce + myNonce boxNonce + time time.Time // Time we last received a packet + coords []byte // coords of destination + packet []byte // a buffered packet, sent immediately on ping/pong + init bool // Reset if coords change + send chan []byte + recv chan *wire_trafficPacket + nonceMask uint64 + tstamp int64 // tstamp from their last session ping, replay attack mitigation +} + +// FIXME replay attacks (include nonce or some sequence number) +type sessionPing struct { + sendPermPub boxPubKey // Sender's permanent key + handle handle // Random number to ID session + sendSesPub boxPubKey // Session key to use + coords []byte + tstamp int64 // unix time, but the only real requirement is that it increases + isPong bool +} + +// Returns true if the session was updated, false otherwise +func (s *sessionInfo) update(p *sessionPing) bool { + if !(p.tstamp > s.tstamp) { return false } + if p.sendPermPub != s.theirPermPub { return false } // Shouldn't happen + if p.sendSesPub != s.theirSesPub { + // FIXME need to protect against replay attacks + // Put a sequence number or a timestamp or something in the pings? + // Or just return false, make the session time out? + s.theirSesPub = p.sendSesPub + s.theirHandle = p.handle + s.sharedSesKey = *getSharedKey(&s.mySesPriv, &s.theirSesPub) + s.theirNonce = boxNonce{} + s.nonceMask = 0 + } + s.coords = append([]byte{}, p.coords...) + s.time = time.Now() + s.tstamp = p.tstamp + s.init = true + return true +} + +func (s *sessionInfo) timedout() bool { + return time.Since(s.time) > time.Minute +} + +type sessions struct { + core *Core + // Maps known permanent keys to their shared key, used by DHT a lot + permShared map[boxPubKey]*boxSharedKey + // Maps (secret) handle onto session info + sinfos map[handle]*sessionInfo + // Maps mySesPub onto handle + byMySes map[boxPubKey]*handle + // Maps theirPermPub onto handle + byTheirPerm map[boxPubKey]*handle + addrToPerm map[address]*boxPubKey + subnetToPerm map[subnet]*boxPubKey +} + +func (ss *sessions) init(core *Core) { + ss.core = core + ss.permShared = make(map[boxPubKey]*boxSharedKey) + ss.sinfos = make(map[handle]*sessionInfo) + ss.byMySes = make(map[boxPubKey]*handle) + ss.byTheirPerm = make(map[boxPubKey]*handle) + ss.addrToPerm = make(map[address]*boxPubKey) + ss.subnetToPerm = make(map[subnet]*boxPubKey) +} + +func (ss *sessions) getSessionForHandle(handle *handle) (*sessionInfo, bool) { + sinfo, isIn := ss.sinfos[*handle] + if isIn && sinfo.timedout() { + // We have a session, but it has timed out + return nil, false + } + return sinfo, isIn +} + +func (ss *sessions) getByMySes(key *boxPubKey) (*sessionInfo, bool) { + h, isIn := ss.byMySes[*key] + if !isIn { return nil, false } + sinfo, isIn := ss.getSessionForHandle(h) + return sinfo, isIn +} + +func (ss *sessions) getByTheirPerm(key *boxPubKey) (*sessionInfo, bool) { + h, isIn := ss.byTheirPerm[*key] + if !isIn { return nil, false } + sinfo, isIn := ss.getSessionForHandle(h) + return sinfo, isIn +} + +func (ss *sessions) getByTheirAddr(addr *address) (*sessionInfo, bool) { + p, isIn := ss.addrToPerm[*addr] + if !isIn { return nil, false } + sinfo, isIn := ss.getByTheirPerm(p) + return sinfo, isIn +} + +func (ss *sessions) getByTheirSubnet(snet *subnet) (*sessionInfo, bool) { + p, isIn := ss.subnetToPerm[*snet] + if !isIn { return nil, false } + sinfo, isIn := ss.getByTheirPerm(p) + return sinfo, isIn +} + +func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo { + sinfo := sessionInfo{} + sinfo.core = ss.core + sinfo.theirPermPub = *theirPermKey + pub, priv := newBoxKeys() + sinfo.mySesPub = *pub + sinfo.mySesPriv = *priv + sinfo.myNonce = *newBoxNonce() // TODO make sure nonceIsOK tolerates this + higher := false + for idx := range ss.core.boxPub { + if ss.core.boxPub[idx] > sinfo.theirPermPub[idx] { + higher = true + break + } else if ss.core.boxPub[idx] < sinfo.theirPermPub[idx] { + break + } + } + if higher { + // higher => odd nonce + sinfo.myNonce[len(sinfo.myNonce)-1] |= 0x01 + } else { + // lower => even nonce + sinfo.myNonce[len(sinfo.myNonce)-1] &= 0xfe + } + sinfo.myHandle = *newHandle() + sinfo.theirAddr = *address_addrForNodeID(getNodeID(&sinfo.theirPermPub)) + sinfo.theirSubnet = *address_subnetForNodeID(getNodeID(&sinfo.theirPermPub)) + sinfo.send = make(chan []byte, 1) + sinfo.recv = make(chan *wire_trafficPacket, 1) + go sinfo.doWorker() + sinfo.time = time.Now() + // 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 + ss.addrToPerm[sinfo.theirAddr] = &sinfo.theirPermPub + ss.subnetToPerm[sinfo.theirSubnet] = &sinfo.theirPermPub + return &sinfo +} + +func (sinfo *sessionInfo) close() { + delete(sinfo.core.sessions.sinfos, sinfo.myHandle) + delete(sinfo.core.sessions.byMySes, sinfo.mySesPub) + delete(sinfo.core.sessions.byTheirPerm, sinfo.theirPermPub) + delete(sinfo.core.sessions.addrToPerm, sinfo.theirAddr) + delete(sinfo.core.sessions.subnetToPerm, sinfo.theirSubnet) + close(sinfo.send) + close(sinfo.recv) +} + +func (ss *sessions) getPing(sinfo *sessionInfo) sessionPing { + loc := ss.core.switchTable.getLocator() + coords := loc.getCoords() + ref := sessionPing{ + sendPermPub: ss.core.boxPub, + handle: sinfo.myHandle, + sendSesPub: sinfo.mySesPub, + tstamp: time.Now().Unix(), + coords: coords, + } + sinfo.myNonce.update() + return ref +} + +func (ss *sessions) getSharedKey(myPriv *boxPrivKey, + theirPub *boxPubKey) *boxSharedKey { + if skey, isIn := ss.permShared[*theirPub] ; isIn { return skey } + // First do some cleanup + const maxKeys = dht_bucket_number*dht_bucket_size + for key := range ss.permShared { + // Remove a random key until the store is small enough + if len(ss.permShared) < maxKeys { break } + delete(ss.permShared, key) + } + ss.permShared[*theirPub] = getSharedKey(myPriv, theirPub) + return ss.permShared[*theirPub] +} + +func (ss *sessions) ping(sinfo *sessionInfo) { + ss.sendPingPong(sinfo, false) +} + +func (ss *sessions) sendPingPong(sinfo *sessionInfo, isPong bool) { + ping := ss.getPing(sinfo) + ping.isPong = isPong + bs := ping.encode() + shared := ss.getSharedKey(&ss.core.boxPriv, &sinfo.theirPermPub) + payload, nonce := boxSeal(shared, bs, nil) + p := wire_protoTrafficPacket{ + ttl: ^uint64(0), + coords: sinfo.coords, + toKey: sinfo.theirPermPub, + fromKey: ss.core.boxPub, + nonce: *nonce, + payload: payload, + } + packet := p.encode() + ss.core.router.out(packet) +} + +func (ss *sessions) handlePing(ping *sessionPing) { + // Get the corresponding session (or create a new session) + sinfo, isIn := ss.getByTheirPerm(&ping.sendPermPub) + if !isIn || sinfo.timedout() { + if isIn { sinfo.close() } + ss.createSession(&ping.sendPermPub) + sinfo, isIn = ss.getByTheirPerm(&ping.sendPermPub) + if !isIn { panic("This should not happen") } + } + // Update the session + if !sinfo.update(ping) { /*panic("Should not happen in testing")*/ ; return } + if !ping.isPong{ ss.sendPingPong(sinfo, true) } + if sinfo.packet != nil { + // send + var bs []byte + bs, sinfo.packet = sinfo.packet, nil + go func() { sinfo.send<-bs }() + } +} + +func (n *boxNonce) minus(m *boxNonce) int64 { + diff := int64(0) + for idx := range n { + diff *= 256 + diff += int64(n[idx]) - int64(m[idx]) + if diff > 64 { diff = 64 } + if diff < -64 { diff = -64 } + } + return diff +} + +func (sinfo *sessionInfo) nonceIsOK(theirNonce *boxNonce) bool { + // The bitmask is to allow for some non-duplicate out-of-order packets + diff := theirNonce.minus(&sinfo.theirNonce) + if diff > 0 { return true } + return ^sinfo.nonceMask & (0x01 << uint64(-diff)) != 0 +} + +func (sinfo *sessionInfo) updateNonce(theirNonce *boxNonce) { + // Shift nonce mask if needed + // Set bit + diff := theirNonce.minus(&sinfo.theirNonce) + if diff > 0 { + sinfo.nonceMask <<= uint64(diff) + sinfo.nonceMask &= 0x01 + } else { + sinfo.nonceMask &= 0x01 << uint64(-diff) + } + sinfo.theirNonce = *theirNonce +} + +func (ss *sessions) resetInits() { + for _, sinfo := range ss.sinfos { sinfo.init = false } +} + +//////////////////////////////////////////////////////////////////////////////// + +// This is for a per-session worker +// It handles calling the relatively expensive crypto operations +// It's also responsible for keeping nonces consistent + +func (sinfo *sessionInfo) doWorker() { + for { + select { + case p, ok := <-sinfo.recv: if ok { sinfo.doRecv(p) } else { return } + case bs, ok := <-sinfo.send: if ok { sinfo.doSend(bs) } else { return } + } + } +} + +func (sinfo *sessionInfo) doSend(bs []byte) { + defer util_putBytes(bs) + if !sinfo.init { return } // To prevent using empty session keys + payload, nonce := boxSeal(&sinfo.sharedSesKey, bs, &sinfo.myNonce) + defer util_putBytes(payload) + p := wire_trafficPacket{ + ttl: ^uint64(0), + coords: sinfo.coords, + handle: sinfo.theirHandle, + nonce: *nonce, + payload: payload, + } + packet := p.encode() + sinfo.core.router.out(packet) +} + +func (sinfo *sessionInfo) doRecv(p *wire_trafficPacket) { + defer util_putBytes(p.payload) + if !sinfo.nonceIsOK(&p.nonce) { return } + bs, isOK := boxOpen(&sinfo.sharedSesKey, p.payload, &p.nonce) + if !isOK { util_putBytes(bs) ; return } + sinfo.updateNonce(&p.nonce) + sinfo.time = time.Now() + sinfo.core.router.recvPacket(bs, &sinfo.theirAddr) +} + diff --git a/src/yggdrasil/signature.go b/src/yggdrasil/signature.go new file mode 100644 index 0000000..07dd8e1 --- /dev/null +++ b/src/yggdrasil/signature.go @@ -0,0 +1,58 @@ +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" +import "time" + +type sigManager struct { + mutex sync.RWMutex + checked map[sigBytes]knownSig + lastCleaned time.Time +} + +type knownSig struct { + bs []byte + time time.Time +} + +func (m *sigManager) init() { + m.checked = make(map[sigBytes]knownSig) +} + +func (m *sigManager) check(key *sigPubKey, sig *sigBytes, bs []byte) bool { + if m.isChecked(sig, bs) { return true } + verified := verify(key, bs, sig) + if verified { m.putChecked(sig, bs) } + return verified +} + +func (m *sigManager) isChecked(sig *sigBytes, bs []byte) bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + k, isIn := m.checked[*sig] + if !isIn { return false } + if 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 +} + +func (m *sigManager) putChecked(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{bs: bs, time: now} + m.checked[*newsig] = k +} + diff --git a/src/yggdrasil/switch.go b/src/yggdrasil/switch.go new file mode 100644 index 0000000..6a725f7 --- /dev/null +++ b/src/yggdrasil/switch.go @@ -0,0 +1,398 @@ +package yggdrasil + +// This part constructs a spanning tree of the network +// It routes packets based on distance on the spanning tree +// In general, this is *not* equivalent to routing on the tree +// It falls back to the tree in the worst case, but it can take shortcuts too +// This is the part that makse routing reasonably efficient on scale-free graphs + +// TODO document/comment everything in a lot more detail + +// TODO? use a pre-computed lookup table (python version had this) +// A little annoying to do with constant changes from bandwidth estimates + +import "time" +import "sync" +import "sync/atomic" + +//import "fmt" + +const switch_timeout = time.Minute + +// You should be able to provide crypto signatures for this +// 1 signature per coord, from the *sender* to that coord +// E.g. A->B->C has sigA(A->B) and sigB(A->B->C) +type switchLocator struct { + root sigPubKey + tstamp int64 + coords []switchPort +} + +func firstIsBetter(first, second *sigPubKey) bool { + // Higher TreeID is better + ftid := getTreeID(first) + stid := getTreeID(second) + for idx := 0 ; idx < len(ftid) ; idx++ { + if ftid[idx] == stid[idx] { continue } + return ftid[idx] > stid[idx] + } + // Edge case, when comparing identical IDs + return false +} + +func (l *switchLocator) clone() switchLocator { + // Used to create a deep copy for use in messages + // Copy required because we need to mutate coords before sending + // (By appending the port from us to the destination) + loc := *l + loc.coords = make([]switchPort, len(l.coords), len(l.coords)+1) + copy(loc.coords, l.coords) + return loc +} + +func (l *switchLocator) dist(dest []byte) int { + // Returns distance (on the tree) from these coords + offset := 0 + fdc := 0 + for { + if fdc >= len(l.coords) { break } + coord, length := wire_decode_uint64(dest[offset:]) + if length == 0 { break } + if l.coords[fdc] != switchPort(coord) { break } + fdc++ + offset += length + } + dist := len(l.coords[fdc:]) + for { + _, length := wire_decode_uint64(dest[offset:]) + if length == 0 { break } + dist++ + offset += length + } + return dist +} + +func (l *switchLocator) getCoords() []byte { + bs := make([]byte, 0, len(l.coords)) + for _, coord := range l.coords { + c := wire_encode_uint64(uint64(coord)) + bs = append(bs, c...) + } + return bs +} + +func (x *switchLocator) isAncestorOf(y *switchLocator) bool { + if x.root != y.root { return false } + if len(x.coords) > len(y.coords) { return false } + for idx := range x.coords { + if x.coords[idx] != y.coords[idx] { return false } + } + return true +} + +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 + coords []switchPort // Coords of this peer (taken from coords of the sent locator) + time time.Time // Time this node was last seen + firstSeen time.Time + port switchPort // Interface number of this peer + seq uint64 // Seq number we last saw this peer advertise +} + +type switchMessage struct { + from sigPubKey // key of the sender + locator switchLocator // Locator advertised for the receiver, not the sender's loc! + seq uint64 +} + +type switchPort uint64 +type tableElem struct { + locator switchLocator + firstSeen time.Time +} + +type lookupTable struct { + self switchLocator + elems map[switchPort]tableElem +} + +type switchData struct { + // All data that's mutable and used by exported Table methods + // To be read/written with atomic.Value Store/Load calls + locator switchLocator + seq uint64 // Sequence number, reported to peers, so they know about changes + peers map[switchPort]peerInfo + sigs []sigInfo +} + +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 +} + +func (t *switchTable) init(core *Core, key sigPubKey) { + now := time.Now() + t.core = core + t.key = key + locator := switchLocator{root: key, tstamp: now.Unix()} + peers := make(map[switchPort]peerInfo) + t.data = switchData{locator: locator, peers: peers} + t.updater.Store(&sync.Once{}) + t.table.Store(lookupTable{elems: make(map[switchPort]tableElem)}) + t.drop = make(map[sigPubKey]int64) + doTicker := func () { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + for { + <-ticker.C + t.Tick() + } + } + go doTicker() +} + +func (t *switchTable) getLocator() switchLocator { + t.mutex.RLock() + defer t.mutex.RUnlock() + return t.data.locator.clone() +} + +func (t *switchTable) Tick() { + // Periodic maintenance work to keep things internally consistent + t.mutex.Lock() // Write lock + defer t.mutex.Unlock() // Release lock when we're done + t.cleanRoot() + t.cleanPeers() + t.cleanDropped() +} + +func (t *switchTable) cleanRoot() { + // TODO rethink how this is done?... + // Get rid of the root if it looks like its timed out + now := time.Now() + doUpdate := false + //fmt.Println("DEBUG clean root:", now.Sub(t.time)) + if now.Sub(t.time) > switch_timeout { + //fmt.Println("root timed out", t.data.locator) + dropped := t.data.peers[t.parent] + dropped.time = t.time + t.drop[t.data.locator.root] = t.data.locator.tstamp + doUpdate = true + //t.core.log.Println("DEBUG: switch root timeout", len(t.drop)) + } + // Or, if we're better than our root, root ourself + if firstIsBetter(&t.key, &t.data.locator.root) { + //fmt.Println("root is worse than us", t.data.locator.Root) + doUpdate = true + //t.core.log.Println("DEBUG: switch root replace with self", t.data.locator.Root) + } + // Or, if we are the root, possibly update our timestamp + if t.data.locator.root == t.key && + now.Sub(t.time) > switch_timeout/2 { + //fmt.Println("root is self and old, updating", t.data.locator.Root) + doUpdate = true + } + if doUpdate { + t.parent = switchPort(0) + t.time = now + if t.data.locator.root != t.key { + t.data.seq++ + t.updater.Store(&sync.Once{}) + select { + case t.core.router.reset<-struct{}{}: + default: + } + } + t.data.locator = switchLocator{root: t.key, tstamp: now.Unix()} + t.data.sigs = nil + } +} + +func (t *switchTable) cleanPeers() { + now := time.Now() + changed := false + for idx, info := range t.data.peers { + if info.port != switchPort(0) && now.Sub(info.time) > 6*time.Second /*switch_timeout*/ { + //fmt.Println("peer timed out", t.key, info.locator) + delete(t.data.peers, idx) + changed = true + } + } + if changed { t.updater.Store(&sync.Once{}) } +} + +func (t *switchTable) cleanDropped() { + // TODO only call this after root changes, not periodically + for root, _ := range t.drop { + if !firstIsBetter(&root, &t.data.locator.root) { delete(t.drop, root) } + } +} + +func (t *switchTable) createMessage(port switchPort) (*switchMessage, []sigInfo) { + t.mutex.RLock() + defer t.mutex.RUnlock() + msg := switchMessage{from: t.key, locator: t.data.locator.clone()} + msg.locator.coords = append(msg.locator.coords, port) + msg.seq = t.data.seq + return &msg, t.data.sigs +} + +func (t *switchTable) handleMessage(msg *switchMessage, fromPort switchPort, sigs []sigInfo) { + t.mutex.Lock() + defer t.mutex.Unlock() + now := time.Now() + if len(msg.locator.coords) == 0 { return } // Should always have >=1 links + oldSender, isIn := t.data.peers[fromPort] + if !isIn { oldSender.firstSeen = now } + sender := peerInfo{key: msg.from, + locator: msg.locator, + coords: msg.locator.coords[:len(msg.locator.coords)-1], + time: now, + firstSeen: oldSender.firstSeen, + port: fromPort, + seq: msg.seq} + equiv := func (x *switchLocator, y *switchLocator) bool { + if x.root != y.root { return false } + if len(x.coords) != len(y.coords) { return false } + for idx := range x.coords { + if x.coords[idx] != y.coords[idx] { return false } + } + return true + } + doUpdate := false + if !equiv(&msg.locator, &oldSender.locator) { + doUpdate = true + sender.firstSeen = now + } + t.data.peers[fromPort] = sender + updateRoot := false + oldParent, isIn := t.data.peers[t.parent] + noParent := !isIn + noLoop := func () bool { + for idx := 0 ; idx < len(sigs)-1 ; idx++ { + if sigs[idx].next == t.core.sigPub { return false } + } + if msg.locator.root == t.core.sigPub { return false } + 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(msg.locator.coords)*int(pTime.Seconds()) + pCost := len(t.data.locator.coords)*int(sTime.Seconds()) + dropTstamp, isIn := t.drop[msg.locator.root] + // Here be dragons + switch { + case !noLoop: // do nothing + case isIn && dropTstamp >= msg.locator.tstamp: // do nothing + case firstIsBetter(&msg.locator.root, &t.data.locator.root): updateRoot = true + case t.data.locator.root != msg.locator.root: // do nothing + case t.data.locator.tstamp > msg.locator.tstamp: // do nothing + case noParent: updateRoot = true + case cost < pCost: updateRoot = true + case sender.port == t.parent && + (msg.locator.tstamp > t.data.locator.tstamp || + !equiv(&msg.locator, &t.data.locator)): updateRoot = true + } + if updateRoot { + if !equiv(&msg.locator, &t.data.locator) { + doUpdate = true + t.data.seq++ + select { + case t.core.router.reset<-struct{}{}: + default: + } + //t.core.log.Println("Switch update:", msg.Locator.Root, msg.Locator.Tstamp, msg.Locator.Coords) + //fmt.Println("Switch update:", msg.Locator.Root, msg.Locator.Tstamp, msg.Locator.Coords) + } + if t.data.locator.tstamp != msg.locator.tstamp { t.time = now } + t.data.locator = msg.locator + t.parent = sender.port + t.data.sigs = sigs + //t.core.log.Println("Switch update:", msg.Locator.Root, msg.Locator.Tstamp, msg.Locator.Coords) + } + if doUpdate { t.updater.Store(&sync.Once{}) } + return +} + +func (t *switchTable) updateTable() { + // WARNING this should only be called from within t.data.updater.Do() + // It relies on the sync.Once for synchronization with messages and lookups + // TODO use a pre-computed faster lookup table + // Instead of checking distance for every destination every time + // Array of structs, indexed by first coord that differs from self + // Each struct has stores the best port to forward to, and a next coord map + // Move to struct, then iterate over coord maps until you dead end + // The last port before the dead end should be the closest + t.mutex.RLock() + defer t.mutex.RUnlock() + newTable := lookupTable{ + self: t.data.locator.clone(), + elems: make(map[switchPort]tableElem), + } + for _, pinfo := range t.data.peers { + //if !pinfo.forward { continue } + loc := pinfo.locator.clone() + loc.coords = loc.coords[:len(loc.coords)-1] // Remove the them->self link + newTable.elems[pinfo.port] = tableElem { + locator: loc, + //degree: pinfo.degree, + firstSeen: pinfo.firstSeen, + //forward: pinfo.forward, + } + } + t.table.Store(newTable) +} + +func (t *switchTable) lookup(dest []byte, ttl uint64) (switchPort, uint64) { + t.updater.Load().(*sync.Once).Do(t.updateTable) + table := t.table.Load().(lookupTable) + ports := t.core.peers.getPorts() + getBandwidth := func (port switchPort) float64 { + var bandwidth float64 + if p, isIn := ports[port]; isIn { + bandwidth = p.getBandwidth() + } + return bandwidth + } + var best switchPort + myDist := table.self.dist(dest) //getDist(table.self.coords) + if !(uint64(myDist) < ttl) { return 0, 0 } + // score is in units of bandwidth / distance + bestScore := float64(-1) + for port, info := range table.elems { + if info.locator.root != table.self.root { continue } + dist := info.locator.dist(dest) //getDist(info.locator.coords) + if !(dist < myDist) { continue } + score := getBandwidth(port) + score /= float64(1+dist) + if score > bestScore { + best = port + bestScore = score + } + } + //t.core.log.Println("DEBUG: sending to", best, "bandwidth", getBandwidth(best)) + return best, uint64(myDist) +} + +//////////////////////////////////////////////////////////////////////////////// + +//Signature stuff + +type sigInfo struct { + next sigPubKey + sig sigBytes +} + +//////////////////////////////////////////////////////////////////////////////// + diff --git a/src/yggdrasil/tcp.go b/src/yggdrasil/tcp.go new file mode 100644 index 0000000..7abe583 --- /dev/null +++ b/src/yggdrasil/tcp.go @@ -0,0 +1,246 @@ +package yggdrasil + +// This sends packets to peers using TCP as a transport +// It's generally better tested than the UDP implementation +// Using it regularly is insane, but I find TCP easier to test/debug with it +// Updating and optimizing the UDP version is a higher priority + +// TODO: +// Something needs to make sure we're getting *valid* packets +// Could be used to DoS (connect, give someone else's keys, spew garbage) +// I guess the "peer" part should watch for link packets, disconnect? + +import "net" +import "time" +import "errors" +import "sync" +import "fmt" + +const tcp_msgSize = 2048+65535 // TODO figure out what makes sense + +type tcpInterface struct { + core *Core + serv *net.TCPListener + mutex sync.Mutex // Protecting the below + calls map[string]struct{} +} + +type tcpKeys struct { + box boxPubKey + sig sigPubKey +} + +func (iface *tcpInterface) init(core *Core, addr string) { + iface.core = core + tcpAddr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { panic(err) } + iface.serv, err = net.ListenTCP("tcp", tcpAddr) + if err != nil { panic(err) } + iface.calls = make(map[string]struct{}) + go iface.listener() +} + +func (iface *tcpInterface) listener() { + defer iface.serv.Close() + iface.core.log.Println("Listening on:", iface.serv.Addr().String()) + for { + sock, err := iface.serv.AcceptTCP() + if err != nil { panic(err) } + go iface.handler(sock) + } +} + +func (iface *tcpInterface) call(saddr string) { + go func() { + quit := false + iface.mutex.Lock() + if _, isIn := iface.calls[saddr]; isIn { + quit = true + } else { + iface.calls[saddr] = struct{}{} + defer func() { + iface.mutex.Lock() + delete(iface.calls, saddr) + iface.mutex.Unlock() + }() + } + iface.mutex.Unlock() + if !quit { + conn, err := net.DialTimeout("tcp", saddr, 6*time.Second) + if err != nil { return } + sock := conn.(*net.TCPConn) + iface.handler(sock) + } + }() +} + +func (iface *tcpInterface) handler(sock *net.TCPConn) { + defer sock.Close() + // Get our keys + keys := []byte{} + keys = append(keys, tcp_key[:]...) + keys = append(keys, iface.core.boxPub[:]...) + keys = append(keys, iface.core.sigPub[:]...) + _, err := sock.Write(keys) + if err != nil { return } + timeout := time.Now().Add(6*time.Second) + sock.SetReadDeadline(timeout) + n, err := sock.Read(keys) + if err != nil { return } + if n < len(keys) { /*panic("Partial key packet?") ;*/ return } + ks := tcpKeys{} + if !tcp_chop_keys(&ks.box, &ks.sig, &keys) { /*panic("Invalid key packet?") ;*/ return } + // Quit the parent call if this is a connection to ourself + equiv := func(k1, k2 []byte) bool { + for idx := range k1 { + if k1[idx] != k2[idx] { return false } + } + return true + } + if equiv(ks.box[:], iface.core.boxPub[:]) { return } // testing + if equiv(ks.sig[:], iface.core.sigPub[:]) { return } + // Note that multiple connections to the same node are allowed + // E.g. over different interfaces + linkIn := make(chan []byte, 1) + p := iface.core.peers.newPeer(&ks.box, &ks.sig)//, in, out) + in := func(bs []byte) { + p.handlePacket(bs, linkIn) + } + out := make(chan []byte, 1024) // TODO? what size makes sense + defer close(out) + go func() { + var stack [][]byte + put := func(msg []byte) { + stack = append(stack, msg) + for len(stack) > 1024 { + util_putBytes(stack[0]) + stack = stack[1:] + } + } + send := func() { + msg := stack[len(stack)-1] + stack = stack[:len(stack)-1] + buf := net.Buffers{tcp_msg[:], + wire_encode_uint64(uint64(len(msg))), + msg} + size := 0 + for _, bs := range buf { size += len(bs) } + start := time.Now() + buf.WriteTo(sock) + timed := time.Since(start) + pType, _ := wire_decode_uint64(msg) + if pType == wire_LinkProtocolTraffic { + p.updateBandwidth(size, timed) + } + util_putBytes(msg) + } + for msg := range out { + put(msg) + for len(stack) > 0 { + // Keep trying to fill the stack (LIFO order) while sending + select { + case msg, ok := <-out: + if !ok { return } + put(msg) + default: send() + } + } + } + }() + p.out = func(msg []byte) { + defer func() { recover() }() + for { + select { + case out<-msg: return + default: util_putBytes(<-out) + } + } + } + sock.SetNoDelay(true) + go p.linkLoop(linkIn) + defer func() { + // Put all of our cleanup here... + p.core.peers.mutex.Lock() + oldPorts := p.core.peers.getPorts() + newPorts := make(map[switchPort]*peer) + for k,v := range oldPorts{ newPorts[k] = v } + delete(newPorts, p.port) + p.core.peers.putPorts(newPorts) + p.core.peers.mutex.Unlock() + close(linkIn) + }() + them := sock.RemoteAddr() + themNodeID := getNodeID(&ks.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) + return +} + +func (iface *tcpInterface) reader(sock *net.TCPConn, in func([]byte)) { + bs := make([]byte, 2*tcp_msgSize) + frag := bs[:0] + for { + timeout := time.Now().Add(6*time.Second) + sock.SetReadDeadline(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 !ok { break } // We didn't get the whole message yet + newMsg := append(util_getBytes(), msg...) + in(newMsg) + util_yield() + } + frag = append(bs[:0], frag...) + } +} + +//////////////////////////////////////////////////////////////////////////////// + +// Magic bytes to check +var tcp_key = [...]byte{'k', 'e', 'y', 's'} +var tcp_msg = [...]byte{0xde, 0xad, 0xb1, 0x75} // "dead bits" + +func tcp_chop_keys(box *boxPubKey, sig *sigPubKey, bs *[]byte) bool { + // This one is pretty simple: we know how long the message should be + // So don't call this with a message that's too short + if len(*bs) < len(tcp_key) + len(*box) + len(*sig) { return false } + for idx := range tcp_key { + if (*bs)[idx] != tcp_key[idx] { return false } + } + (*bs) = (*bs)[len(tcp_key):] + copy(box[:], *bs) + (*bs) = (*bs)[len(box):] + copy(sig[:], *bs) + (*bs) = (*bs)[len(sig):] + return true +} + +func tcp_chop_msg(bs *[]byte) ([]byte, bool, error) { + // Returns msg, ok, err + if len(*bs) < len(tcp_msg) { return nil, false, nil } + for idx := range tcp_msg { + if (*bs)[idx] != tcp_msg[idx] { + return nil, false, errors.New("Bad message!") + } + } + msgLen, msgLenLen := wire_decode_uint64((*bs)[len(tcp_msg):]) + if msgLen > tcp_msgSize { return nil, false, errors.New("Oversized message!") } + msgBegin := len(tcp_msg) + msgLenLen + msgEnd := msgBegin + int(msgLen) + if msgLenLen == 0 || len(*bs) < msgEnd { + // We don't have the full message + // Need to buffer this and wait for the rest to come in + return nil, false, nil + } + msg := (*bs)[msgBegin:msgEnd] + (*bs) = (*bs)[msgEnd:] + return msg, true, nil +} + diff --git a/src/yggdrasil/tun.go b/src/yggdrasil/tun.go new file mode 100644 index 0000000..b87528a --- /dev/null +++ b/src/yggdrasil/tun.go @@ -0,0 +1,56 @@ +package yggdrasil + +// This manages the tun driver to send/recv packets to/from applications + +import water "github.com/songgao/water" + +const IPv6_HEADER_LENGTH = 40 + +type tunDevice struct { + core *Core + send chan<- []byte + recv <-chan []byte + mtu int + iface *water.Interface +} + +func (tun *tunDevice) init(core *Core) { + tun.core = core +} + +func (tun *tunDevice) setup(addr string, mtu int) error { + iface, err := water.New(water.Config{ DeviceType: water.TUN }) + if err != nil { panic(err) } + tun.iface = iface + tun.mtu = mtu //1280 // Lets default to the smallest thing allowed for now + return tun.setupAddress(addr) +} + +func (tun *tunDevice) write() error { + for { + data := <-tun.recv + if _, err := tun.iface.Write(data); err != nil { return err } + util_putBytes(data) + } +} + +func (tun *tunDevice) read() error { + buf := make([]byte, tun.mtu) + for { + n, err := tun.iface.Read(buf) + if err != nil { return err } + if buf[0] & 0xf0 != 0x60 || + n != 256*int(buf[4]) + int(buf[5]) + IPv6_HEADER_LENGTH { + // Either not an IPv6 packet or not the complete packet for some reason + panic("Should not happen in testing") + continue + } + packet := append(util_getBytes(), buf[:n]...) + tun.send<-packet + } +} + +func (tun *tunDevice) close() error { + return tun.iface.Close() +} + diff --git a/src/yggdrasil/tun_linux.go b/src/yggdrasil/tun_linux.go new file mode 100644 index 0000000..7dc03ac --- /dev/null +++ b/src/yggdrasil/tun_linux.go @@ -0,0 +1,36 @@ +package yggdrasil + +// The linux platform specific tun parts +// It depends on iproute2 being installed to set things on the tun device + +import "fmt" +import "os/exec" +import "strings" + +func (tun *tunDevice) setupAddress(addr string) error { + // Set address + cmd := exec.Command("ip", "-f", "inet6", + "addr", "add", addr, + "dev", tun.iface.Name()) + tun.core.log.Printf("ip command: %v", strings.Join(cmd.Args, " ")) + output, err := cmd.CombinedOutput() + if err != nil { + tun.core.log.Printf("Linux ip failed: %v.", err) + tun.core.log.Println(string(output)) + return err + } + // Set MTU and bring device up + cmd = exec.Command("ip", "link", "set", + "dev", tun.iface.Name(), + "mtu", fmt.Sprintf("%d", tun.mtu), + "up") + tun.core.log.Printf("ip command: %v", strings.Join(cmd.Args, " ")) + output, err = cmd.CombinedOutput() + if err != nil { + tun.core.log.Printf("Linux ip failed: %v.", err) + tun.core.log.Println(string(output)) + return err + } + return nil +} + diff --git a/src/yggdrasil/tun_other.go b/src/yggdrasil/tun_other.go new file mode 100644 index 0000000..06a8f63 --- /dev/null +++ b/src/yggdrasil/tun_other.go @@ -0,0 +1,12 @@ +// +build !linux + +package yggdrasil + +// This is to catch unsupported platforms +// If your platform supports tun devices, you could try configuring it manually + +func (tun *tunDevice) setupAddress(addr string) error { + tun.core.log.Println("Platform not supported, you must set the address of", tun.iface.Name(), "to", addr) + return nil +} + diff --git a/src/yggdrasil/udp.go b/src/yggdrasil/udp.go new file mode 100644 index 0000000..4c96be6 --- /dev/null +++ b/src/yggdrasil/udp.go @@ -0,0 +1,275 @@ +package yggdrasil + +// This communicates with peers via UDP +// It's not as well tested or debugged as the TCP transport +// It's intended to use UDP, so debugging/optimzing this is a high priority +// TODO? use golang.org/x/net/ipv6.PacketConn's ReadBatch and WriteBatch? +// To send all chunks of a message / recv all available chunks in one syscall +// Chunks are currently murged, but outgoing messages aren't chunked +// This is just to support chunking in the future, if it's needed and debugged +// Basically, right now we might send UDP packets that are too large + +import "net" +import "time" +import "sync" +import "fmt" + +type udpInterface struct { + core *Core + sock *net.UDPConn // Or more general PacketConn? + mutex sync.RWMutex // each conn has an owner goroutine + conns map[connAddr]*connInfo +} + +type connAddr string // TODO something more efficient, but still a valid map key +type connInfo struct { + addr connAddr + peer *peer + linkIn chan []byte + keysIn chan *udpKeys + timeout int // count of how many heartbeats have been missed + in func([]byte) + out chan []byte + countIn uint8 + countOut uint8 +} + +type udpKeys struct { + box boxPubKey + sig sigPubKey +} + +func (iface *udpInterface) init(core *Core, addr string) { + iface.core = core + udpAddr, err := net.ResolveUDPAddr("udp", addr) + if err != nil { panic(err) } + iface.sock, err = net.ListenUDP("udp", udpAddr) + if err != nil { panic(err) } + iface.conns = make(map[connAddr]*connInfo) + go iface.reader() +} + +func (iface *udpInterface) sendKeys(addr connAddr) { + udpAddr, err := net.ResolveUDPAddr("udp", string(addr)) + if err != nil { panic(err) } + msg := []byte{} + msg = udp_encode(msg, 0, 0, 0, nil) + msg = append(msg, iface.core.boxPub[:]...) + msg = append(msg, iface.core.sigPub[:]...) + iface.sock.WriteToUDP(msg, udpAddr) +} + +func udp_isKeys(msg []byte) bool { + keyLen := 3 + boxPubKeyLen + sigPubKeyLen + return len(msg) == keyLen && msg[0] == 0x00 +} + +func (iface *udpInterface) startConn(info *connInfo) { + ticker := time.NewTicker(6*time.Second) + defer ticker.Stop() + defer func () { + // Cleanup + // FIXME this still leaks a peer struct + iface.mutex.Lock() + delete(iface.conns, info.addr) + iface.mutex.Unlock() + iface.core.peers.mutex.Lock() + oldPorts := iface.core.peers.getPorts() + newPorts := make(map[switchPort]*peer) + for k,v := range oldPorts{ newPorts[k] = v } + delete(newPorts, info.peer.port) + iface.core.peers.putPorts(newPorts) + iface.core.peers.mutex.Unlock() + close(info.linkIn) + close(info.keysIn) + close(info.out) + iface.core.log.Println("Removing peer:", info.addr) + }() + for { + select { + case ks := <-info.keysIn: { + // FIXME? need signatures/sequence-numbers or something + // Spoofers could lock out a peer with fake/bad keys + if ks.box == info.peer.box && ks.sig == info.peer.sig { + info.timeout = 0 + } + } + case <-ticker.C: { + if info.timeout > 10 { return } + info.timeout++ + iface.sendKeys(info.addr) + } + } + } +} + +func (iface *udpInterface) handleKeys(msg []byte, addr connAddr) { + //defer util_putBytes(msg) + var ks udpKeys + _, _, _, bs := udp_decode(msg) + switch { + case !wire_chop_slice(ks.box[:], &bs): return + case !wire_chop_slice(ks.sig[:], &bs): return + } + if ks.box == iface.core.boxPub { return } + if ks.sig == iface.core.sigPub { return } + iface.mutex.RLock() + conn, isIn := iface.conns[addr] + iface.mutex.RUnlock() // TODO? keep the lock longer?... + if !isIn { + udpAddr, err := net.ResolveUDPAddr("udp", string(addr)) + if err != nil { panic(err) } + conn = &connInfo{ + addr: connAddr(addr), + peer: iface.core.peers.newPeer(&ks.box, &ks.sig), + linkIn: make(chan []byte, 1), + keysIn: make(chan *udpKeys, 1), + out: make(chan []byte, 1024), + } + /* + conn.in = func (msg []byte) { conn.peer.handlePacket(msg, conn.linkIn) } + conn.peer.out = func (msg []byte) { + start := time.Now() + iface.sock.WriteToUDP(msg, udpAddr) + timed := time.Since(start) + conn.peer.updateBandwidth(len(msg), timed) + util_putBytes(msg) + } // Old version, always one syscall per packet + //*/ + /* + conn.peer.out = func (msg []byte) { + defer func() { recover() }() + select { + case conn.out<-msg: + default: util_putBytes(msg) + } + } + go func () { + for msg := range conn.out { + start := time.Now() + iface.sock.WriteToUDP(msg, udpAddr) + timed := time.Since(start) + conn.peer.updateBandwidth(len(msg), timed) + util_putBytes(msg) + } + }() + //*/ + //* + var inChunks uint8 + var inBuf []byte + conn.in = func(bs []byte) { + //defer util_putBytes(bs) + chunks, chunk, count, payload := udp_decode(bs) + //iface.core.log.Println("DEBUG:", addr, chunks, chunk, count, len(payload)) + //iface.core.log.Println("DEBUG: payload:", payload) + if count != conn.countIn { + inChunks = 0 + inBuf = inBuf[:0] + conn.countIn = count + } + if chunk <= chunks && chunk == inChunks + 1 { + //iface.core.log.Println("GOING:", addr, chunks, chunk, count, len(payload)) + inChunks += 1 + inBuf = append(inBuf, payload...) + if chunks != chunk { return } + msg := append(util_getBytes(), inBuf...) + conn.peer.handlePacket(msg, conn.linkIn) + //iface.core.log.Println("DONE:", addr, chunks, chunk, count, len(payload)) + } + } + conn.peer.out = func (msg []byte) { + defer func() { recover() }() + select { + case conn.out<-msg: + default: util_putBytes(msg) + } + } + go func () { + //var chunks [][]byte + var out []byte + for msg := range conn.out { + var chunks [][]byte + bs := msg + for len(bs) > udp_chunkSize { + chunks, bs = append(chunks, bs[:udp_chunkSize]), bs[udp_chunkSize:] + } + chunks = append(chunks, bs) + //iface.core.log.Println("DEBUG: out chunks:", len(chunks), len(msg)) + if len(chunks) > 255 { continue } + start := time.Now() + for idx,bs := range chunks { + nChunks, nChunk, count := uint8(len(chunks)), uint8(idx)+1, conn.countOut + out = udp_encode(out[:0], nChunks, nChunk, count, bs) + //iface.core.log.Println("DEBUG out:", nChunks, nChunk, count, len(bs)) + iface.sock.WriteToUDP(out, udpAddr) + } + timed := time.Since(start) + conn.countOut += 1 + conn.peer.updateBandwidth(len(msg), timed) + util_putBytes(msg) + } + }() + //*/ + iface.mutex.Lock() + iface.conns[addr] = conn + iface.mutex.Unlock() + themNodeID := getNodeID(&ks.box) + themAddr := address_addrForNodeID(themNodeID) + themAddrString := net.IP(themAddr[:]).String() + themString := fmt.Sprintf("%s@%s", themAddrString, addr) + iface.core.log.Println("Adding peer:", themString) + go iface.startConn(conn) + go conn.peer.linkLoop(conn.linkIn) + iface.sendKeys(conn.addr) + } + func() { + defer func() { recover() }() + select { + case conn.keysIn<-&ks: + default: + } + }() +} + +func (iface *udpInterface) handlePacket(msg []byte, addr connAddr) { + iface.mutex.RLock() + if conn, isIn := iface.conns[addr]; isIn { + conn.in(msg) + } + iface.mutex.RUnlock() +} + +func (iface *udpInterface) reader() { + bs := make([]byte, 2048) // This needs to be large enough for everything... + for { + //iface.core.log.Println("Starting read") + n, udpAddr, err := iface.sock.ReadFromUDP(bs) + //iface.core.log.Println("Read", n, udpAddr.String(), err) + if err != nil { panic(err) ; break } + if n > 1500 { panic(n) } + //msg := append(util_getBytes(), bs[:n]...) + msg := bs[:n] + addr := connAddr(udpAddr.String()) + if udp_isKeys(msg) { + iface.handleKeys(msg, addr) + } else { + iface.handlePacket(msg, addr) + } + } +} + +//////////////////////////////////////////////////////////////////////////////// + +const udp_chunkSize = 65535 + +func udp_decode(bs []byte) (chunks, chunk, count uint8, payload []byte) { + if len(bs) >= 3 { + chunks, chunk, count, payload = bs[0], bs[1], bs[2], bs[3:] + } + return +} + +func udp_encode(out []byte, chunks, chunk, count uint8, payload []byte) []byte { + return append(append(out, chunks, chunk, count), payload...) +} + diff --git a/src/yggdrasil/util.go b/src/yggdrasil/util.go new file mode 100644 index 0000000..4eb403b --- /dev/null +++ b/src/yggdrasil/util.go @@ -0,0 +1,79 @@ +package yggdrasil + +// These are misc. utility functions that didn't really fit anywhere else + +import "fmt" +import "runtime" +//import "sync" + +func Util_testAddrIDMask() { + for idx := 0 ; idx < 16 ; idx++ { + var orig NodeID + orig[8] = 42 + for bidx := 0 ; bidx < idx ; bidx++ { + orig[bidx/8] |= (0x80 >> uint8(bidx % 8)) + } + addr := address_addrForNodeID(&orig) + nid, mask := addr.getNodeIDandMask() + for b := 0 ; b < len(mask) ; b++ { + nid[b] &= mask[b] + orig[b] &= mask[b] + } + if *nid != orig { + fmt.Println(orig) + fmt.Println(*addr) + fmt.Println(*nid) + fmt.Println(*mask) + panic(idx) + } + } +} + +func util_yield() { + runtime.Gosched() +} + +func util_lockthread() { + runtime.LockOSThread() +} + +func util_unlockthread() { + runtime.UnlockOSThread() +} + +/* +var byteStore sync.Pool = sync.Pool{ + New: func () interface{} { return []byte(nil) }, +} + +func util_getBytes() []byte { + return byteStore.Get().([]byte)[:0] +} + +func util_putBytes(bs []byte) { + byteStore.Put(bs) // FIXME? The cast to interface{} allocates... +} +*/ + +var byteStore chan []byte + +func util_initByteStore() { + if byteStore == nil { + byteStore = make(chan []byte, 32) + } +} + +func util_getBytes() []byte { + select { + case bs := <-byteStore: return bs[:0] + default: return nil + } +} + +func util_putBytes(bs []byte) { + select { + case byteStore<-bs: + default: + } +} + diff --git a/src/yggdrasil/wire.go b/src/yggdrasil/wire.go new file mode 100644 index 0000000..5ca106d --- /dev/null +++ b/src/yggdrasil/wire.go @@ -0,0 +1,492 @@ +package yggdrasil + +// Wire formatting tools +// These are all ugly and probably not very secure + +// Packet types, as an Encode_uint64 at the start of each packet +// TODO? make things still work after reordering (after things stabilize more?) +// Type safety would also be nice, `type wire_type uint64`, rewrite as needed? +const ( + wire_Traffic = iota // data being routed somewhere, handle for crypto + wire_ProtocolTraffic // protocol traffic, pub keys for crypto + wire_LinkProtocolTraffic // link proto traffic, pub keys for crypto + wire_SwitchAnnounce // TODO put inside protocol traffic header + wire_SwitchHopRequest // TODO put inside protocol traffic header + wire_SwitchHop // TODO put inside protocol traffic header + wire_SessionPing // inside protocol traffic header + wire_SessionPong // inside protocol traffic header + wire_DHTLookupRequest // inside protocol traffic header + wire_DHTLookupResponse // inside protocol traffic header + wire_SearchRequest // inside protocol traffic header + wire_SearchResponse // inside protocol traffic header + //wire_Keys // udp key packet (boxPub, sigPub) +) + +// Encode uint64 using a variable length scheme +// Similar to binary.Uvarint, but big-endian +func wire_encode_uint64(elem uint64) []byte { + return wire_put_uint64(elem, nil) +} + +// Occasionally useful for appending to an existing slice (if there's room) +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)) + } + // 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...) +} + +// Decode uint64 from a []byte slice +// Returns the decoded uint64 and the number of bytes used +func wire_decode_uint64(bs []byte) (uint64, int) { + length := 0 + elem := uint64(0) + for _, b := range bs { + elem <<= 7 + elem |= uint64(b & 0x7f) + length++ + if b & 0x80 == 0 { break } + } + return elem, length +} + +func wire_intToUint(i int64) uint64 { + var u uint64 + if i < 0 { + u = uint64(-i) << 1 + u |= 0x01 // sign bit + } else { + u = uint64(i) << 1 + } + return u +} + +func wire_intFromUint(u uint64) int64 { + var i int64 + i = int64(u >> 1) + if u & 0x01 != 0 { i *= -1 } + return i +} + +//////////////////////////////////////////////////////////////////////////////// + +// Takes coords, returns coords prefixed with encoded coord length +func wire_encode_coords(coords []byte) ([]byte) { + coordLen := wire_encode_uint64(uint64(len(coords))) + bs := make([]byte, 0, len(coordLen)+len(coords)) + bs = append(bs, coordLen...) + bs = append(bs, coords...) + return bs +} + +func wire_put_coords(coords []byte, bs []byte) ([]byte) { + bs = wire_put_uint64(uint64(len(coords)), bs) + bs = append(bs, coords...) + return bs +} + +// Takes a packet that begins with coords (starting with coord length) +// Returns a slice of coords and the number of bytes read +func wire_decode_coords(packet []byte) ([]byte, int) { + coordLen, coordBegin := wire_decode_uint64(packet) + coordEnd := coordBegin+int(coordLen) + //if coordBegin == 0 { panic("No coords found") } // Testing + //if coordEnd > len(packet) { panic("Packet too short") } // Testing + if coordBegin == 0 || coordEnd > len(packet) { return nil, 0 } + return packet[coordBegin:coordEnd], coordEnd +} + +//////////////////////////////////////////////////////////////////////////////// +// TODO move this msg stuff somewhere else, use encode() and decode() methods + +// Announces that we can send parts of a Message with a particular seq +type msgAnnounce struct { + root sigPubKey + tstamp int64 + seq uint64 + len uint64 + //Deg uint64 + //RSeq uint64 +} + +func (m *msgAnnounce) encode() []byte { + bs := wire_encode_uint64(wire_SwitchAnnounce) + bs = append(bs, m.root[:]...) + bs = append(bs, wire_encode_uint64(wire_intToUint(m.tstamp))...) + bs = append(bs, wire_encode_uint64(m.seq)...) + bs = append(bs, wire_encode_uint64(m.len)...) + //bs = append(bs, wire_encode_uint64(m.Deg)...) + //bs = append(bs, wire_encode_uint64(m.RSeq)...) + return bs +} + +func (m *msgAnnounce) decode(bs []byte) bool { + var pType uint64 + var tstamp uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_SwitchAnnounce: return false + case !wire_chop_slice(m.root[:], &bs): return false + case !wire_chop_uint64(&tstamp, &bs): return false + case !wire_chop_uint64(&m.seq, &bs): return false + case !wire_chop_uint64(&m.len, &bs): return false + //case !wire_chop_uint64(&m.Deg, &bs): return false + //case !wire_chop_uint64(&m.RSeq, &bs): return false + } + m.tstamp = wire_intFromUint(tstamp) + return true +} + +type msgHopReq struct { + root sigPubKey + tstamp int64 + seq uint64 + hop uint64 +} + +func (m *msgHopReq) encode() []byte { + bs := wire_encode_uint64(wire_SwitchHopRequest) + bs = append(bs, m.root[:]...) + bs = append(bs, wire_encode_uint64(wire_intToUint(m.tstamp))...) + bs = append(bs, wire_encode_uint64(m.seq)...) + bs = append(bs, wire_encode_uint64(m.hop)...) + return bs +} + +func (m *msgHopReq) decode(bs []byte) bool { + var pType uint64 + var tstamp uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_SwitchHopRequest: return false + case !wire_chop_slice(m.root[:], &bs): return false + case !wire_chop_uint64(&tstamp, &bs): return false + case !wire_chop_uint64(&m.seq, &bs): return false + case !wire_chop_uint64(&m.hop, &bs): return false + } + m.tstamp = wire_intFromUint(tstamp) + return true +} + +type msgHop struct { + root sigPubKey + tstamp int64 + seq uint64 + hop uint64 + port switchPort + next sigPubKey + sig sigBytes +} + +func (m *msgHop) encode() []byte { + bs := wire_encode_uint64(wire_SwitchHop) + bs = append(bs, m.root[:]...) + bs = append(bs, wire_encode_uint64(wire_intToUint(m.tstamp))...) + bs = append(bs, wire_encode_uint64(m.seq)...) + bs = append(bs, wire_encode_uint64(m.hop)...) + bs = append(bs, wire_encode_uint64(uint64(m.port))...) + bs = append(bs, m.next[:]...) + bs = append(bs, m.sig[:]...) + return bs +} + +func (m *msgHop) decode(bs []byte) bool { + var pType uint64 + var tstamp uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_SwitchHop: return false + case !wire_chop_slice(m.root[:], &bs): return false + case !wire_chop_uint64(&tstamp, &bs): return false + case !wire_chop_uint64(&m.seq, &bs): return false + case !wire_chop_uint64(&m.hop, &bs): return false + case !wire_chop_uint64((*uint64)(&m.port), &bs): return false + case !wire_chop_slice(m.next[:], &bs): return false + case !wire_chop_slice(m.sig[:], &bs): return false + } + m.tstamp = wire_intFromUint(tstamp) + return true +} + +// Format used to check signatures only, so no need to also support decoding +func wire_encode_locator(loc *switchLocator) []byte { + coords := wire_encode_coords(loc.getCoords()) + var bs []byte + bs = append(bs, loc.root[:]...) + bs = append(bs, wire_encode_uint64(wire_intToUint(loc.tstamp))...) + bs = append(bs, coords...) + return bs +} + +func wire_chop_slice(toSlice []byte, fromSlice *[]byte) bool { + if len(*fromSlice) < len(toSlice) { return false } + copy(toSlice, *fromSlice) + *fromSlice = (*fromSlice)[len(toSlice):] + return true +} + +func wire_chop_coords(toCoords *[]byte, fromSlice *[]byte) bool { + coords, coordLen := wire_decode_coords(*fromSlice) + if coordLen == 0 { return false } + *toCoords = append((*toCoords)[:0], coords...) + *fromSlice = (*fromSlice)[coordLen:] + return true +} + +func wire_chop_uint64(toUInt64 *uint64, fromSlice *[]byte) bool { + dec, decLen := wire_decode_uint64(*fromSlice) + if decLen == 0 { return false } + *toUInt64 = dec + *fromSlice = (*fromSlice)[decLen:] + return true +} + +//////////////////////////////////////////////////////////////////////////////// + +// Wire traffic packets + +type wire_trafficPacket struct { + ttl uint64 // TODO? hide this as a wire format detail, not set by user + coords []byte + handle handle + nonce boxNonce + payload []byte +} + +// This is basically MarshalBinary, but decode doesn't allow that... +func (p *wire_trafficPacket) encode() []byte { + bs := util_getBytes() + bs = wire_put_uint64(wire_Traffic, bs) + bs = wire_put_uint64(p.ttl, bs) + bs = wire_put_coords(p.coords, bs) + bs = append(bs, p.handle[:]...) + bs = append(bs, p.nonce[:]...) + bs = append(bs, p.payload...) + return bs +} + +// Not just UnmarshalBinary becuase the original slice isn't always copied from +func (p *wire_trafficPacket) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_Traffic: return false + case !wire_chop_uint64(&p.ttl, &bs): return false + case !wire_chop_coords(&p.coords, &bs): return false + case !wire_chop_slice(p.handle[:], &bs): return false + case !wire_chop_slice(p.nonce[:], &bs): return false + } + p.payload = append(util_getBytes(), bs...) + return true +} + +type wire_protoTrafficPacket struct { + ttl uint64 // TODO? hide this as a wire format detail, not set by user + coords []byte + toKey boxPubKey + fromKey boxPubKey + nonce boxNonce + payload []byte +} + +func (p *wire_protoTrafficPacket) encode() []byte { + coords := wire_encode_coords(p.coords) + bs := wire_encode_uint64(wire_ProtocolTraffic) + bs = append(bs, wire_encode_uint64(p.ttl)...) + bs = append(bs, coords...) + bs = append(bs, p.toKey[:]...) + bs = append(bs, p.fromKey[:]...) + bs = append(bs, p.nonce[:]...) + bs = append(bs, p.payload...) + return bs +} + +func(p *wire_protoTrafficPacket) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_ProtocolTraffic: return false + case !wire_chop_uint64(&p.ttl, &bs): return false + case !wire_chop_coords(&p.coords, &bs): return false + case !wire_chop_slice(p.toKey[:], &bs): return false + case !wire_chop_slice(p.fromKey[:], &bs): return false + case !wire_chop_slice(p.nonce[:], &bs): return false + } + p.payload = bs + return true +} + +type wire_linkProtoTrafficPacket struct { + toKey boxPubKey + fromKey boxPubKey + nonce boxNonce + payload []byte +} + +func (p *wire_linkProtoTrafficPacket) encode() []byte { + bs := wire_encode_uint64(wire_LinkProtocolTraffic) + bs = append(bs, p.toKey[:]...) + bs = append(bs, p.fromKey[:]...) + bs = append(bs, p.nonce[:]...) + bs = append(bs, p.payload...) + return bs +} + +func(p *wire_linkProtoTrafficPacket) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_LinkProtocolTraffic: return false + case !wire_chop_slice(p.toKey[:], &bs): return false + case !wire_chop_slice(p.fromKey[:], &bs): return false + case !wire_chop_slice(p.nonce[:], &bs): return false + } + p.payload = bs + return true +} + +//////////////////////////////////////////////////////////////////////////////// + +func (p *sessionPing) encode() []byte { + var pTypeVal uint64 + if p.isPong { + pTypeVal = wire_SessionPong + } else { + pTypeVal = wire_SessionPing + } + bs := wire_encode_uint64(pTypeVal) + //p.sendPermPub used in top level (crypto), so skipped here + bs = append(bs, p.handle[:]...) + bs = append(bs, p.sendSesPub[:]...) + bs = append(bs, wire_encode_uint64(wire_intToUint(p.tstamp))...) + coords := wire_encode_coords(p.coords) + bs = append(bs, coords...) + return bs +} + +func (p *sessionPing) decode(bs []byte) bool { + var pType uint64 + var tstamp uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_SessionPing && pType != wire_SessionPong: return false + //p.sendPermPub used in top level (crypto), so skipped here + case !wire_chop_slice(p.handle[:], &bs): return false + case !wire_chop_slice(p.sendSesPub[:], &bs): return false + case !wire_chop_uint64(&tstamp, &bs): return false + case !wire_chop_coords(&p.coords, &bs): return false + } + p.tstamp = wire_intFromUint(tstamp) + if pType == wire_SessionPong { p.isPong = true } + return true +} + +//////////////////////////////////////////////////////////////////////////////// + +func (r *dhtReq) encode() []byte { + coords := wire_encode_coords(r.coords) + bs := wire_encode_uint64(wire_DHTLookupRequest) + bs = append(bs, r.key[:]...) + bs = append(bs, coords...) + bs = append(bs, r.dest[:]...) + return bs +} + +func (r *dhtReq) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_DHTLookupRequest: return false + case !wire_chop_slice(r.key[:], &bs): return false + case !wire_chop_coords(&r.coords, &bs): return false + case !wire_chop_slice(r.dest[:], &bs): return false + default: return true + } +} + +func (r *dhtRes) encode() []byte { + coords := wire_encode_coords(r.coords) + bs := wire_encode_uint64(wire_DHTLookupResponse) + bs = append(bs, r.key[:]...) + bs = append(bs, coords...) + bs = append(bs, r.dest[:]...) + for _, info := range r.infos { + coords = wire_encode_coords(info.coords) + bs = append(bs, info.key[:]...) + bs = append(bs, coords...) + } + return bs +} + +func (r *dhtRes) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_DHTLookupResponse: return false + case !wire_chop_slice(r.key[:], &bs): return false + case !wire_chop_coords(&r.coords, &bs): return false + case !wire_chop_slice(r.dest[:], &bs): return false + } + for len(bs) > 0 { + info := dhtInfo{} + switch { + case !wire_chop_slice(info.key[:], &bs): return false + case !wire_chop_coords(&info.coords, &bs): return false + } + r.infos = append(r.infos, &info) + } + return true +} + +//////////////////////////////////////////////////////////////////////////////// + +func (r *searchReq) encode() []byte { + coords := wire_encode_coords(r.coords) + bs := wire_encode_uint64(wire_SearchRequest) + bs = append(bs, r.key[:]...) + bs = append(bs, coords...) + bs = append(bs, r.dest[:]...) + return bs +} + +func (r *searchReq) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_SearchRequest: return false + case !wire_chop_slice(r.key[:], &bs): return false + case !wire_chop_coords(&r.coords, &bs): return false + case !wire_chop_slice(r.dest[:], &bs): return false + default: return true + } +} + +func (r *searchRes) encode() []byte { + coords := wire_encode_coords(r.coords) + bs := wire_encode_uint64(wire_SearchResponse) + bs = append(bs, r.key[:]...) + bs = append(bs, coords...) + bs = append(bs, r.dest[:]...) + return bs +} + +func (r *searchRes) decode(bs []byte) bool { + var pType uint64 + switch { + case !wire_chop_uint64(&pType, &bs): return false + case pType != wire_SearchResponse: return false + case !wire_chop_slice(r.key[:], &bs): return false + case !wire_chop_coords(&r.coords, &bs): return false + case !wire_chop_slice(r.dest[:], &bs): return false + default: return true + } +} + diff --git a/yggdrasil.go b/yggdrasil.go new file mode 100644 index 0000000..39f6170 --- /dev/null +++ b/yggdrasil.go @@ -0,0 +1,209 @@ +package main + +import "bytes" +import "encoding/hex" +import "encoding/json" +import "flag" +import "fmt" +import "io/ioutil" +import "net" +import "os" +import "os/signal" +import "time" + +import _ "net/http/pprof" +import "net/http" +import "log" +import "runtime" + +import "golang.org/x/net/ipv6" + +import . "yggdrasil" + +/** +* This is a very crude wrapper around src/yggdrasil +* It can generate a new config (--genconf) +* It can read a config from stdin (--useconf) +* It can run with an automatic config (--autoconf) +*/ + +type nodeConfig struct { + Listen string + Peers []string + BoxPub string + BoxPriv string + SigPub string + SigPriv string + Multicast bool +} + +type node struct { + core Core + sock *ipv6.PacketConn +} + +func (n *node) init(cfg *nodeConfig, logger *log.Logger) { + boxPub, err := hex.DecodeString(cfg.BoxPub) + if err != nil { panic(err) } + boxPriv, err := hex.DecodeString(cfg.BoxPriv) + if err != nil { panic(err) } + sigPub, err := hex.DecodeString(cfg.SigPub) + if err != nil { panic(err) } + sigPriv, err := hex.DecodeString(cfg.SigPriv) + if err != nil { panic(err) } + n.core.DEBUG_init(boxPub, boxPriv, sigPub, sigPriv) + n.core.DEBUG_setLogger(logger) + logger.Println("Starting interface...") + n.core.DEBUG_setupAndStartGlobalUDPInterface(cfg.Listen) + logger.Println("Started interface") + go func () { + if len(cfg.Peers) == 0 { return } + for { + for _, p := range cfg.Peers { + n.core.DEBUG_sendUDPKeys(p) + time.Sleep(time.Second) + } + } + }() +} + +func generateConfig() *nodeConfig { + core := Core{} + bpub, bpriv := core.DEBUG_newBoxKeys() + spub, spriv := core.DEBUG_newSigKeys() + cfg := nodeConfig{} + cfg.Listen = "[::]:0" + cfg.BoxPub = hex.EncodeToString(bpub[:]) + cfg.BoxPriv = hex.EncodeToString(bpriv[:]) + cfg.SigPub = hex.EncodeToString(spub[:]) + cfg.SigPriv = hex.EncodeToString(spriv[:]) + cfg.Peers = []string{} + cfg.Multicast = true + return &cfg +} + +func doGenconf() string { + cfg := generateConfig() + bs, err := json.MarshalIndent(cfg, "", " ") + if err != nil { panic(err) } + return string(bs) +} + +var multicastAddr = "[ff02::114]:9001" + +func (n *node) listen() { + groupAddr, err := net.ResolveUDPAddr("udp6", multicastAddr) + if err != nil { panic(err) } + bs := make([]byte, 2048) + for { + nBytes, rcm, fromAddr, err := n.sock.ReadFrom(bs) + if err != nil { panic(err) } + //if rcm == nil { continue } // wat + //fmt.Println("DEBUG:", "packet from:", fromAddr.String()) + if !rcm.Dst.IsLinkLocalMulticast() { continue } + if !rcm.Dst.Equal(groupAddr.IP) { continue } + anAddr := string(bs[:nBytes]) + addr, err := net.ResolveUDPAddr("udp6", anAddr) + if err != nil { panic(err) ; continue } // Panic for testing, remove later + from := fromAddr.(*net.UDPAddr) + //fmt.Println("DEBUG:", "heard:", addr.IP.String(), "from:", from.IP.String()) + if addr.IP.String() != from.IP.String() { continue } + addr.Zone = from.Zone + saddr := addr.String() + //if _, isIn := n.peers[saddr]; isIn { continue } + //n.peers[saddr] = struct{}{} + n.core.DEBUG_sendUDPKeys(saddr) + //fmt.Println("DEBUG:", "added multicast peer:", saddr) + } +} + +func (n *node) announce() { + groupAddr, err := net.ResolveUDPAddr("udp6", multicastAddr) + if err != nil { panic(err) } + udpaddr := n.core.DEBUG_getGlobalUDPAddr() + anAddr, err := net.ResolveUDPAddr("udp6", udpaddr.String()) + if err != nil { panic(err) } + destAddr, err := net.ResolveUDPAddr("udp6", multicastAddr) + if err != nil { panic(err) } + for { + ifaces, err := net.Interfaces() + if err != nil { panic(err) } + for _, iface := range ifaces { + n.sock.JoinGroup(&iface, groupAddr) + //err := n.sock.JoinGroup(&iface, groupAddr) + //if err != nil { panic(err) } + addrs, err := iface.Addrs() + if err != nil { panic(err) } + for _, addr := range addrs { + addrIP, _, _ := net.ParseCIDR(addr.String()) + if addrIP.To4() != nil { continue } // IPv6 only + if !addrIP.IsLinkLocalUnicast() { continue } + anAddr.IP = addrIP + anAddr.Zone = iface.Name + destAddr.Zone = iface.Name + msg := []byte(anAddr.String()) + n.sock.WriteTo(msg, nil, destAddr) + break + } + time.Sleep(time.Second) + } + time.Sleep(time.Second) + } +} + +var pprof = flag.Bool("pprof", false, "Run pprof, see http://localhost:6060/debug/pprof/") +var genconf = flag.Bool("genconf", false, "print a new config to stdout") +var useconf = flag.Bool("useconf", false, "read config from stdin") +var autoconf = flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)") + +func main() { + flag.Parse() + var cfg *nodeConfig + switch { + case *autoconf: cfg = generateConfig() + case *useconf: + config, err := ioutil.ReadAll(os.Stdin) + if err != nil { panic(err) } + decoder := json.NewDecoder(bytes.NewReader(config)) + err = decoder.Decode(&cfg) + if err != nil { panic(err) } + case *genconf: fmt.Println(doGenconf()) + default: flag.PrintDefaults() + } + if cfg == nil { return } + logger := log.New(os.Stdout, "", log.Flags()) + if *pprof { + runtime.SetBlockProfileRate(1) + go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }() + } + // Setup + logger.Println("Initializing...") + n := node{} + n.init(cfg, logger) + logger.Println("Starting tun...") + n.core.DEBUG_startTun() // 1280, the smallest supported MTU + //n.core.DEBUG_startTunWithMTU(65535) // Largest supported MTU + defer func() { + logger.Println("Closing...") + n.core.DEBUG_stopTun() + }() + logger.Println("Started...") + if cfg.Multicast { + addr, err := net.ResolveUDPAddr("udp", multicastAddr) + if err != nil { panic(err) } + listenString := fmt.Sprintf("[::]:%v", addr.Port) + conn, err := net.ListenPacket("udp6", listenString) + if err != nil { panic(err) } + //defer conn.Close() // Let it close on its own when the application exits + n.sock = ipv6.NewPacketConn(conn) + if err = n.sock.SetControlMessage(ipv6.FlagDst, true) ; err != nil { panic(err) } + go n.listen() + go n.announce() + } + // Catch interrupt to exit gracefully + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + <-c + logger.Println("Stopping...") +} +