diff --git a/.circleci/config.yml b/.circleci/config.yml index 036c97e3..f3092ba1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,8 +7,6 @@ jobs: docker: - image: circleci/golang:1.11 - working_directory: /go/src/github.com/{{ORG_NAME}}/{{REPO_NAME}} - steps: - checkout @@ -17,7 +15,9 @@ jobs: command: | mkdir /tmp/upload echo 'export CINAME=$(sh contrib/semver/name.sh)' >> $BASH_ENV - echo 'export CIVERSION=$(sh contrib/semver/version.sh | cut -c 2-)' >> $BASH_ENV + echo 'export CIVERSION=$(sh contrib/semver/version.sh --bare)' >> $BASH_ENV + git config --global user.email "$(git log --format='%ae' HEAD -1)"; + git config --global user.name "$(git log --format='%an' HEAD -1)"; - run: name: Install alien @@ -50,6 +50,13 @@ jobs: GOOS=darwin GOARCH=amd64 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-amd64 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-darwin-amd64; GOOS=darwin GOARCH=386 ./build && mv yggdrasil /tmp/upload/$CINAME-$CIVERSION-darwin-i386 && mv yggdrasilctl /tmp/upload/$CINAME-$CIVERSION-yggdrasilctl-darwin-i386; + - run: + name: Build for macOS (.pkg format) + command: | + rm -rf {yggdrasil,yggdrasilctl} + GOOS=darwin GOARCH=amd64 ./build && PKGARCH=amd64 sh contrib/macos/create-pkg.sh && mv *.pkg /tmp/upload/ + GOOS=darwin GOARCH=386 ./build && PKGARCH=i386 sh contrib/macos/create-pkg.sh && mv *.pkg /tmp/upload/ + - run: name: Build for OpenBSD command: | @@ -91,3 +98,13 @@ jobs: - store_artifacts: path: /tmp/upload destination: / + + - run: + name: Create tags (master branch only) + command: > + if [ "${CIRCLE_BRANCH}" == "master" ]; then + git tag -f -a $(sh contrib/semver/version.sh) -m "Created by CircleCI" && git push -f --tags; + else + echo "Only runs for master branch (this is ${CIRCLE_BRANCH})"; + fi; + when: on_success diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c72c27a..2db4dde1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,33 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - in case of vulnerabilities. --> +## [0.3.0] - 2018-12-12 +### Added +- Crypto-key routing support for tunnelling both IPv4 and IPv6 over Yggdrasil +- Add advanced `SwitchOptions` in configuration file for tuning the switch +- Add `dhtPing` to the admin socket to aid in crawling the network +- New macOS .pkgs built automatically by CircleCI +- Add Dockerfile to repository for Docker support +- Add `-json` command line flag for generating and normalising configuration in plain JSON instead of HJSON +- Build name and version numbers are now imprinted onto the build, accessible through `yggdrasil -version` and `yggdrasilctl getSelf` +- Add ability to disable admin socket by setting `AdminListen` to `"none"` +- `yggdrasilctl` now tries to look for the default configuration file to find `AdminListen` if `-endpoint` is not specified +- `yggdrasilctl` now returns more useful logging in the event of a fatal error + +### Changed +- Switched to Chord DHT (instead of Kademlia, although still compatible at the protocol level) +- The `AdminListen` option and `yggdrasilctl` now default to `unix:///var/run/yggdrasil.sock` on BSDs, macOS and Linux +- Cleaned up some of the parameter naming in the admin socket +- Latency-based parent selection for the switch instead of uptime-based (should help to avoid high latency links somewhat) +- Real peering endpoints now shown in the admin socket `getPeers` call to help identify peerings +- Reuse the multicast port on supported platforms so that multiple Yggdrasil processes can run +- `yggdrasilctl` now has more useful help text (with `-help` or when no arguments passed) + +### Fixed +- Memory leaks in the DHT fixed +- Crash fixed where the ICMPv6 NDP goroutine would incorrectly start in TUN mode +- Removing peers from the switch table if they stop sending switch messages but keep the TCP connection alive + ## [0.2.7] - 2018-10-13 ### Added - Session firewall, which makes it possible to control who can open sessions with your node diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4cac86d9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1 @@ +contrib/docker/Dockerfile diff --git a/LICENSE b/LICENSE index 53320c35..1182c200 100644 --- a/LICENSE +++ b/LICENSE @@ -17,11 +17,10 @@ statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. - GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + 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. diff --git a/README.md b/README.md index 15731952..11a4cbe5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ You're encouraged to play with it, but it is strongly advised not to use it for ## Building -1. Install Go (tested on 1.9+, [godeb](https://github.com/niemeyer/godeb) is recommended for debian-based linux distributions). +1. Install Go (requires 1.11 or later, [godeb](https://github.com/niemeyer/godeb) is recommended for Debian-based Linux distributions). 2. Clone this repository. 2. `./build` diff --git a/VERSION b/VERSION deleted file mode 100644 index 3b04cfb6..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.2 diff --git a/build b/build index c07e5eac..e463c852 100755 --- a/build +++ b/build @@ -1,25 +1,36 @@ #!/bin/sh -while getopts ud option + +PKGSRC=${PKGSRC:-github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil} +PKGNAME=${PKGNAME:-$(sh contrib/semver/name.sh)} +PKGVER=${PKGVER:-$(sh contrib/semver/version.sh --bare)} + +LDFLAGS="-X $PKGSRC.buildName=$PKGNAME -X $PKGSRC.buildVersion=$PKGVER" + +while getopts "udtc:l:" option do case "${option}" in u) UPX=true;; d) DEBUG=true;; + t) TABLES=true;; + c) GCFLAGS="$GCFLAGS $OPTARG";; + l) LDFLAGS="$LDFLAGS $OPTARG";; esac done -export GOPATH=$PWD -echo "Downloading..." -go get -d -v -go get -d -v yggdrasil -for file in *.go ; do - echo "Building: $file" - #go build $@ $file + +if [ -z $TABLES ]; then + STRIP="-s -w" +fi + +for CMD in `ls cmd/` ; do + echo "Building: $CMD" + if [ $DEBUG ]; then - go build -tags debug -v $file + go build -ldflags="$LDFLAGS" -gcflags="$GCFLAGS" -tags debug -v ./cmd/$CMD else - go build -ldflags="-s -w" -v $file + go build -ldflags="$LDFLAGS $STRIP" -gcflags="$GCFLAGS" -v ./cmd/$CMD fi if [ $UPX ]; then - upx --brute ${file%.go} + upx --brute $CMD fi done diff --git a/yggdrasil.go b/cmd/yggdrasil/main.go similarity index 89% rename from yggdrasil.go rename to cmd/yggdrasil/main.go index 447bb3ec..5b756908 100644 --- a/yggdrasil.go +++ b/cmd/yggdrasil/main.go @@ -21,9 +21,9 @@ import ( "github.com/mitchellh/mapstructure" "github.com/neilalexander/hjson-go" - "yggdrasil" - "yggdrasil/config" - "yggdrasil/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/config" + "github.com/yggdrasil-network/yggdrasil-go/src/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil" ) type nodeConfig = config.NodeConfig @@ -69,15 +69,22 @@ func generateConfig(isAutoconf bool) *nodeConfig { cfg.SessionFirewall.Enable = false cfg.SessionFirewall.AllowFromDirect = true cfg.SessionFirewall.AllowFromRemote = true + cfg.SwitchOptions.MaxTotalQueueSize = yggdrasil.SwitchQueueTotalMinSize return &cfg } // Generates a new configuration and returns it in HJSON format. This is used // with -genconf. -func doGenconf() string { +func doGenconf(isjson bool) string { cfg := generateConfig(false) - bs, err := hjson.Marshal(cfg) + var bs []byte + var err error + if isjson { + bs, err = json.MarshalIndent(cfg, "", " ") + } else { + bs, err = hjson.Marshal(cfg) + } if err != nil { panic(err) } @@ -88,14 +95,20 @@ func doGenconf() string { func main() { // Configure the command line parameters. genconf := flag.Bool("genconf", false, "print a new config to stdout") - useconf := flag.Bool("useconf", false, "read config from stdin") - useconffile := flag.String("useconffile", "", "read config from specified file path") + useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin") + useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path") normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised") + confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON") autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)") + version := flag.Bool("version", false, "prints the version of this build") flag.Parse() var cfg *nodeConfig switch { + case *version: + fmt.Println("Build name:", yggdrasil.GetBuildName()) + fmt.Println("Build version:", yggdrasil.GetBuildVersion()) + os.Exit(0) case *autoconf: // Use an autoconf-generated config, this will give us random keys and // port numbers, and will use an automatically selected TUN/TAP interface. @@ -185,7 +198,12 @@ func main() { // their configuration file with newly mapped names (like above) or to // convert from plain JSON to commented HJSON. if *normaliseconf { - bs, err := hjson.Marshal(cfg) + var bs []byte + if *confjson { + bs, err = json.MarshalIndent(cfg, "", " ") + } else { + bs, err = hjson.Marshal(cfg) + } if err != nil { panic(err) } @@ -194,7 +212,7 @@ func main() { } case *genconf: // Generate a new configuration and print it to stdout. - fmt.Println(doGenconf()) + fmt.Println(doGenconf(*confjson)) default: // No flags were provided, therefore print the list of flags to stdout. flag.PrintDefaults() diff --git a/yggdrasilctl.go b/cmd/yggdrasilctl/main.go similarity index 60% rename from yggdrasilctl.go rename to cmd/yggdrasilctl/main.go index d98386b7..b37c2561 100644 --- a/yggdrasilctl.go +++ b/cmd/yggdrasilctl/main.go @@ -1,52 +1,117 @@ package main -import "errors" -import "flag" -import "fmt" -import "strings" -import "net" -import "net/url" -import "sort" -import "encoding/json" -import "strconv" -import "os" +import ( + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "log" + "net" + "net/url" + "os" + "sort" + "strconv" + "strings" -import "yggdrasil/defaults" + "golang.org/x/text/encoding/unicode" + + "github.com/neilalexander/hjson-go" + "github.com/yggdrasil-network/yggdrasil-go/src/defaults" +) type admin_info map[string]interface{} func main() { - server := flag.String("endpoint", defaults.GetDefaults().DefaultAdminListen, "Admin socket endpoint") - injson := flag.Bool("json", false, "Output in JSON format") + logbuffer := &bytes.Buffer{} + logger := log.New(logbuffer, "", log.Flags()) + defer func() { + if r := recover(); r != nil { + logger.Println("Fatal error:", r) + fmt.Print(logbuffer) + os.Exit(1) + } + }() + + endpoint := defaults.GetDefaults().DefaultAdminListen + + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] command [key=value] [key=value] ...\n", os.Args[0]) + fmt.Println("Options:") + flag.PrintDefaults() + fmt.Println("Commands:\n - Use \"list\" for a list of available commands") + fmt.Println("Examples:") + fmt.Println(" - ", os.Args[0], "list") + fmt.Println(" - ", os.Args[0], "getPeers") + fmt.Println(" - ", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false") + fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getDHT") + fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT") + } + server := flag.String("endpoint", endpoint, "Admin socket endpoint") + injson := flag.Bool("json", false, "Output in JSON format (as opposed to pretty-print)") + verbose := flag.Bool("v", false, "Verbose output (includes public keys)") flag.Parse() args := flag.Args() if len(args) == 0 { - fmt.Println("usage:", os.Args[0], "[-endpoint=proto://server] [-json] command [key=value] [...]") - fmt.Println("example:", os.Args[0], "getPeers") - fmt.Println("example:", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false") - fmt.Println("example:", os.Args[0], "-endpoint=tcp://localhost:9001 getDHT") - fmt.Println("example:", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT") + flag.Usage() return } + if *server == endpoint { + if config, err := ioutil.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil { + if bytes.Compare(config[0:2], []byte{0xFF, 0xFE}) == 0 || + bytes.Compare(config[0:2], []byte{0xFE, 0xFF}) == 0 { + utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM) + decoder := utf.NewDecoder() + config, err = decoder.Bytes(config) + if err != nil { + panic(err) + } + } + var dat map[string]interface{} + if err := hjson.Unmarshal(config, &dat); err != nil { + panic(err) + } + if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") { + endpoint = ep + logger.Println("Found platform default config file", defaults.GetDefaults().DefaultConfigFile) + logger.Println("Using endpoint", endpoint, "from AdminListen") + } else { + logger.Println("Configuration file doesn't contain appropriate AdminListen option") + logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) + } + } else { + logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile) + logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen) + } + } else { + logger.Println("Using endpoint", endpoint, "from command line") + } + var conn net.Conn - u, err := url.Parse(*server) + u, err := url.Parse(endpoint) if err == nil { switch strings.ToLower(u.Scheme) { case "unix": - conn, err = net.Dial("unix", (*server)[7:]) + logger.Println("Connecting to UNIX socket", endpoint[7:]) + conn, err = net.Dial("unix", endpoint[7:]) case "tcp": + logger.Println("Connecting to TCP socket", u.Host) conn, err = net.Dial("tcp", u.Host) default: + logger.Println("Unknown protocol or malformed address - check your endpoint") err = errors.New("protocol not supported") } } else { - conn, err = net.Dial("tcp", *server) + logger.Println("Connecting to TCP socket", u.Host) + conn, err = net.Dial("tcp", endpoint) } if err != nil { panic(err) } + logger.Println("Connected") defer conn.Close() decoder := json.NewDecoder(conn) @@ -56,11 +121,13 @@ func main() { for c, a := range args { if c == 0 { + logger.Printf("Sending request: %v\n", a) send["request"] = a continue } tokens := strings.Split(a, "=") if i, err := strconv.Atoi(tokens[1]); err == nil { + logger.Printf("Sending parameter %s: %d\n", tokens[0], i) send[tokens[0]] = i } else { switch strings.ToLower(tokens[1]) { @@ -71,28 +138,31 @@ func main() { default: send[tokens[0]] = tokens[1] } + logger.Printf("Sending parameter %s: %v\n", tokens[0], send[tokens[0]]) } } if err := encoder.Encode(&send); err != nil { panic(err) } + logger.Printf("Request sent") if err := decoder.Decode(&recv); err == nil { + logger.Printf("Response received") if recv["status"] == "error" { if err, ok := recv["error"]; ok { - fmt.Println("Error:", err) + fmt.Println("Admin socket returned an error:", err) } else { - fmt.Println("Unspecified error occured") + fmt.Println("Admin socket returned an error but didn't specify any error text") } os.Exit(1) } if _, ok := recv["request"]; !ok { fmt.Println("Missing request in response (malformed response?)") - return + os.Exit(1) } if _, ok := recv["response"]; !ok { fmt.Println("Missing response body (malformed response?)") - return + os.Exit(1) } req := recv["request"].(map[string]interface{}) res := recv["response"].(map[string]interface{}) @@ -107,7 +177,7 @@ func main() { switch strings.ToLower(req["request"].(string)) { case "dot": fmt.Println(res["dot"]) - case "help", "getpeers", "getswitchpeers", "getdht", "getsessions": + case "list", "getpeers", "getswitchpeers", "getdht", "getsessions", "dhtping": maxWidths := make(map[string]int) var keyOrder []string keysOrdered := false @@ -116,6 +186,11 @@ func main() { for slk, slv := range tlv.(map[string]interface{}) { if !keysOrdered { for k := range slv.(map[string]interface{}) { + if !*verbose { + if k == "box_pub_key" || k == "box_sig_key" { + continue + } + } keyOrder = append(keyOrder, fmt.Sprint(k)) } sort.Strings(keyOrder) @@ -175,6 +250,12 @@ func main() { } case "getself": for k, v := range res["self"].(map[string]interface{}) { + if buildname, ok := v.(map[string]interface{})["build_name"].(string); ok && buildname != "unknown" { + fmt.Println("Build name:", buildname) + } + if buildversion, ok := v.(map[string]interface{})["build_version"].(string); ok && buildversion != "unknown" { + fmt.Println("Build version:", buildversion) + } fmt.Println("IPv6 address:", k) if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok { fmt.Println("IPv6 subnet:", subnet) @@ -182,6 +263,14 @@ func main() { if coords, ok := v.(map[string]interface{})["coords"].(string); ok { fmt.Println("Coords:", coords) } + if *verbose { + if boxPubKey, ok := v.(map[string]interface{})["box_pub_key"].(string); ok { + fmt.Println("Public encryption key:", boxPubKey) + } + if boxSigKey, ok := v.(map[string]interface{})["box_sig_key"].(string); ok { + fmt.Println("Public signing key:", boxSigKey) + } + } } case "getswitchqueues": maximumqueuesize := float64(4194304) @@ -202,8 +291,8 @@ func main() { fmt.Printf("Highest queue size: %d bytes\n", uint(highestqueuesize)) } if m, ok := v["maximum_queues_size"].(float64); ok { - fmt.Printf("Maximum queue size: %d bytes\n", uint(maximumqueuesize)) maximumqueuesize = m + fmt.Printf("Maximum queue size: %d bytes\n", uint(maximumqueuesize)) } if queues, ok := v["queues"].([]interface{}); ok { if len(queues) != 0 { @@ -213,7 +302,7 @@ func main() { queuesize := v.(map[string]interface{})["queue_size"].(float64) queuepackets := v.(map[string]interface{})["queue_packets"].(float64) queueid := v.(map[string]interface{})["queue_id"].(string) - portqueues[queueport] += 1 + portqueues[queueport]++ portqueuesize[queueport] += queuesize portqueuepackets[queueport] += queuepackets queuesizepercent := (100 / maximumqueuesize) * queuesize @@ -231,7 +320,7 @@ func main() { uint(k), uint(v), uint(queuesizepercent), uint(portqueuepackets[k])) } } - case "addpeer", "removepeer", "addallowedencryptionpublickey", "removeallowedencryptionpublickey": + case "addpeer", "removepeer", "addallowedencryptionpublickey", "removeallowedencryptionpublickey", "addsourcesubnet", "addroute", "removesourcesubnet", "removeroute": if _, ok := res["added"]; ok { for _, v := range res["added"].([]interface{}) { fmt.Println("Added:", fmt.Sprint(v)) @@ -274,14 +363,38 @@ func main() { fmt.Println("-", v) } } + case "getsourcesubnets": + if _, ok := res["source_subnets"]; !ok { + fmt.Println("No source subnets found") + } else if res["source_subnets"] == nil { + fmt.Println("No source subnets found") + } else { + fmt.Println("Source subnets:") + for _, v := range res["source_subnets"].([]interface{}) { + fmt.Println("-", v) + } + } + case "getroutes": + if _, ok := res["routes"]; !ok { + fmt.Println("No routes found") + } else if res["routes"] == nil { + fmt.Println("No routes found") + } else { + fmt.Println("Routes:") + for _, v := range res["routes"].([]interface{}) { + fmt.Println("-", v) + } + } default: if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil { fmt.Println(string(json)) } } + } else { + logger.Println("Error receiving response:", err) } - if v, ok := recv["status"]; ok && v == "error" { + if v, ok := recv["status"]; ok && v != "success" { os.Exit(1) } os.Exit(0) diff --git a/contrib/config/yggdrasilconf.go b/contrib/config/yggdrasilconf.go index bc6e1326..78c0e6d7 100644 --- a/contrib/config/yggdrasilconf.go +++ b/contrib/config/yggdrasilconf.go @@ -17,7 +17,7 @@ import ( "github.com/neilalexander/hjson-go" "golang.org/x/text/encoding/unicode" - "yggdrasil/config" + "github.com/yggdrasil-network/yggdrasil-go/src/config" ) type nodeConfig = config.NodeConfig diff --git a/contrib/deb/generate.sh b/contrib/deb/generate.sh index 9d5064bd..5af31d5c 100644 --- a/contrib/deb/generate.sh +++ b/contrib/deb/generate.sh @@ -7,12 +7,12 @@ if [ `pwd` != `git rev-parse --show-toplevel` ] then echo "You should run this script from the top-level directory of the git repo" - exit -1 + exit 1 fi PKGBRANCH=$(basename `git name-rev --name-only HEAD`) PKGNAME=$(sh contrib/semver/name.sh) -PKGVERSION=$(sh contrib/semver/version.sh | cut -c 2-) +PKGVERSION=$(sh contrib/semver/version.sh --bare) PKGARCH=${PKGARCH-amd64} PKGFILE=$PKGNAME-$PKGVERSION-$PKGARCH.deb PKGREPLACES=yggdrasil @@ -29,7 +29,7 @@ elif [ $PKGARCH = "armhf" ]; then GOARCH=arm GOOS=linux GOARM=7 ./build elif [ $PKGARCH = "arm64" ]; then GOARCH=arm64 GOOS=linux ./build else echo "Specify PKGARCH=amd64,i386,mips,mipsel,armhf,arm64" - exit -1 + exit 1 fi echo "Building $PKGFILE" diff --git a/contrib/docker/Dockerfile b/contrib/docker/Dockerfile new file mode 100644 index 00000000..6b4bfcb6 --- /dev/null +++ b/contrib/docker/Dockerfile @@ -0,0 +1,22 @@ +FROM docker.io/golang:alpine as builder + +COPY . /src +WORKDIR /src +RUN apk add git && ./build + +FROM docker.io/alpine +LABEL maintainer="Christer Waren/CWINFO " + +COPY --from=builder /src/yggdrasil /usr/bin/yggdrasil +COPY --from=builder /src/yggdrasilctl /usr/bin/yggdrasilctl +COPY contrib/docker/entrypoint.sh /usr/bin/entrypoint.sh + +# RUN addgroup -g 1000 -S yggdrasil-network \ +# && adduser -u 1000 -S -g 1000 --home /etc/yggdrasil-network yggdrasil-network +# +# USER yggdrasil-network +# TODO: Make running unprivileged work + +VOLUME [ "/etc/yggdrasil-network" ] + +ENTRYPOINT [ "/usr/bin/entrypoint.sh" ] diff --git a/contrib/docker/entrypoint.sh b/contrib/docker/entrypoint.sh new file mode 100755 index 00000000..26c685a8 --- /dev/null +++ b/contrib/docker/entrypoint.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +set -e + +CONF_DIR="/etc/yggdrasil-network" + +if [ ! -f "$CONF_DIR/config.conf" ]; then + echo "generate $CONF_DIR/config.conf" + yggdrasil --genconf > "$CONF_DIR/config.conf" +fi + +yggdrasil --useconf < "$CONF_DIR/config.conf" +exit $? diff --git a/contrib/macos/create-pkg.sh b/contrib/macos/create-pkg.sh new file mode 100755 index 00000000..cc9a74f7 --- /dev/null +++ b/contrib/macos/create-pkg.sh @@ -0,0 +1,122 @@ +#!/bin/sh + +# Check if xar and mkbom are available +command -v xar >/dev/null 2>&1 || ( + echo "Building xar" + sudo apt-get install libxml2-dev libssl1.0-dev zlib1g-dev -y + mkdir -p /tmp/xar && cd /tmp/xar + git clone https://github.com/mackyle/xar && cd xar/xar + (sh autogen.sh && make && sudo make install) || (echo "Failed to build xar"; exit 1) +) +command -v mkbom >/dev/null 2>&1 || ( + echo "Building mkbom" + mkdir -p /tmp/mkbom && cd /tmp/mkbom + git clone https://github.com/hogliux/bomutils && cd bomutils + sudo make install || (echo "Failed to build mkbom"; exit 1) +) + +# Check if we can find the files we need - they should +# exist if you are running this script from the root of +# the yggdrasil-go repo and you have ran ./build +test -f yggdrasil || (echo "yggdrasil binary not found"; exit 1) +test -f yggdrasilctl || (echo "yggdrasilctl binary not found"; exit 1) +test -f contrib/macos/yggdrasil.plist || (echo "contrib/macos/yggdrasil.plist not found"; exit 1) +test -f contrib/semver/version.sh || (echo "contrib/semver/version.sh not found"; exit 1) + +# Delete the pkgbuild folder if it already exists +test -d pkgbuild && rm -rf pkgbuild + +# Create our folder structure +mkdir -p pkgbuild/scripts +mkdir -p pkgbuild/flat/base.pkg +mkdir -p pkgbuild/flat/Resources/en.lproj +mkdir -p pkgbuild/root/usr/local/bin +mkdir -p pkgbuild/root/Library/LaunchDaemons + +# Copy package contents into the pkgbuild root +cp yggdrasil pkgbuild/root/usr/local/bin +cp yggdrasilctl pkgbuild/root/usr/local/bin +cp contrib/macos/yggdrasil.plist pkgbuild/root/Library/LaunchDaemons + +# Create the postinstall script +cat > pkgbuild/scripts/postinstall << EOF +#!/bin/sh + +# Normalise the config if it exists, generate it if it doesn't +if [ -f /etc/yggdrasil.conf ]; +then + mkdir -p /Library/Preferences/Yggdrasil + echo "Backing up configuration file to /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d`" + cp /etc/yggdrasil.conf /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d` + echo "Normalising /etc/yggdrasil.conf" + /usr/local/bin/yggdrasil -useconffile /Library/Preferences/Yggdrasil/yggdrasil.conf.`date +%Y%m%d` -normaliseconf > /etc/yggdrasil.conf +else + /usr/local/bin/yggdrasil -genconf > /etc/yggdrasil.conf +fi + +# Unload existing Yggdrasil launchd service, if possible +test -f /Library/LaunchDaemons/yggdrasil.plist && (launchctl unload /Library/LaunchDaemons/yggdrasil.plist || true) + +# Load Yggdrasil launchd service and start Yggdrasil +launchctl load /Library/LaunchDaemons/yggdrasil.plist +EOF + +# Set execution permissions +chmod +x pkgbuild/scripts/postinstall +chmod +x pkgbuild/root/usr/local/bin/yggdrasil +chmod +x pkgbuild/root/usr/local/bin/yggdrasilctl + +# Pack payload and scripts +( cd pkgbuild/scripts && find . | cpio -o --format odc --owner 0:80 | gzip -c ) > pkgbuild/flat/base.pkg/Scripts +( cd pkgbuild/root && find . | cpio -o --format odc --owner 0:80 | gzip -c ) > pkgbuild/flat/base.pkg/Payload + +# Work out metadata for the package info +PKGNAME=$(sh contrib/semver/name.sh) +PKGVERSION=$(sh contrib/semver/version.sh --bare) +PKGARCH=${PKGARCH-amd64} +PAYLOADSIZE=$(( $(wc -c pkgbuild/flat/base.pkg/Payload | awk '{ print $1 }') / 1024 )) + +# Create the PackageInfo file +cat > pkgbuild/flat/base.pkg/PackageInfo << EOF + + + + + + +EOF + +# Create the BOM +( cd pkgbuild && mkbom root flat/base.pkg/Bom ) + +# Create the Distribution file +cat > pkgbuild/flat/Distribution << EOF + + + Yggdrasil (${PKGNAME}-${PKGVERSION}) + + + + + + + + + + + #base.pkg + +EOF + +# Finally pack the .pkg +( cd pkgbuild/flat && xar --compression none -cf "../../${PKGNAME}-${PKGVERSION}-macos-${PKGARCH}.pkg" * ) diff --git a/contrib/rpm/yggdrasil.spec b/contrib/rpm/yggdrasil.spec new file mode 100644 index 00000000..bab50906 --- /dev/null +++ b/contrib/rpm/yggdrasil.spec @@ -0,0 +1,47 @@ +Name: yggdrasil +Version: 0.3.0 +Release: 1%{?dist} +Summary: End-to-end encrypted IPv6 networking + +License: GPLv3 +URL: https://yggdrasil-network.github.io +Source0: https://codeload.github.com/yggdrasil-network/yggdrasil-go/tar.gz/v0.3.0 + +%{?systemd_requires} +BuildRequires: systemd golang >= 1.11 + +%description +Yggdrasil is a proof-of-concept to explore a wholly different approach to +network routing. Whereas current computer networks depend heavily on very +centralised design and configuration, Yggdrasil breaks this mould by making +use of a global spanning tree to form a scalable IPv6 encrypted mesh network. + +%prep +%setup -qn yggdrasil-go-%{version} + +%build +./build -t -l "-linkmode=external" + +%install +rm -rf %{buildroot} +mkdir -p %{buildroot}/%{_bindir} +mkdir -p %{buildroot}/%{_sysconfdir}/systemd/system +install -m 0755 yggdrasil %{buildroot}/%{_bindir}/yggdrasil +install -m 0755 yggdrasilctl %{buildroot}/%{_bindir}/yggdrasilctl +install -m 0755 contrib/systemd/yggdrasil.service %{buildroot}/%{_sysconfdir}/systemd/system/yggdrasil.service +install -m 0755 contrib/systemd/yggdrasil-resume.service %{buildroot}/%{_sysconfdir}/systemd/system/yggdrasil-resume.service + +%files +%{_bindir}/yggdrasil +%{_bindir}/yggdrasilctl +%{_sysconfdir}/systemd/system/yggdrasil.service +%{_sysconfdir}/systemd/system/yggdrasil-resume.service + +%post +%systemd_post yggdrasil.service + +%preun +%systemd_preun yggdrasil.service + +%postun +%systemd_postun_with_restart yggdrasil.service diff --git a/contrib/semver/name.sh b/contrib/semver/name.sh index d749d3ff..935cc750 100644 --- a/contrib/semver/name.sh +++ b/contrib/semver/name.sh @@ -1,7 +1,16 @@ #!/bin/sh -# Get the branch name, removing any "/" characters from pull requests -BRANCH=$(git symbolic-ref --short HEAD | tr -d "/" 2>/dev/null) +# Get the current branch name +BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null) + +# Complain if the git history is not available +if [ $? != 0 ]; then + printf "unknown" + exit 1 +fi + +# Remove "/" characters from the branch name if present +BRANCH=$(echo $BRANCH | tr -d "/") # Check if the branch name is not master if [ "$BRANCH" = "master" ]; then diff --git a/contrib/semver/version.sh b/contrib/semver/version.sh index bd009a9a..331046f3 100644 --- a/contrib/semver/version.sh +++ b/contrib/semver/version.sh @@ -4,7 +4,7 @@ DEVELOPBRANCH="yggdrasil-network/develop" # Get the last tag -TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*" 2>/dev/null) +TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*\.0" 2>/dev/null) # Get last merge to master MERGE=$(git rev-list $TAG..master --grep "from $DEVELOPBRANCH" 2>/dev/null | head -n 1) @@ -12,12 +12,25 @@ MERGE=$(git rev-list $TAG..master --grep "from $DEVELOPBRANCH" 2>/dev/null | hea # Get the number of merges since the last merge to master PATCH=$(git rev-list $TAG..master --count --merges --grep="from $DEVELOPBRANCH" 2>/dev/null) +# Decide whether we should prepend the version with "v" - the default is that +# we do because we use it in git tags, but we might not always need it +PREPEND="v" +if [ "$1" = "--bare" ]; then + PREPEND="" +fi + # If it fails then there's no last tag - go from the first commit if [ $? != 0 ]; then PATCH=$(git rev-list HEAD --count 2>/dev/null) - printf 'v0.0.%d' "$PATCH" - exit -1 + # Complain if the git history is not available + if [ $? != 0 ]; then + printf 'unknown' + exit 1 + fi + + printf '%s0.0.%d' "$PREPEND" "$PATCH" + exit 1 fi # Get the number of merges on the current branch since the last tag @@ -32,9 +45,13 @@ BRANCH=$(git rev-parse --abbrev-ref HEAD) # Output in the desired format if [ $PATCH = 0 ]; then - printf 'v%d.%d' "$MAJOR" "$MINOR" + if [ ! -z $FULL ]; then + printf '%s%d.%d.0' "$PREPEND" "$MAJOR" "$MINOR" + else + printf '%s%d.%d' "$PREPEND" "$MAJOR" "$MINOR" + fi else - printf 'v%d.%d.%d' "$MAJOR" "$MINOR" "$PATCH" + printf '%s%d.%d.%d' "$PREPEND" "$MAJOR" "$MINOR" "$PATCH" fi # Add the build tag on non-master branches @@ -43,4 +60,3 @@ if [ $BRANCH != "master" ]; then printf -- "-%04d" "$BUILD" fi fi - diff --git a/doc/README.md b/doc/README.md deleted file mode 100644 index 4310c5be..00000000 --- a/doc/README.md +++ /dev/null @@ -1,188 +0,0 @@ -# Yggdrasil-go - -## What is it? - -This is a toy implementation of an encrypted IPv6 network. -A number of years ago, I started to spend some of my free time studying and routing schemes, and eventually decided that it made sense to come up with my own. -After much time spent reflecting on the problem, and a few failed starts, I eventually cobbled together one that seemed to have, more or less, the performance characteristics I was looking for. -I resolved to eventually write a proof-of-principle / test implementation, and I thought it would make sense to include many of the nice bells and whistles that I've grown accustomed to from using [cjdns](https://github.com/cjdelisle/cjdns), plus a few additional features that I wanted to test. -Fast forward through a couple years of procrastination, and I've finally started working on it in my limited spare time. -I've found that it's now marginally more interesting than embarrassing, so here it is. - -The routing scheme was designed for scalable name-independent routing on graphs with an internet-like topology. -By internet-like, I mean that the network has a densely connected core with many triangles, a diameter that increases slowly with network size, and where any sparse edges tend to be relatively tree-like, all of which appear to be common features of large graphs describing "organically" grown relationships. -By scalable name-independent routing, I mean: - -1. Scalable: resource consumption should grow slowly with the size of the network. -In particular, for internet-like networks, the goal is to use only a (poly)logarithmic amount of memory, use a logarithmic amount of bandwidth per one-hop neighbor for control traffic, and to maintain low average multiplicative path stretch (introducing overhead of perhaps a few percent) that does not become worse as the network grows. - -2. Name-independent: a node's identifier should be independent of network topology and state, such that a node may freely change their identifier in a static network, or keep it static under state changes in a dynamic network. -In particular, addresses are self-assigned and derived from a public key, which circumvents the use of a centralized addressing authority or public key infrastructure. - -Running this code will: - -1. Set up a `tun` device and assign it a Unique Local Address (ULA) in `fd00::/8`. -2. Connect to other nodes running the software. -3. Route traffic for and through other nodes. - -A device's ULA is actually from `fd00::/9`, and a matching `/64` prefix is available under `fd80::/9`. This allows the node to advertise a route on its LAN, as a workaround for unsupported devices. - -## Building - -1. Install Go (tested on 1.9, I use [godeb](https://github.com/niemeyer/godeb)). -2. Clone this repository. -2. `./build` - -It's written in Go because I felt like learning a new language, and Go seemed like an easy language to learn while still being a reasonable choice for language to prototype network code. -Note that the build script defines its own `$GOPATH`, so the build and its dependencies should be self contained. -It only works on Linux at this time, because a little code (related to the `tun` device) is platform dependent, and changing that hasn't been a high priority. - -## Running - -To run the program, you'll need permission to create a `tun` device and configure it using `ip`. -If you don't want to mess with capabilities for the `tun` device, then using `sudo` should work, with the usual security caveats about running a program as root. - -To run with default settings: - -1. `./yggdrasil --autoconf` - -That will generate a new set of keys (and an IP address) each time the program is run. -The program will bind to all addresses on a random port and listen for incoming connections. -It will send announcements over IPv6 link-local multicast, and attempt to start a connection if it hears an announcement from another device. - -In practice, you probably want to run this instead: - -1. `./yggdrasil --genconf > conf.json` -2. `./yggdrasil --useconf < conf.json` - -The first step generates a configuration file with a set of cryptographic keys and default settings. -The second step runs the program using the configuration provided in that file. -Because ULAs are derived from keys, using a fixed set of keys causes a node to keep the same address each time the program is run. - -If you want to use it as an overlay network on top of e.g. the internet, then you can do so by adding the address and port of the device you want to connect to (as a string, e.g. `"1.2.3.4:5678"`) to the list of `Peers` in the configuration file. -This should accept IPv4 and IPv6 addresses, and I think it should resolve host/domain names, but I haven't really tested that, so your mileage may vary. -You can also configure which address and/or port to listen on by editing the configuration file, in case you want to bind to a specific address or listen for incoming connections on a fixed port. - -Also note that the nodes is connected to the network through a `tun` device, so it follows point-to-point semantics. -This means it's limited to routing traffic with source and destination addresses in `fd00::/8`--you can't add a prefix to your routing table "via" an address in that range, as the router has no idea who you meant to send it to. -In particular, this means you can't set a working default route that *directly* uses the overlay network, but I've had success *indirectly* using it to connect to an off-the-shelf VPN that I can use as a default route for internet access. - -## Optional: advertise a prefix locally - -Suppose a node has been given the address: `fd00:1111:2222:3333:4444:5555:6666:7777` - -Then the node may also use addresses from the prefix: `fd80:1111:2222:3333::/64` (note the `fd00` -> `fd80`, a separate `/9` is used for prefixes). - -To advertise this prefix and a route to `fd00::/8`, the following seems to work for me: - -1. Enable IPv6 forwarding (e.g. `sysctl -w net.ipv6.conf.all.forwarding=1` or add it to sysctl.conf). - -2. `ip addr add fd80:1111:2222:3333::1/64 dev eth0` or similar, to assign an address for the router to use in that prefix, where the LAN is reachable through `eth0`. - -3. Install/run `radvd` with something like the following in `/etc/radvd.conf`: -``` -interface eth0 -{ - AdvSendAdvert on; - prefix fd80:1111:2222:3333::/64 { - AdvOnLink on; - AdvAutonomous on; - }; - route fd00::/8 {}; -}; -``` - -Now any IPv6-enabled device in the LAN can use stateless address auto-configuration to assign itself a working `fd00::/8` address from the `/64` prefix, and communicate with the wider network through the router, without requiring any special configuration for each device. -I've used this to e.g. get my phone on the network. -Note that there are a some differences when accessing the network this way: - -1. There are 64 fewer bits of address space available for self-certifying addresses. -This means that it is 64 bits easier to brute force a prefix collision than collision for a full node's IP address. As such, you may want to change addresses frequently, or else brute force an address with more security bits (see: `misc/genkeys.go`). - -2. The LAN depends on the router for cryptography. -So while traffic going through the WAN is encrypted, the LAN is still just a LAN. You may want to secure your network. - -3. Related to the above, the cryptography and I/O through the `tun` device both place additional load on the router, above what is normally present from forwarding packets between full nodes in the network, so the router may need more computing power to reach line rate. - -## How does it work? - -Consider the internet, which uses a network-of-networks model with address aggregation. -Addresses are allocated by a central authority, as blocks of contiguous addresses with a matching prefix. -Within a network, each node may represent one or more prefixes, with each prefix representing a network of one or more nodes. -On the largest scale, BGP is used to route traffic between networks (autonomous systems), and other protocols can be used to route within a network. -The effectiveness of such hierarchical addressing and routing strategies depend on network topology, with the internet's observed topology being the worst case of all known topologies from a scalability standpoint (see [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) for a better explanation of the issue, but the problem is essentially that address aggregation is ineffective in a network with a large number of nodes and a small diameter). - -The routing scheme implemented by this code tries a different approach. -Instead of using assigned addresses and a routing table based on prefixes and address aggregation, routing and addressing are handled through a combination of: - -1. Self-assigned cryptographically generated addresses, to handle address allocation without a central authority. -2. A kademlia-like distributed hash table, to look up a node's (name-dependent) routing information from their (name-independent routing) IP address. -3. A name-dependent routing scheme based on greedy routing in a metric space, constructed from an arbitrarily rooted spanning tree, which gives a reasonable approximation of the true distance between nodes for certain network topologies (namely the scale-free topology that seems to emerge in many large graphs, including the internet). The spanning tree embedding takes stability into account when selecting which one-hop neighbor to use as a parent, and path selection uses (poorly) estimated available bandwidth as a criteria, subject to the constraint that metric space distances must decrease with each hop. Incidentally, the name `yggdrasil` was selected for this test code because that's obviously what you call an immense tree that connects worlds. - -The network then presents itself as having a single "flat" address with no aggregation. -Under the hood, it runs as an overlay on top of existing IP networks. -Link-local IPv6 multicast traffic is used to advertise on the underlying networks, which can as easily be a wired or wireless LAN, a direct (e.g. ethernet) connection between two devices, a wireless ad-hoc network, etc. -Additional connections can be added manually to peer over networks where link-local multicast is insufficient, which allows you to e.g. use the internet to bridge local networks. - -The name-dependent routing layer uses cryptographically signed (`Ed25519`) path-vector-like routing messages, similar to S-BGP, which should prevent route poisoning and related attacks. -For encryption, it uses the Go implementation of the `nacl/box` scheme, which is built from a Curve25519 key exchange with XSalsa20 as a stream cypher and Poly1305 for integrity and authentication. -Permanent keys are used for protocol traffic, including the ephemeral key exchange, and a hash of a node's permanent public key is used to construct a node's address. -Ephemeral keys are used for encapsulated IP(v6) traffic, which provides forward secrecy. -Go's `crypto/rand` library is used for nonce generation. -In short, I've tried to not make this a complete security disaster, but the code hasn't been independently audited and I'm nothing close to a security expert, so it should be considered a proof-of-principle rather than a safe implementation. -At a minimum, I know of no way to prevent gray hole attacks. - -I realize that this is a terribly short description of how it works, so I may elaborate further in another document if the need arises. -Otherwise, I guess you could try to read my terrible and poorly documented code if you want to know more. - -## Related work - -A lot of inspiration comes from [cjdns](https://github.com/cjdelisle/cjdns). -I'm a contributor to that project, and I wanted to test out some ideas that weren't convenient to prototype in the existing code base, which is why I wrote this toy. - -On the routing side, a lot of influence came from compact routing. -A number of compact routing schemes are evaluated in [arxiv:0708.2309](https://arxiv.org/abs/0708.2309) and may be used as a basis for comparison. -When tested in a simplified simulation environment on CAIDA's 9204-node "skitter" network graph used in that paper, I observed an average multiplicative stretch of about 1.08 with my routing scheme, as implemented here. -This can be lowered to less than 1.02 using a source-routed version of the algorithm and including node degree as an additional parameter of the embedding, which is of academic interest, but degree's unverifiability makes it impractical for this implementation. -In either case, this only requires 1 routing table entry per one-hop neighbor (this averages ~6 for in the skitter network graph), plus a logarithmic number of DHT entries (expected to be ~26, based on extrapolations from networks with a few hundred nodes--running the full implementation on the skitter graph is impractical on my machine). -I don't think stretch is really an appropriate metric, as it doesn't consider the difference to total network cost from a high-stretch short path vs a high-stretch long path. -In this scheme, and I believe in most compact routing schemes, longer paths tend to have lower multiplicative stretch, and shorter paths are more likely to have longer stretch. -I would argue that this is preferable to the alternative. - -While I use a slightly different approach, the idea to try a greedy routing scheme was inspired by the use of greedy routing on networks embedded in the hyperbolic plane (such as [Kleinberg's work](https://doi.org/10.1109%2FINFCOM.2007.221) and [Greedy Forwarding on the NDN Testbed](https://www.caida.org/research/routing/greedy_forwarding_ndn/)). -I use distance on a spanning tree as the metric, as seems to work well on the types of networks I'm concerned with, and it simplifies other aspects of the implementation. -The hyperbolic embedding algorithms I'm aware of, or specifically the distributed ones, operate by constructing a spanning tree of the network and then embedding the tree. -So I don't see much harm, at present, of skipping the hyperbolic plane and directly using the tree for the metric space. - -## Misc. notes - -This is a toy experiment / proof-of-concept. -It's only meant to test if / how well some ideas work. -I have no idea what I'm doing, so for all I know it's entirely possible that it could crash your computer, eat your homework, or set fire to your house. -Some parts are also written to be as bad as I could make them while still being technically correct, in an effort to make bugs obvious if they occur, which means that even when it does work it may be fragile and error prone. - -In particular, you should expect it to perform poorly under mobility events, and to converge slowly in dynamic networks. All else being equal, this implementation should tend to prefer long-lived links over short-lived ones when embedding, and (poorly estimated) high bandwidth links over low bandwidth ones when forwarding traffic. As such, in multi-homed or mobile scenarios, there may be some tendency for it to make decisions you disagree with. - -While stretch is low on internet-like graphs, the best upper bound I've established on the *additive* stretch of this scheme, after convergence, is the same as for tree routing: proportional to network diameter. For sparse graphs with a large diameter, the scheme may not find particularly efficient paths, even under ideal circumstances. I would argue that such networks tend not to grow large enough for scalability to be an issue, so another routing scheme is better suited to those networks. - -Regarding the announce-able prefix thing, what I wanted to do is use `fc00::/7`, where `fc00::/8` is for nodes and `fd00::/8` is for prefixes. -I would also possibly widen the prefixes to `/48`, to match [rfc4193](https://tools.ietf.org/html/rfc4193), and possibly provide an option to keep using a `/64` by splitting it into two `/9` blocks (where `/64` prefixes would continue to live in `fd80::/9`), or else convince myself that the security implications of another 16 bits don't matter (to avoid the complexity of splitting it into two `/9` ranges for prefixes). -Using `fc00::/8` this way would cause issues if trying to also run cjdns. -Since I like cjdns, and want the option of running it on the same nodes, I've decided not to do that. -If I ever give up on avoiding cjdns conflicts, then I may change the addressing scheme to match the above. - -Despite the tree being constructed from path-vector-like routing messages, there's no support for routing policy right now. -As a result, peer relationships are bimodal: either you're not connected to someone, or you're connected and you'll route traffic *to* and *through* them. -Nodes also accept all incoming connections, so if you want to limit who can connect then you'll need to provide some other kind of access controls. - -The current implementation does all of its setup when the program starts, and then nothing can be reconfigured without restarting the program. -At some point I may add a remote API, so a running node can be reconfigured (to e.g. add/remove peers) without restarting, or probe the internal state of the router to get useful debugging info. -So far, things seem to work the way I want/expect without much trouble, so I haven't felt the need to do this yet. - -Some parts of the implementation can take advantage of multiple cores, but other parts that could simply do not. -Some parts are fast, but other parts are slower than they have any right to be, e.g. I can't figure out why some syscalls are as expensive as they are, so the `tun` in particular tends to be a CPU bottleneck (multi-queue could help in some cases, but that just spreads the cost around, and it doesn't help with single streams of traffic). -The Go runtime's GC tends to have short pauses, but it does have pauses. -So even if the ideas that went into this routing scheme turn out to be useful, this implementation is likely to remain mediocre at best for the foreseeable future. -If the is thing works well and the protocol stabilizes, then it's worth considering re-implementation and/or a formal spec and RFC. -In such a case, it's entirely reasonable to change parts of the spec purely to make the efficient implementation easier (e.g. it makes sense to want zero-copy networking, but a couple parts of the current protocol might make that impractical). - diff --git a/doc/Whitepaper.md b/doc/Whitepaper.md index fca9ffc2..674f6dc0 100644 --- a/doc/Whitepaper.md +++ b/doc/Whitepaper.md @@ -87,14 +87,17 @@ These signatures prevent nodes from forging arbitrary routing advertisements. The first hop, from the root, also includes a sequence number, which must be updated periodically. A node will blacklist the current root (keeping a record of the last sequence number observed) if the root fails to update for longer than some timeout (currently hard coded at 1 minute). Normally, a root node will update their sequence number for frequently than this (once every 30 seconds). -Nodes are throttled to ignore updates with a new sequence number for some period after updating their most recently seen sequence number (currently this cooldown is 10 seconds). +Nodes are throttled to ignore updates with a new sequence number for some period after updating their most recently seen sequence number (currently this cooldown is 15 seconds). The implementation chooses to set the sequence number equal to the unix time on the root's clock, so that a new (higher) sequence number will be selected if the root is restarted and the clock is not set back. Other than the root node, every other node in the network must select one of its neighbors to use as their parent. -This selection is done by maximizing: ` / `. -Here, `uptime` is the time between when we first and last received a message from the node which advertised the node's current location in the tree (resetting to zero if the location changes), and timeout is the time we wait before dropping a root due to inactivity. -This essentially means the numerator is at least as long as the amount of time between when the neighbor was first seen at its present location, and when the advertisement from the neighbor becomes invalid due to root timeout. -Resetting the uptime with each coordinate change causes nodes to favor long-lived stable paths over short-lived unstable ones, for the purposes of tree construction (indirectly impacting route selection). +This selection is done by tracking when each neighbor first sends us a message with a new timestamp from the root, to determine the ordering of the latency of each path from the root, to each neighbor, and then to the node that's searching for a parent. +These relative latencies are tracked by, for each neighbor, keeping a score vs each other neighbor. +If a neighbor sends a message with an updated timestamp before another neighbor, then the faster neighbor's score is increased by 1. +If the neighbor sends a message slower, then the score is decreased by 2, to make sure that a node must be reliably faster (at least 2/3 of the time) to see a net score increase over time. +If a node begins to advertise new coordinates, then its score vs all other nodes is reset to 0. +A node switches to a new parent if a neighbor's score (vs the current parent) reaches some threshold, currently 240, which corresponds to about 2 hours of being a reliably faster path. +The intended outcome of this process is that stable connections from fixed infrastructure near the "core" of the network should (eventually) select parents that minimize latency from the root to themselves, while the more dynamic parts of the network, presumably more towards the edges, will try to favor reliability when selecting a parent. The distance metric between nodes is simply the distance between the nodes if they routed on the spanning tree. This is equal to the sum of the distance from each node to the last common ancestor of the two nodes being compared. @@ -103,15 +106,14 @@ In practice, only the coords are used for routing, while the root and timestamp, ## Name-independent routing -A [Kademlia](https://en.wikipedia.org/wiki/Kademlia)-like Distributed Hash Table (DHT) is used as a distributed database that maps NodeIDs onto coordinates in the spanning tree metric space. -The DHT is Kademlia-like in that it uses the `xor` metric and structures the hash table into k-buckets (with 2 nodes per bucket in the normal case, plus some additional slots for keyspace neighbors and one-hop neighbors at the router level). -It differs from kademlia in that there are no values in the key:value store -- it only stores information about DHT peers. +A [Chord](https://en.wikipedia.org/wiki/Chord_(peer-to-peer))-like Distributed Hash Table (DHT) is used as a distributed database that maps NodeIDs onto coordinates in the spanning tree metric space. +The DHT is Chord-like in that it uses a successor/predecessor structure to do lookups in `O(n)` time with `O(1)` entries, then augments this with some additional information, adding roughly `O(logn)` additional entries, to reduce the lookup time to something around `O(logn)`. +In the long term, the idea is to favor spending our bandwidth making sure the minimum `O(1)` part is right, to prioritize correctness, and then try to conserve bandwidth (and power) by being a bit lazy about checking the remaining `O(logn)` portion when it's not in use. -The main complication is that, when the DHT is bootstrapped off of a node's one-hop neighbors, with no special measures taken about which nodes are included in each bucket, then the network may diverge (settle into a stable bad state, where at least some lookups will always fail). -The current strategy is to place additional preferences on which nodes are kept in each bucket -- in particular, we try to keep the closest nodes in xor space in each bucket. -This seems to mitigate the issue in some quick tests, but it's a topic that could use additional study. - -Other than these differences, the DHT is more-or-less what you might expect from a kad implementation. +To be specific, the DHT stores the immediate successor of a node, plus the next node it manages to find which is strictly closer (by the tree hop-count metric) than all previous nodes. +The same process is repeated for predecessor nodes, and lookups walk the network in the predecessor direction, with each key being owned by its successor (to make sure defaulting to 0 for unknown bits of a `NodeID` doesn't cause us to overshoot the target during a lookup). +In addition, all of a node's one-hop neighbors are included in the DHT, since we get this information "for free", and we must include it in our DHT to ensure that the network doesn't diverge to a broken state (though I suspect that only adding parents or parent-child relationships may be sufficient -- worth trying to prove or disprove, if somebody's bored). +The DHT differs from Chord in that there are no values in the key:value store -- it only stores information about DHT peers -- and that it uses a [Kademlia](https://en.wikipedia.org/wiki/Kademlia)-inspired iterative-parallel lookup process. To summarize the entire routing procedure, when given only a node's IP address, the goal is to find a route to the destination. That happens through 3 steps: diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..db6d359a --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/yggdrasil-network/yggdrasil-go + +require ( + github.com/docker/libcontainer v2.2.1+incompatible + github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0 + github.com/mitchellh/mapstructure v1.1.2 + github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165 + github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 + github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae + golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 + golang.org/x/net v0.0.0-20181207154023-610586996380 + golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e + golang.org/x/text v0.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..7d064112 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/docker/libcontainer v2.2.1+incompatible h1:++SbbkCw+X8vAd4j2gOCzZ2Nn7s2xFALTf7LZKmM1/0= +github.com/docker/libcontainer v2.2.1+incompatible/go.mod h1:osvj61pYsqhNCMLGX31xr7klUBhHb/ZBuXS0o1Fvwbw= +github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0 h1:YnZmFjg0Nvk8851WTVWlqMC1ecJH07Ctz+Ezxx4u54g= +github.com/kardianos/minwinsvc v0.0.0-20151122163309-cad6b2b879b0/go.mod h1:rUi0/YffDo1oXBOGn1KRq7Fr07LX48XEBecQnmwjsAo= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165 h1:Oo7Yfu5lEQLGvvh2p9Z8FRHJIsl7fdOCK9xXFNBkqmQ= +github.com/neilalexander/hjson-go v0.0.0-20180509131856-23267a251165/go.mod h1:l+Zao6IpQ+6d/y7LnYnOfbfOeU/9xRiTi4HLVpnkcTg= +github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w= +github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY= +github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae h1:MYCANF1kehCG6x6G+/9txLfq6n3lS5Vp0Mxn1hdiBAc= +github.com/yggdrasil-network/water v0.0.0-20180615095340-f732c88f34ae/go.mod h1:R0SBCsugm+Sf1katgTb2t7GXMm+nRIv43tM4VDZbaOs= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/net v0.0.0-20181207154023-610586996380 h1:zPQexyRtNYBc7bcHmehl1dH6TB3qn8zytv8cBGLDNY0= +golang.org/x/net v0.0.0-20181207154023-610586996380/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo= +golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/misc/genkeys.go b/misc/genkeys.go index 6e2df3f9..e995805f 100644 --- a/misc/genkeys.go +++ b/misc/genkeys.go @@ -16,7 +16,7 @@ import "encoding/hex" import "flag" import "fmt" import "runtime" -import . "yggdrasil" +import . "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil" var doSig = flag.Bool("sig", false, "generate new signing keys instead") diff --git a/misc/sim/run-sim b/misc/sim/run-sim index 985fe2f3..14057e81 100755 --- a/misc/sim/run-sim +++ b/misc/sim/run-sim @@ -1,4 +1,2 @@ #!/bin/bash -export GOPATH=$PWD -go get -d yggdrasil -go run -tags debug misc/sim/treesim.go +go run -tags debug misc/sim/treesim.go "$@" diff --git a/misc/sim/treesim.go b/misc/sim/treesim.go index c5c67e79..f4cd75fa 100644 --- a/misc/sim/treesim.go +++ b/misc/sim/treesim.go @@ -8,10 +8,11 @@ import "strconv" import "time" import "log" +import "runtime" import "runtime/pprof" import "flag" -import . "yggdrasil" +import . "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil" //////////////////////////////////////////////////////////////////////////////// @@ -267,6 +268,7 @@ func pingNodes(store map[[32]byte]*Node) { copy(packet[8:24], sourceAddr) copy(packet[24:40], destAddr) copy(packet[40:], bs) + packet[0] = 6 << 4 source.send <- packet } destCount := 0 @@ -279,17 +281,7 @@ func pingNodes(store map[[32]byte]*Node) { } destAddr := dest.core.DEBUG_getAddr()[:] ticker := time.NewTicker(150 * time.Millisecond) - ch := make(chan bool, 1) - ch <- true - doTicker := func() { - for range ticker.C { - select { - case ch <- true: - default: - } - } - } - go doTicker() + sendTo(payload, destAddr) for loop := true; loop; { select { case packet := <-dest.recv: @@ -298,8 +290,9 @@ func pingNodes(store map[[32]byte]*Node) { loop = false } } - case <-ch: + case <-ticker.C: sendTo(payload, destAddr) + //dumpDHTSize(store) // note that this uses racey functions to read things... } } ticker.Stop() @@ -386,7 +379,7 @@ func (n *Node) startTCP(listen string) { } func (n *Node) connectTCP(remoteAddr string) { - n.core.AddPeer(remoteAddr) + n.core.AddPeer(remoteAddr, remoteAddr) } //////////////////////////////////////////////////////////////////////////////// @@ -437,7 +430,7 @@ func main() { pingNodes(kstore) //pingBench(kstore) // Only after disabling debug output //stressTest(kstore) - //time.Sleep(120*time.Second) + //time.Sleep(120 * time.Second) dumpDHTSize(kstore) // note that this uses racey functions to read things... if false { // This connects the sim to the local network @@ -456,4 +449,5 @@ func main() { var block chan struct{} <-block } + runtime.GC() } diff --git a/src/yggdrasil/config/config.go b/src/config/config.go similarity index 67% rename from src/yggdrasil/config/config.go rename to src/config/config.go index bcf4f322..9671eca3 100644 --- a/src/yggdrasil/config/config.go +++ b/src/config/config.go @@ -3,9 +3,9 @@ package config // NodeConfig defines all configuration values needed to run a signle yggdrasil node type NodeConfig struct { Listen string `comment:"Listen address for peer connections. Default is to listen for all\nTCP connections over IPv4 and IPv6 with a random port."` - AdminListen string `comment:"Listen address for admin connections Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X."` - Peers []string `comment:"List of connection strings for static peers in URI format, i.e.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j."` - InterfacePeers map[string][]string `comment:"List of connection strings for static peers in URI format, arranged\nby source interface, i.e. { \"eth0\": [ tcp://a.b.c.d:e ] }. Note that\nSOCKS peerings will NOT be affected by this option and should go in\nthe \"Peers\" section instead."` + AdminListen string `comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."` + Peers []string `comment:"List of connection strings for static peers in URI format, e.g.\ntcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j."` + InterfacePeers map[string][]string `comment:"List of connection strings for static peers in URI format, arranged\nby source interface, e.g. { \"eth0\": [ tcp://a.b.c.d:e ] }. Note that\nSOCKS peerings will NOT be affected by this option and should go in\nthe \"Peers\" section instead."` ReadTimeout int32 `comment:"Read timeout for connections, specified in milliseconds. If less\nthan 6000 and not negative, 6000 (the default) is used. If negative,\nreads won't time out."` AllowedEncryptionPublicKeys []string `comment:"List of peer encryption public keys to allow or incoming TCP\nconnections from. If left empty/undefined then all connections\nwill be allowed by default."` EncryptionPublicKey string `comment:"Your public encryption key. Your peers may ask you for this to put\ninto their AllowedEncryptionPublicKeys configuration."` @@ -17,6 +17,8 @@ type NodeConfig struct { IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."` IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."` SessionFirewall SessionFirewall `comment:"The session firewall controls who can send/receive network traffic\nto/from. This is useful if you want to protect this node without\nresorting to using a real firewall. This does not affect traffic\nbeing routed via this node to somewhere else. Rules are prioritised as\nfollows: blacklist, whitelist, always allow outgoing, direct, remote."` + TunnelRouting TunnelRouting `comment:"Allow tunneling non-Yggdrasil traffic over Yggdrasil. This effectively\nallows you to use Yggdrasil to route to, or to bridge other networks,\nsimilar to a VPN tunnel. Tunnelling works between any two nodes and\ndoes not require them to be directly peered."` + SwitchOptions SwitchOptions `comment:"Advanced options for tuning the switch. Normally you will not need\nto edit these options."` //Net NetConfig `comment:"Extended options for connecting to peers over other networks."` } @@ -26,6 +28,7 @@ type NetConfig struct { I2P I2PConfig `comment:"Experimental options for configuring peerings over I2P."` } +// SessionFirewall controls the session firewall configuration type SessionFirewall struct { Enable bool `comment:"Enable or disable the session firewall. If disabled, network traffic\nfrom any node will be allowed. If enabled, the below rules apply."` AllowFromDirect bool `comment:"Allow network traffic from directly connected peers."` @@ -34,3 +37,17 @@ type SessionFirewall struct { WhitelistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always accepted,\nregardless of AllowFromDirect or AllowFromRemote."` BlacklistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always rejected,\nregardless of the whitelist, AllowFromDirect or AllowFromRemote."` } + +// TunnelRouting contains the crypto-key routing tables for tunneling +type TunnelRouting struct { + Enable bool `comment:"Enable or disable tunnel routing."` + IPv6Destinations map[string]string `comment:"IPv6 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"aaaa:bbbb:cccc::/e\": \"boxpubkey\", ... }"` + IPv6Sources []string `comment:"Optional IPv6 source subnets which are allowed to be tunnelled in\naddition to this node's Yggdrasil address/subnet. If not\nspecified, only traffic originating from this node's Yggdrasil\naddress or subnet will be tunnelled."` + IPv4Destinations map[string]string `comment:"IPv4 CIDR subnets, mapped to the EncryptionPublicKey to which they\nshould be routed, e.g. { \"a.b.c.d/e\": \"boxpubkey\", ... }"` + IPv4Sources []string `comment:"IPv4 source subnets which are allowed to be tunnelled. Unlike for\nIPv6, this option is required for bridging IPv4 traffic. Only\ntraffic with a source matching these subnets will be tunnelled."` +} + +// SwitchOptions contains tuning options for the switch +type SwitchOptions struct { + MaxTotalQueueSize uint64 `comment:"Maximum size of all switch queues combined (in bytes)."` +} diff --git a/src/yggdrasil/config/i2p.go b/src/config/i2p.go similarity index 100% rename from src/yggdrasil/config/i2p.go rename to src/config/i2p.go diff --git a/src/yggdrasil/config/tor.go b/src/config/tor.go similarity index 100% rename from src/yggdrasil/config/tor.go rename to src/config/tor.go diff --git a/src/yggdrasil/defaults/defaults.go b/src/defaults/defaults.go similarity index 85% rename from src/yggdrasil/defaults/defaults.go rename to src/defaults/defaults.go index 753efc53..3834990e 100644 --- a/src/yggdrasil/defaults/defaults.go +++ b/src/defaults/defaults.go @@ -7,6 +7,9 @@ type platformDefaultParameters struct { // Admin socket DefaultAdminListen string + // Configuration (used for yggdrasilctl) + DefaultConfigFile string + // TUN/TAP MaximumIfMTU int DefaultIfMTU int diff --git a/src/yggdrasil/defaults/defaults_darwin.go b/src/defaults/defaults_darwin.go similarity index 72% rename from src/yggdrasil/defaults/defaults_darwin.go rename to src/defaults/defaults_darwin.go index 778162c0..9bac3aad 100644 --- a/src/yggdrasil/defaults/defaults_darwin.go +++ b/src/defaults/defaults_darwin.go @@ -7,7 +7,10 @@ package defaults func GetDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin - DefaultAdminListen: "tcp://localhost:9001", + DefaultAdminListen: "unix:///var/run/yggdrasil.sock", + + // Configuration (used for yggdrasilctl) + DefaultConfigFile: "/etc/yggdrasil.conf", // TUN/TAP MaximumIfMTU: 65535, diff --git a/src/yggdrasil/defaults/defaults_freebsd.go b/src/defaults/defaults_freebsd.go similarity index 72% rename from src/yggdrasil/defaults/defaults_freebsd.go rename to src/defaults/defaults_freebsd.go index 7c5c7752..df1a3c65 100644 --- a/src/yggdrasil/defaults/defaults_freebsd.go +++ b/src/defaults/defaults_freebsd.go @@ -7,7 +7,10 @@ package defaults func GetDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin - DefaultAdminListen: "tcp://localhost:9001", + DefaultAdminListen: "unix:///var/run/yggdrasil.sock", + + // Configuration (used for yggdrasilctl) + DefaultConfigFile: "/etc/yggdrasil.conf", // TUN/TAP MaximumIfMTU: 32767, diff --git a/src/yggdrasil/defaults/defaults_linux.go b/src/defaults/defaults_linux.go similarity index 72% rename from src/yggdrasil/defaults/defaults_linux.go rename to src/defaults/defaults_linux.go index 85287eeb..2f3459ca 100644 --- a/src/yggdrasil/defaults/defaults_linux.go +++ b/src/defaults/defaults_linux.go @@ -7,7 +7,10 @@ package defaults func GetDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin - DefaultAdminListen: "tcp://localhost:9001", + DefaultAdminListen: "unix:///var/run/yggdrasil.sock", + + // Configuration (used for yggdrasilctl) + DefaultConfigFile: "/etc/yggdrasil.conf", // TUN/TAP MaximumIfMTU: 65535, diff --git a/src/yggdrasil/defaults/defaults_netbsd.go b/src/defaults/defaults_netbsd.go similarity index 72% rename from src/yggdrasil/defaults/defaults_netbsd.go rename to src/defaults/defaults_netbsd.go index 8e8f7b5f..40476dcb 100644 --- a/src/yggdrasil/defaults/defaults_netbsd.go +++ b/src/defaults/defaults_netbsd.go @@ -7,7 +7,10 @@ package defaults func GetDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin - DefaultAdminListen: "tcp://localhost:9001", + DefaultAdminListen: "unix:///var/run/yggdrasil.sock", + + // Configuration (used for yggdrasilctl) + DefaultConfigFile: "/etc/yggdrasil.conf", // TUN/TAP MaximumIfMTU: 9000, diff --git a/src/yggdrasil/defaults/defaults_openbsd.go b/src/defaults/defaults_openbsd.go similarity index 72% rename from src/yggdrasil/defaults/defaults_openbsd.go rename to src/defaults/defaults_openbsd.go index 8b3e2bbc..cd6d202a 100644 --- a/src/yggdrasil/defaults/defaults_openbsd.go +++ b/src/defaults/defaults_openbsd.go @@ -7,7 +7,10 @@ package defaults func GetDefaults() platformDefaultParameters { return platformDefaultParameters{ // Admin - DefaultAdminListen: "tcp://localhost:9001", + DefaultAdminListen: "unix:///var/run/yggdrasil.sock", + + // Configuration (used for yggdrasilctl) + DefaultConfigFile: "/etc/yggdrasil.conf", // TUN/TAP MaximumIfMTU: 16384, diff --git a/src/yggdrasil/defaults/defaults_other.go b/src/defaults/defaults_other.go similarity index 84% rename from src/yggdrasil/defaults/defaults_other.go rename to src/defaults/defaults_other.go index d780872b..a01ab7af 100644 --- a/src/yggdrasil/defaults/defaults_other.go +++ b/src/defaults/defaults_other.go @@ -9,6 +9,9 @@ func GetDefaults() platformDefaultParameters { // Admin DefaultAdminListen: "tcp://localhost:9001", + // Configuration (used for yggdrasilctl) + DefaultConfigFile: "/etc/yggdrasil.conf", + // TUN/TAP MaximumIfMTU: 65535, DefaultIfMTU: 65535, diff --git a/src/yggdrasil/defaults/defaults_windows.go b/src/defaults/defaults_windows.go similarity index 78% rename from src/yggdrasil/defaults/defaults_windows.go rename to src/defaults/defaults_windows.go index 83877d62..3b04783a 100644 --- a/src/yggdrasil/defaults/defaults_windows.go +++ b/src/defaults/defaults_windows.go @@ -9,6 +9,9 @@ func GetDefaults() platformDefaultParameters { // Admin DefaultAdminListen: "tcp://localhost:9001", + // Configuration (used for yggdrasilctl) + DefaultConfigFile: "C:\\Program Files\\Yggdrasil\\yggdrasil.conf", + // TUN/TAP MaximumIfMTU: 65535, DefaultIfMTU: 65535, diff --git a/src/yggdrasil/admin.go b/src/yggdrasil/admin.go index 9d3866f8..266d5a89 100644 --- a/src/yggdrasil/admin.go +++ b/src/yggdrasil/admin.go @@ -14,7 +14,7 @@ import ( "sync/atomic" "time" - "yggdrasil/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/defaults" ) // TODO: Add authentication @@ -52,12 +52,12 @@ func (a *admin) addHandler(name string, args []string, handler func(admin_info) func (a *admin) init(c *Core, listenaddr string) { a.core = c a.listenaddr = listenaddr - a.addHandler("help", nil, func(in admin_info) (admin_info, error) { + a.addHandler("list", []string{}, func(in admin_info) (admin_info, error) { handlers := make(map[string]interface{}) for _, handler := range a.handlers { handlers[handler.name] = admin_info{"fields": handler.args} } - return admin_info{"help": handlers}, nil + return admin_info{"list": handlers}, nil }) a.addHandler("dot", []string{}, func(in admin_info) (admin_info, error) { return admin_info{"dot": string(a.getResponse_dot())}, nil @@ -202,41 +202,133 @@ func (a *admin) init(c *Core, listenaddr string) { a.addHandler("getAllowedEncryptionPublicKeys", []string{}, func(in admin_info) (admin_info, error) { return admin_info{"allowed_box_pubs": a.getAllowedEncryptionPublicKeys()}, nil }) - a.addHandler("addAllowedEncryptionPublicKey", []string{"key"}, func(in admin_info) (admin_info, error) { - if a.addAllowedEncryptionPublicKey(in["key"].(string)) == nil { + a.addHandler("addAllowedEncryptionPublicKey", []string{"box_pub_key"}, func(in admin_info) (admin_info, error) { + if a.addAllowedEncryptionPublicKey(in["box_pub_key"].(string)) == nil { return admin_info{ "added": []string{ - in["key"].(string), + in["box_pub_key"].(string), }, }, nil } else { return admin_info{ "not_added": []string{ - in["key"].(string), + in["box_pub_key"].(string), }, }, errors.New("Failed to add allowed key") } }) - a.addHandler("removeAllowedEncryptionPublicKey", []string{"key"}, func(in admin_info) (admin_info, error) { - if a.removeAllowedEncryptionPublicKey(in["key"].(string)) == nil { + a.addHandler("removeAllowedEncryptionPublicKey", []string{"box_pub_key"}, func(in admin_info) (admin_info, error) { + if a.removeAllowedEncryptionPublicKey(in["box_pub_key"].(string)) == nil { return admin_info{ "removed": []string{ - in["key"].(string), + in["box_pub_key"].(string), }, }, nil } else { return admin_info{ "not_removed": []string{ - in["key"].(string), + in["box_pub_key"].(string), }, }, errors.New("Failed to remove allowed key") } }) + a.addHandler("addSourceSubnet", []string{"subnet"}, func(in admin_info) (admin_info, error) { + var err error + a.core.router.doAdmin(func() { + err = a.core.router.cryptokey.addSourceSubnet(in["subnet"].(string)) + }) + if err == nil { + return admin_info{"added": []string{in["subnet"].(string)}}, nil + } else { + return admin_info{"not_added": []string{in["subnet"].(string)}}, errors.New("Failed to add source subnet") + } + }) + a.addHandler("addRoute", []string{"subnet", "box_pub_key"}, func(in admin_info) (admin_info, error) { + var err error + a.core.router.doAdmin(func() { + err = a.core.router.cryptokey.addRoute(in["subnet"].(string), in["box_pub_key"].(string)) + }) + if err == nil { + return admin_info{"added": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, nil + } else { + return admin_info{"not_added": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, errors.New("Failed to add route") + } + }) + a.addHandler("getSourceSubnets", []string{}, func(in admin_info) (admin_info, error) { + var subnets []string + a.core.router.doAdmin(func() { + getSourceSubnets := func(snets []net.IPNet) { + for _, subnet := range snets { + subnets = append(subnets, subnet.String()) + } + } + getSourceSubnets(a.core.router.cryptokey.ipv4sources) + getSourceSubnets(a.core.router.cryptokey.ipv6sources) + }) + return admin_info{"source_subnets": subnets}, nil + }) + a.addHandler("getRoutes", []string{}, func(in admin_info) (admin_info, error) { + routes := make(admin_info) + a.core.router.doAdmin(func() { + getRoutes := func(ckrs []cryptokey_route) { + for _, ckr := range ckrs { + routes[ckr.subnet.String()] = hex.EncodeToString(ckr.destination[:]) + } + } + getRoutes(a.core.router.cryptokey.ipv4routes) + getRoutes(a.core.router.cryptokey.ipv6routes) + }) + return admin_info{"routes": routes}, nil + }) + a.addHandler("removeSourceSubnet", []string{"subnet"}, func(in admin_info) (admin_info, error) { + var err error + a.core.router.doAdmin(func() { + err = a.core.router.cryptokey.removeSourceSubnet(in["subnet"].(string)) + }) + if err == nil { + return admin_info{"removed": []string{in["subnet"].(string)}}, nil + } else { + return admin_info{"not_removed": []string{in["subnet"].(string)}}, errors.New("Failed to remove source subnet") + } + }) + a.addHandler("removeRoute", []string{"subnet", "box_pub_key"}, func(in admin_info) (admin_info, error) { + var err error + a.core.router.doAdmin(func() { + err = a.core.router.cryptokey.removeRoute(in["subnet"].(string), in["box_pub_key"].(string)) + }) + if err == nil { + return admin_info{"removed": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, nil + } else { + return admin_info{"not_removed": []string{fmt.Sprintf("%s via %s", in["subnet"].(string), in["box_pub_key"].(string))}}, errors.New("Failed to remove route") + } + }) + a.addHandler("dhtPing", []string{"box_pub_key", "coords", "[target]"}, func(in admin_info) (admin_info, error) { + if in["target"] == nil { + in["target"] = "none" + } + result, err := a.admin_dhtPing(in["box_pub_key"].(string), in["coords"].(string), in["target"].(string)) + if err == nil { + infos := make(map[string]map[string]string, len(result.Infos)) + for _, dinfo := range result.Infos { + info := map[string]string{ + "box_pub_key": hex.EncodeToString(dinfo.key[:]), + "coords": fmt.Sprintf("%v", dinfo.coords), + } + addr := net.IP(address_addrForNodeID(getNodeID(&dinfo.key))[:]).String() + infos[addr] = info + } + return admin_info{"nodes": infos}, nil + } else { + return admin_info{}, err + } + }) } // start runs the admin API socket to listen for / respond to admin API calls. func (a *admin) start() error { - go a.listen() + if a.listenaddr != "none" && a.listenaddr != "" { + go a.listen() + } return nil } @@ -251,7 +343,19 @@ func (a *admin) listen() { if err == nil { switch strings.ToLower(u.Scheme) { case "unix": + if _, err := os.Stat(a.listenaddr[7:]); err == nil { + a.core.log.Println("WARNING:", a.listenaddr[7:], "already exists and may be in use by another process") + } a.listener, err = net.Listen("unix", a.listenaddr[7:]) + if err == nil { + switch a.listenaddr[7:8] { + case "@": // maybe abstract namespace + default: + if err := os.Chmod(a.listenaddr[7:], 0660); err != nil { + a.core.log.Println("WARNING:", a.listenaddr[:7], "may have unsafe permissions!") + } + } + } case "tcp": a.listener, err = net.Listen("tcp", u.Host) default: @@ -386,7 +490,6 @@ func (n *admin_nodeInfo) toString() string { out = append(out, fmt.Sprintf("%v: %v", p.key, p.val)) } return strings.Join(out, ", ") - return fmt.Sprint(*n) } // printInfos returns a newline separated list of strings from admin_nodeInfos, e.g. a printable string of info about all peers. @@ -467,10 +570,18 @@ func (a *admin) getData_getSelf() *admin_nodeInfo { table := a.core.switchTable.table.Load().(lookupTable) coords := table.self.getCoords() self := admin_nodeInfo{ + {"box_pub_key", hex.EncodeToString(a.core.boxPub[:])}, {"ip", a.core.GetAddress().String()}, {"subnet", a.core.GetSubnet().String()}, {"coords", fmt.Sprint(coords)}, } + if name := GetBuildName(); name != "unknown" { + self = append(self, admin_pair{"build_name", name}) + } + if version := GetBuildVersion(); version != "unknown" { + self = append(self, admin_pair{"build_version", version}) + } + return &self } @@ -492,6 +603,8 @@ func (a *admin) getData_getPeers() []admin_nodeInfo { {"uptime", int(time.Since(p.firstSeen).Seconds())}, {"bytes_sent", atomic.LoadUint64(&p.bytesSent)}, {"bytes_recvd", atomic.LoadUint64(&p.bytesRecvd)}, + {"endpoint", p.endpoint}, + {"box_pub_key", hex.EncodeToString(p.box[:])}, } peerInfos = append(peerInfos, info) } @@ -516,6 +629,8 @@ func (a *admin) getData_getSwitchPeers() []admin_nodeInfo { {"port", elem.port}, {"bytes_sent", atomic.LoadUint64(&peer.bytesSent)}, {"bytes_recvd", atomic.LoadUint64(&peer.bytesRecvd)}, + {"endpoint", peer.endpoint}, + {"box_pub_key", hex.EncodeToString(peer.box[:])}, } peerInfos = append(peerInfos, info) } @@ -525,7 +640,7 @@ func (a *admin) getData_getSwitchPeers() []admin_nodeInfo { // getData_getSwitchQueues returns info from Core.switchTable for an queue data. func (a *admin) getData_getSwitchQueues() admin_nodeInfo { var peerInfos admin_nodeInfo - switchTable := a.core.switchTable + switchTable := &a.core.switchTable getSwitchQueues := func() { queues := make([]map[string]interface{}, 0) for k, v := range switchTable.queues.bufs { @@ -544,7 +659,7 @@ func (a *admin) getData_getSwitchQueues() admin_nodeInfo { {"queues_size", switchTable.queues.size}, {"highest_queues_count", switchTable.queues.maxbufs}, {"highest_queues_size", switchTable.queues.maxsize}, - {"maximum_queues_size", switch_buffer_maxSize}, + {"maximum_queues_size", switchTable.queueTotalMaxSize}, } } a.core.switchTable.doAdmin(getSwitchQueues) @@ -554,25 +669,24 @@ func (a *admin) getData_getSwitchQueues() admin_nodeInfo { // getData_getDHT returns info from Core.dht for an admin response. func (a *admin) getData_getDHT() []admin_nodeInfo { var infos []admin_nodeInfo - now := time.Now() getDHT := func() { - for i := 0; i < a.core.dht.nBuckets(); i++ { - b := a.core.dht.getBucket(i) - getInfo := func(vs []*dhtInfo, isPeer bool) { - for _, v := range vs { - addr := *address_addrForNodeID(v.getNodeID()) - info := admin_nodeInfo{ - {"ip", net.IP(addr[:]).String()}, - {"coords", fmt.Sprint(v.coords)}, - {"bucket", i}, - {"peer_only", isPeer}, - {"last_seen", int(now.Sub(v.recv).Seconds())}, - } - infos = append(infos, info) - } + now := time.Now() + var dhtInfos []*dhtInfo + for _, v := range a.core.dht.table { + dhtInfos = append(dhtInfos, v) + } + sort.SliceStable(dhtInfos, func(i, j int) bool { + return dht_ordered(&a.core.dht.nodeID, dhtInfos[i].getNodeID(), dhtInfos[j].getNodeID()) + }) + for _, v := range dhtInfos { + addr := *address_addrForNodeID(v.getNodeID()) + info := admin_nodeInfo{ + {"ip", net.IP(addr[:]).String()}, + {"coords", fmt.Sprint(v.coords)}, + {"last_seen", int(now.Sub(v.recv).Seconds())}, + {"box_pub_key", hex.EncodeToString(v.key[:])}, } - getInfo(b.other, false) - getInfo(b.peers, true) + infos = append(infos, info) } } a.core.router.doAdmin(getDHT) @@ -592,6 +706,7 @@ func (a *admin) getData_getSessions() []admin_nodeInfo { {"was_mtu_fixed", sinfo.wasMTUFixed}, {"bytes_sent", sinfo.bytesSent}, {"bytes_recvd", sinfo.bytesRecvd}, + {"box_pub_key", hex.EncodeToString(sinfo.theirPermPub[:])}, } infos = append(infos, info) } @@ -633,6 +748,64 @@ func (a *admin) removeAllowedEncryptionPublicKey(bstr string) (err error) { return } +// Send a DHT ping to the node with the provided key and coords, optionally looking up the specified target NodeID. +func (a *admin) admin_dhtPing(keyString, coordString, targetString string) (dhtRes, error) { + var key boxPubKey + if keyBytes, err := hex.DecodeString(keyString); err != nil { + return dhtRes{}, err + } else { + copy(key[:], keyBytes) + } + var coords []byte + for _, cstr := range strings.Split(strings.Trim(coordString, "[]"), " ") { + if cstr == "" { + // Special case, happens if trimmed is the empty string, e.g. this is the root + continue + } + if u64, err := strconv.ParseUint(cstr, 10, 8); err != nil { + return dhtRes{}, err + } else { + coords = append(coords, uint8(u64)) + } + } + resCh := make(chan *dhtRes, 1) + info := dhtInfo{ + key: key, + coords: coords, + } + target := *info.getNodeID() + if targetString == "none" { + // Leave the default target in place + } else if targetBytes, err := hex.DecodeString(targetString); err != nil { + return dhtRes{}, err + } else if len(targetBytes) != len(target) { + return dhtRes{}, errors.New("Incorrect target NodeID length") + } else { + target = NodeID{} + copy(target[:], targetBytes) + } + rq := dhtReqKey{info.key, target} + sendPing := func() { + a.core.dht.addCallback(&rq, func(res *dhtRes) { + defer func() { recover() }() + select { + case resCh <- res: + default: + } + }) + a.core.dht.ping(&info, &target) + } + a.core.router.doAdmin(sendPing) + go func() { + time.Sleep(6 * time.Second) + close(resCh) + }() + for res := range resCh { + return *res, nil + } + return dhtRes{}, errors.New(fmt.Sprintf("DHT ping timeout: %s", keyString)) +} + // getResponse_dot returns a response for a graphviz dot formatted representation of the known parts of the network. // This is color-coded and labeled, and includes the self node, switch peers, nodes known to the DHT, and nodes with open sessions. // The graph is structured as a tree with directed links leading away from the root. diff --git a/src/yggdrasil/ckr.go b/src/yggdrasil/ckr.go new file mode 100644 index 00000000..d88b8d3c --- /dev/null +++ b/src/yggdrasil/ckr.go @@ -0,0 +1,348 @@ +package yggdrasil + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "net" + "sort" +) + +// This module implements crypto-key routing, similar to Wireguard, where we +// allow traffic for non-Yggdrasil ranges to be routed over Yggdrasil. + +type cryptokey struct { + core *Core + enabled bool + ipv4routes []cryptokey_route + ipv6routes []cryptokey_route + ipv4cache map[address]cryptokey_route + ipv6cache map[address]cryptokey_route + ipv4sources []net.IPNet + ipv6sources []net.IPNet +} + +type cryptokey_route struct { + subnet net.IPNet + destination boxPubKey +} + +// Initialise crypto-key routing. This must be done before any other CKR calls. +func (c *cryptokey) init(core *Core) { + c.core = core + c.ipv4routes = make([]cryptokey_route, 0) + c.ipv6routes = make([]cryptokey_route, 0) + c.ipv4cache = make(map[address]cryptokey_route, 0) + c.ipv6cache = make(map[address]cryptokey_route, 0) + c.ipv4sources = make([]net.IPNet, 0) + c.ipv6sources = make([]net.IPNet, 0) +} + +// Enable or disable crypto-key routing. +func (c *cryptokey) setEnabled(enabled bool) { + c.enabled = enabled +} + +// Check if crypto-key routing is enabled. +func (c *cryptokey) isEnabled() bool { + return c.enabled +} + +// Check whether the given address (with the address length specified in bytes) +// matches either the current node's address, the node's routed subnet or the +// list of subnets specified in IPv4Sources/IPv6Sources. +func (c *cryptokey) isValidSource(addr address, addrlen int) bool { + ip := net.IP(addr[:addrlen]) + + if addrlen == net.IPv6len { + // Does this match our node's address? + if bytes.Equal(addr[:16], c.core.router.addr[:16]) { + return true + } + + // Does this match our node's subnet? + if bytes.Equal(addr[:8], c.core.router.subnet[:8]) { + return true + } + } + + // Does it match a configured CKR source? + if c.isEnabled() { + // Build our references to the routing sources + var routingsources *[]net.IPNet + + // Check if the prefix is IPv4 or IPv6 + if addrlen == net.IPv6len { + routingsources = &c.ipv6sources + } else if addrlen == net.IPv4len { + routingsources = &c.ipv4sources + } else { + return false + } + + for _, subnet := range *routingsources { + if subnet.Contains(ip) { + return true + } + } + } + + // Doesn't match any of the above + return false +} + +// Adds a source subnet, which allows traffic with these source addresses to +// be tunnelled using crypto-key routing. +func (c *cryptokey) addSourceSubnet(cidr string) error { + // Is the CIDR we've been given valid? + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + + // Get the prefix length and size + _, prefixsize := ipnet.Mask.Size() + + // Build our references to the routing sources + var routingsources *[]net.IPNet + + // Check if the prefix is IPv4 or IPv6 + if prefixsize == net.IPv6len*8 { + routingsources = &c.ipv6sources + } else if prefixsize == net.IPv4len*8 { + routingsources = &c.ipv4sources + } else { + return errors.New("Unexpected prefix size") + } + + // Check if we already have this CIDR + for _, subnet := range *routingsources { + if subnet.String() == ipnet.String() { + return errors.New("Source subnet already configured") + } + } + + // Add the source subnet + *routingsources = append(*routingsources, *ipnet) + c.core.log.Println("Added CKR source subnet", cidr) + return nil +} + +// Adds a destination route for the given CIDR to be tunnelled to the node +// with the given BoxPubKey. +func (c *cryptokey) addRoute(cidr string, dest string) error { + // Is the CIDR we've been given valid? + ipaddr, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + + // Get the prefix length and size + _, prefixsize := ipnet.Mask.Size() + + // Build our references to the routing table and cache + var routingtable *[]cryptokey_route + var routingcache *map[address]cryptokey_route + + // Check if the prefix is IPv4 or IPv6 + if prefixsize == net.IPv6len*8 { + routingtable = &c.ipv6routes + routingcache = &c.ipv6cache + } else if prefixsize == net.IPv4len*8 { + routingtable = &c.ipv4routes + routingcache = &c.ipv4cache + } else { + return errors.New("Unexpected prefix size") + } + + // Is the route an Yggdrasil destination? + var addr address + var snet subnet + copy(addr[:], ipaddr) + copy(snet[:], ipnet.IP) + if addr.isValid() || snet.isValid() { + return errors.New("Can't specify Yggdrasil destination as crypto-key route") + } + // Do we already have a route for this subnet? + for _, route := range *routingtable { + if route.subnet.String() == ipnet.String() { + return errors.New(fmt.Sprintf("Route already exists for %s", cidr)) + } + } + // Decode the public key + if bpk, err := hex.DecodeString(dest); err != nil { + return err + } else if len(bpk) != boxPubKeyLen { + return errors.New(fmt.Sprintf("Incorrect key length for %s", dest)) + } else { + // Add the new crypto-key route + var key boxPubKey + copy(key[:], bpk) + *routingtable = append(*routingtable, cryptokey_route{ + subnet: *ipnet, + destination: key, + }) + + // Sort so most specific routes are first + sort.Slice(*routingtable, func(i, j int) bool { + im, _ := (*routingtable)[i].subnet.Mask.Size() + jm, _ := (*routingtable)[j].subnet.Mask.Size() + return im > jm + }) + + // Clear the cache as this route might change future routing + // Setting an empty slice keeps the memory whereas nil invokes GC + for k := range *routingcache { + delete(*routingcache, k) + } + + c.core.log.Println("Added CKR destination subnet", cidr) + return nil + } +} + +// Looks up the most specific route for the given address (with the address +// length specified in bytes) from the crypto-key routing table. An error is +// returned if the address is not suitable or no route was found. +func (c *cryptokey) getPublicKeyForAddress(addr address, addrlen int) (boxPubKey, error) { + // Check if the address is a valid Yggdrasil address - if so it + // is exempt from all CKR checking + if addr.isValid() { + return boxPubKey{}, errors.New("Cannot look up CKR for Yggdrasil addresses") + } + + // Build our references to the routing table and cache + var routingtable *[]cryptokey_route + var routingcache *map[address]cryptokey_route + + // Check if the prefix is IPv4 or IPv6 + if addrlen == net.IPv6len { + routingtable = &c.ipv6routes + routingcache = &c.ipv6cache + } else if addrlen == net.IPv4len { + routingtable = &c.ipv4routes + routingcache = &c.ipv4cache + } else { + return boxPubKey{}, errors.New("Unexpected prefix size") + } + + // Check if there's a cache entry for this addr + if route, ok := (*routingcache)[addr]; ok { + return route.destination, nil + } + + // No cache was found - start by converting the address into a net.IP + ip := make(net.IP, addrlen) + copy(ip[:addrlen], addr[:]) + + // Check if we have a route. At this point c.ipv6routes should be + // pre-sorted so that the most specific routes are first + for _, route := range *routingtable { + // Does this subnet match the given IP? + if route.subnet.Contains(ip) { + // Check if the routing cache is above a certain size, if it is evict + // a random entry so we can make room for this one. We take advantage + // of the fact that the iteration order is random here + for k := range *routingcache { + if len(*routingcache) < 1024 { + break + } + delete(*routingcache, k) + } + + // Cache the entry for future packets to get a faster lookup + (*routingcache)[addr] = route + + // Return the boxPubKey + return route.destination, nil + } + } + + // No route was found if we got to this point + return boxPubKey{}, errors.New(fmt.Sprintf("No route to %s", ip.String())) +} + +// Removes a source subnet, which allows traffic with these source addresses to +// be tunnelled using crypto-key routing. +func (c *cryptokey) removeSourceSubnet(cidr string) error { + // Is the CIDR we've been given valid? + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + + // Get the prefix length and size + _, prefixsize := ipnet.Mask.Size() + + // Build our references to the routing sources + var routingsources *[]net.IPNet + + // Check if the prefix is IPv4 or IPv6 + if prefixsize == net.IPv6len*8 { + routingsources = &c.ipv6sources + } else if prefixsize == net.IPv4len*8 { + routingsources = &c.ipv4sources + } else { + return errors.New("Unexpected prefix size") + } + + // Check if we already have this CIDR + for idx, subnet := range *routingsources { + if subnet.String() == ipnet.String() { + *routingsources = append((*routingsources)[:idx], (*routingsources)[idx+1:]...) + c.core.log.Println("Removed CKR source subnet", cidr) + return nil + } + } + return errors.New("Source subnet not found") +} + +// Removes a destination route for the given CIDR to be tunnelled to the node +// with the given BoxPubKey. +func (c *cryptokey) removeRoute(cidr string, dest string) error { + // Is the CIDR we've been given valid? + _, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return err + } + + // Get the prefix length and size + _, prefixsize := ipnet.Mask.Size() + + // Build our references to the routing table and cache + var routingtable *[]cryptokey_route + var routingcache *map[address]cryptokey_route + + // Check if the prefix is IPv4 or IPv6 + if prefixsize == net.IPv6len*8 { + routingtable = &c.ipv6routes + routingcache = &c.ipv6cache + } else if prefixsize == net.IPv4len*8 { + routingtable = &c.ipv4routes + routingcache = &c.ipv4cache + } else { + return errors.New("Unexpected prefix size") + } + + // Decode the public key + bpk, err := hex.DecodeString(dest) + if err != nil { + return err + } else if len(bpk) != boxPubKeyLen { + return errors.New(fmt.Sprintf("Incorrect key length for %s", dest)) + } + netStr := ipnet.String() + + for idx, route := range *routingtable { + if bytes.Equal(route.destination[:], bpk) && route.subnet.String() == netStr { + *routingtable = append((*routingtable)[:idx], (*routingtable)[idx+1:]...) + for k := range *routingcache { + delete(*routingcache, k) + } + c.core.log.Printf("Removed CKR destination subnet %s via %s\n", cidr, dest) + return nil + } + } + return errors.New(fmt.Sprintf("Route does not exists for %s", cidr)) +} diff --git a/src/yggdrasil/core.go b/src/yggdrasil/core.go index 015147c4..5ab91a74 100644 --- a/src/yggdrasil/core.go +++ b/src/yggdrasil/core.go @@ -8,10 +8,13 @@ import ( "net" "regexp" - "yggdrasil/config" - "yggdrasil/defaults" + "github.com/yggdrasil-network/yggdrasil-go/src/config" + "github.com/yggdrasil-network/yggdrasil-go/src/defaults" ) +var buildName string +var buildVersion string + // The Core object represents the Yggdrasil node. You should create a Core // object for each Yggdrasil node you plan to run. type Core struct { @@ -22,7 +25,6 @@ type Core struct { sigPriv sigPrivKey switchTable switchTable peers peers - sigs sigManager sessions sessions router router dht dht @@ -50,7 +52,6 @@ func (c *Core) init(bpub *boxPubKey, c.boxPub, c.boxPriv = *bpub, *bpriv c.sigPub, c.sigPriv = *spub, *spriv c.admin.core = c - c.sigs.init() c.searches.init(c) c.dht.init(c) c.sessions.init(c) @@ -61,12 +62,38 @@ func (c *Core) init(bpub *boxPubKey, c.tun.init(c) } +// Get the current build name. This is usually injected if built from git, +// or returns "unknown" otherwise. +func GetBuildName() string { + if buildName == "" { + return "unknown" + } + return buildName +} + +// Get the current build version. This is usually injected if built from git, +// or returns "unknown" otherwise. +func GetBuildVersion() string { + if buildVersion == "" { + return "unknown" + } + return buildVersion +} + // Starts up Yggdrasil using the provided NodeConfig, and outputs debug logging // through the provided log.Logger. The started stack will include TCP and UDP // sockets, a multicast discovery socket, an admin socket, router, switch and // DHT node. func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error { c.log = log + + if name := GetBuildName(); name != "unknown" { + c.log.Println("Build name:", name) + } + if version := GetBuildVersion(); version != "unknown" { + c.log.Println("Build version:", version) + } + c.log.Println("Starting up...") var boxPub boxPubKey @@ -102,6 +129,10 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error { return err } + if nc.SwitchOptions.MaxTotalQueueSize >= SwitchQueueTotalMinSize { + c.switchTable.queueTotalMaxSize = nc.SwitchOptions.MaxTotalQueueSize + } + if err := c.switchTable.start(); err != nil { c.log.Println("Failed to start switch") return err @@ -121,6 +152,31 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error { return err } + c.router.cryptokey.setEnabled(nc.TunnelRouting.Enable) + if c.router.cryptokey.isEnabled() { + c.log.Println("Crypto-key routing enabled") + for ipv6, pubkey := range nc.TunnelRouting.IPv6Destinations { + if err := c.router.cryptokey.addRoute(ipv6, pubkey); err != nil { + panic(err) + } + } + for _, source := range nc.TunnelRouting.IPv6Sources { + if c.router.cryptokey.addSourceSubnet(source); err != nil { + panic(err) + } + } + for ipv4, pubkey := range nc.TunnelRouting.IPv4Destinations { + if err := c.router.cryptokey.addRoute(ipv4, pubkey); err != nil { + panic(err) + } + } + for _, source := range nc.TunnelRouting.IPv4Sources { + if c.router.cryptokey.addSourceSubnet(source); err != nil { + panic(err) + } + } + } + if err := c.admin.start(); err != nil { c.log.Println("Failed to start admin socket") return err diff --git a/src/yggdrasil/debug.go b/src/yggdrasil/debug.go index 892529b6..e463518d 100644 --- a/src/yggdrasil/debug.go +++ b/src/yggdrasil/debug.go @@ -22,7 +22,7 @@ import "net/http" import "runtime" import "os" -import "yggdrasil/defaults" +import "github.com/yggdrasil-network/yggdrasil-go/src/defaults" // Start the profiler in debug builds, if the required environment variable is set. func init() { @@ -84,7 +84,7 @@ func (c *Core) DEBUG_getPeers() *peers { func (ps *peers) DEBUG_newPeer(box boxPubKey, sig sigPubKey, link boxSharedKey) *peer { //in <-chan []byte, //out chan<- []byte) *peer { - return ps.newPeer(&box, &sig, &link) //, in, out) + return ps.newPeer(&box, &sig, &link, "(simulator)") //, in, out) } /* @@ -229,12 +229,10 @@ func DEBUG_wire_encode_coords(coords []byte) []byte { // DHT, via core func (c *Core) DEBUG_getDHTSize() int { - total := 0 - for bidx := 0; bidx < c.dht.nBuckets(); bidx++ { - b := c.dht.getBucket(bidx) - total += len(b.peers) - total += len(b.other) - } + var total int + c.router.doAdmin(func() { + total = len(c.dht.table) + }) return total } @@ -506,27 +504,43 @@ func (c *Core) DEBUG_addAllowedEncryptionPublicKey(boxStr string) { func DEBUG_simLinkPeers(p, q *peer) { // Sets q.out() to point to p and starts p.linkLoop() - p.linkOut, q.linkOut = make(chan []byte, 1), make(chan []byte, 1) - go func() { - for bs := range p.linkOut { - q.handlePacket(bs) + goWorkers := func(source, dest *peer) { + source.linkOut = make(chan []byte, 1) + send := make(chan []byte, 1) + source.out = func(bs []byte) { + send <- bs } - }() - go func() { - for bs := range q.linkOut { - p.handlePacket(bs) - } - }() - p.out = func(bs []byte) { - p.core.switchTable.idleIn <- p.port - go q.handlePacket(bs) + go source.linkLoop() + go func() { + var packets [][]byte + for { + select { + case packet := <-source.linkOut: + packets = append(packets, packet) + continue + case packet := <-send: + packets = append(packets, packet) + source.core.switchTable.idleIn <- source.port + continue + default: + } + if len(packets) > 0 { + dest.handlePacket(packets[0]) + packets = packets[1:] + continue + } + select { + case packet := <-source.linkOut: + packets = append(packets, packet) + case packet := <-send: + packets = append(packets, packet) + source.core.switchTable.idleIn <- source.port + } + } + }() } - q.out = func(bs []byte) { - q.core.switchTable.idleIn <- q.port - go p.handlePacket(bs) - } - go p.linkLoop() - go q.linkLoop() + goWorkers(p, q) + goWorkers(q, p) p.core.switchTable.idleIn <- p.port q.core.switchTable.idleIn <- q.port } diff --git a/src/yggdrasil/dht.go b/src/yggdrasil/dht.go index 955ef839..aa694f7a 100644 --- a/src/yggdrasil/dht.go +++ b/src/yggdrasil/dht.go @@ -1,38 +1,15 @@ package yggdrasil -/* - -This part has the (kademlia-like) distributed hash table - -It's used to look up coords for a NodeID - -Every node participates in the DHT, and the DHT stores no real keys/values -(Only the peer relationships / lookups are needed) - -This version is intentionally fragile, by being recursive instead of iterative -(it's also not parallel, as a result) -This is to make sure that DHT black holes are visible if they exist -(the iterative parallel approach tends to get around them sometimes) -I haven't seen this get stuck on blackholes, but I also haven't proven it can't -Slight changes *do* make it blackhole hard, bootstrapping isn't an easy problem - -*/ +// A chord-like Distributed Hash Table (DHT). +// Used to look up coords given a NodeID and bitmask (taken from an IPv6 address). +// Keeps track of immediate successor, predecessor, and all peers. +// Also keeps track of other nodes if they're closer in tree space than all other known nodes encountered when heading in either direction to that point, under the hypothesis that, for the kinds of networks we care about, this should probabilistically include the node needed to keep lookups to near O(logn) steps. import ( "sort" "time" ) -// Number of DHT buckets, equal to the number of bits in a NodeID. -// Note that, in practice, nearly all of these will be empty. -const dht_bucket_number = 8 * NodeIDLen - -// Number of nodes to keep in each DHT bucket. -// Additional entries may be kept for peers, for bootstrapping reasons, if they don't already have an entry in the bucket. -const dht_bucket_size = 2 - -// Number of responses to include in a lookup. -// If extras are given, they will be truncated from the response handler to prevent abuse. const dht_lookup_size = 16 // dhtInfo represents everything we know about a node in the DHT. @@ -41,11 +18,9 @@ type dhtInfo struct { nodeID_hidden *NodeID key boxPubKey coords []byte - send time.Time // When we last sent a message - recv time.Time // When we last received a message - pings int // Decide when to drop - throttle time.Duration // Time to wait before pinging a node to bootstrap buckets, increases exponentially from 1 second to 1 minute - bootstrapSend time.Time // The time checked/updated as part of throttle checks + recv time.Time // When we last received a message + pings int // Time out if at least 3 consecutive maintenance pings drop + throttle time.Duration } // Returns the *NodeID associated with dhtInfo.key, calculating it on the fly the first time or from a cache all subsequent times. @@ -56,12 +31,6 @@ func (info *dhtInfo) getNodeID() *NodeID { return info.nodeID_hidden } -// The nodes we known in a bucket (a region of keyspace with a matching prefix of some length). -type bucket struct { - peers []*dhtInfo - other []*dhtInfo -} - // Request for a node to do a lookup. // Includes our key and coords so they can send a response back, and the destination NodeID we want to ask about. type dhtReq struct { @@ -74,30 +43,28 @@ type dhtReq struct { // Includes the key and coords of the node that's responding, and the destination they were asked about. // The main part is Infos []*dhtInfo, the lookup response. type dhtRes struct { - Key boxPubKey // key to respond to - Coords []byte // coords to respond to + Key boxPubKey // key of the sender + Coords []byte // coords of the sender Dest NodeID Infos []*dhtInfo // response } -// Information about a node, either taken from our table or from a lookup response. -// Used to schedule pings at a later time (they're throttled to 1/second for background maintenance traffic). -type dht_rumor struct { - info *dhtInfo - target *NodeID +// Parts of a DHT req usable as a key in a map. +type dhtReqKey struct { + key boxPubKey + dest NodeID } // The main DHT struct. -// Includes a slice of buckets, to organize known nodes based on their region of keyspace. -// Also includes information about outstanding DHT requests and the rumor mill of nodes to ping at some point. type dht struct { - core *Core - nodeID NodeID - buckets_hidden [dht_bucket_number]bucket // Extra is for the self-bucket - peers chan *dhtInfo // other goroutines put incoming dht updates here - reqs map[boxPubKey]map[NodeID]time.Time - offset int - rumorMill []dht_rumor + core *Core + nodeID NodeID + peers chan *dhtInfo // other goroutines put incoming dht updates here + reqs map[dhtReqKey]time.Time // Keeps track of recent outstanding requests + callbacks map[dhtReqKey]dht_callbackInfo // Search and admin lookup callbacks + // These next two could be replaced by a single linked list or similar... + table map[NodeID]*dhtInfo + imp []*dhtInfo } // Initializes the DHT. @@ -105,11 +72,98 @@ func (t *dht) init(c *Core) { t.core = c t.nodeID = *t.core.GetNodeID() t.peers = make(chan *dhtInfo, 1024) - t.reqs = make(map[boxPubKey]map[NodeID]time.Time) + t.callbacks = make(map[dhtReqKey]dht_callbackInfo) + t.reset() +} + +// Resets the DHT in response to coord changes. +// This empties all info from the DHT and drops outstanding requests. +func (t *dht) reset() { + t.reqs = make(map[dhtReqKey]time.Time) + t.table = make(map[NodeID]*dhtInfo) + t.imp = nil +} + +// Does a DHT lookup and returns up to dht_lookup_size results. +func (t *dht) lookup(nodeID *NodeID, everything bool) []*dhtInfo { + results := make([]*dhtInfo, 0, len(t.table)) + for _, info := range t.table { + results = append(results, info) + } + if len(results) > dht_lookup_size { + // Drop the middle part, so we keep some nodes before and after. + // This should help to bootstrap / recover more quickly. + sort.SliceStable(results, func(i, j int) bool { + return dht_ordered(nodeID, results[i].getNodeID(), results[j].getNodeID()) + }) + newRes := make([]*dhtInfo, 0, len(results)) + newRes = append(newRes, results[len(results)-dht_lookup_size/2:]...) + newRes = append(newRes, results[:len(results)-dht_lookup_size/2]...) + results = newRes + results = results[:dht_lookup_size] + } + return results +} + +// Insert into table, preserving the time we last sent a packet if the node was already in the table, otherwise setting that time to now. +func (t *dht) insert(info *dhtInfo) { + if *info.getNodeID() == t.nodeID { + // This shouldn't happen, but don't add it if it does + return + } + info.recv = time.Now() + if oldInfo, isIn := t.table[*info.getNodeID()]; isIn { + sameCoords := true + if len(info.coords) != len(oldInfo.coords) { + sameCoords = false + } else { + for idx := 0; idx < len(info.coords); idx++ { + if info.coords[idx] != oldInfo.coords[idx] { + sameCoords = false + break + } + } + } + if sameCoords { + info.throttle = oldInfo.throttle + } + } + t.imp = nil // It needs to update to get a pointer to the new info + t.table[*info.getNodeID()] = info +} + +// Return true if first/second/third are (partially) ordered correctly. +func dht_ordered(first, second, third *NodeID) bool { + lessOrEqual := func(first, second *NodeID) bool { + for idx := 0; idx < NodeIDLen; idx++ { + if first[idx] > second[idx] { + return false + } + if first[idx] < second[idx] { + return true + } + } + return true + } + firstLessThanSecond := lessOrEqual(first, second) + secondLessThanThird := lessOrEqual(second, third) + thirdLessThanFirst := lessOrEqual(third, first) + switch { + case firstLessThanSecond && secondLessThanThird: + // Nothing wrapped around 0, the easy case + return true + case thirdLessThanFirst && firstLessThanSecond: + // Third wrapped around 0 + return true + case secondLessThanThird && thirdLessThanFirst: + // Second (and third) wrapped around 0 + return true + } + return false } // Reads a request, performs a lookup, and responds. -// If the node that sent the request isn't in our DHT, but should be, then we add them. +// Update info about the node that sent the request. func (t *dht) handleReq(req *dhtReq) { // Send them what they asked for loc := t.core.switchTable.getLocator() @@ -121,273 +175,14 @@ func (t *dht) handleReq(req *dhtReq) { Infos: t.lookup(&req.Dest, false), } t.sendRes(&res, req) - // Also (possibly) add them to our DHT + // Also add them to our DHT info := dhtInfo{ key: req.Key, coords: req.Coords, } - // For bootstrapping to work, we need to add these nodes to the table - // Using insertIfNew, they can lie about coords, but searches will route around them - // Using the mill would mean trying to block off the mill becomes an attack vector - t.insertIfNew(&info, false) -} - -// Reads a lookup response, checks that we had sent a matching request, and processes the response info. -// This mainly consists of updating the node we asked in our DHT (they responded, so we know they're still alive), and adding the response info to the rumor mill. -func (t *dht) handleRes(res *dhtRes) { - t.core.searches.handleDHTRes(res) - reqs, isIn := t.reqs[res.Key] - if !isIn { - return + if _, isIn := t.table[*info.getNodeID()]; !isIn && t.isImportant(&info) { + t.ping(&info, nil) } - _, isIn = reqs[res.Dest] - if !isIn { - return - } - delete(reqs, res.Dest) - now := time.Now() - rinfo := dhtInfo{ - key: res.Key, - coords: res.Coords, - send: now, // Technically wrong but should be OK... - recv: now, - throttle: time.Second, - bootstrapSend: now, - } - // If they're already in the table, then keep the correct send time - bidx, isOK := t.getBucketIndex(rinfo.getNodeID()) - if !isOK { - return - } - b := t.getBucket(bidx) - for _, oldinfo := range b.peers { - if oldinfo.key == rinfo.key { - rinfo.send = oldinfo.send - rinfo.throttle = oldinfo.throttle - rinfo.bootstrapSend = oldinfo.bootstrapSend - } - } - for _, oldinfo := range b.other { - if oldinfo.key == rinfo.key { - rinfo.send = oldinfo.send - rinfo.throttle = oldinfo.throttle - rinfo.bootstrapSend = oldinfo.bootstrapSend - } - } - // Insert into table - t.insert(&rinfo, false) - if res.Dest == *rinfo.getNodeID() { - return - } // No infinite recursions - if len(res.Infos) > dht_lookup_size { - // Ignore any "extra" lookup results - res.Infos = res.Infos[:dht_lookup_size] - } - for _, info := range res.Infos { - if dht_firstCloserThanThird(info.getNodeID(), &res.Dest, rinfo.getNodeID()) { - t.addToMill(info, info.getNodeID()) - } - } -} - -// Does a DHT lookup and returns the results, sorted in ascending order of distance from the destination. -func (t *dht) lookup(nodeID *NodeID, allowCloser bool) []*dhtInfo { - // FIXME this allocates a bunch, sorts, and keeps the part it likes - // It would be better to only track the part it likes to begin with - addInfos := func(res []*dhtInfo, infos []*dhtInfo) []*dhtInfo { - for _, info := range infos { - if info == nil { - panic("Should never happen!") - } - if allowCloser || dht_firstCloserThanThird(info.getNodeID(), nodeID, &t.nodeID) { - res = append(res, info) - } - } - return res - } - var res []*dhtInfo - for bidx := 0; bidx < t.nBuckets(); bidx++ { - b := t.getBucket(bidx) - res = addInfos(res, b.peers) - res = addInfos(res, b.other) - } - doSort := func(infos []*dhtInfo) { - less := func(i, j int) bool { - return dht_firstCloserThanThird(infos[i].getNodeID(), - nodeID, - infos[j].getNodeID()) - } - sort.SliceStable(infos, less) - } - doSort(res) - if len(res) > dht_lookup_size { - res = res[:dht_lookup_size] - } - return res -} - -// Gets the bucket for a specified matching prefix length. -func (t *dht) getBucket(bidx int) *bucket { - return &t.buckets_hidden[bidx] -} - -// Lists the number of buckets. -func (t *dht) nBuckets() int { - return len(t.buckets_hidden) -} - -// Inserts a node into the DHT if they meet certain requirements. -// In particular, they must either be a peer that's not already in the DHT, or else be someone we should insert into the DHT (see: shouldInsert). -func (t *dht) insertIfNew(info *dhtInfo, isPeer bool) { - // Insert if no "other" entry already exists - nodeID := info.getNodeID() - bidx, isOK := t.getBucketIndex(nodeID) - if !isOK { - return - } - b := t.getBucket(bidx) - if (isPeer && !b.containsOther(info)) || t.shouldInsert(info) { - // We've never heard this node before - // TODO is there a better time than "now" to set send/recv to? - // (Is there another "natural" choice that bootstraps faster?) - info.send = time.Now() - info.recv = info.send - t.insert(info, isPeer) - } -} - -// Adds a node to the DHT, possibly removing another node in the process. -func (t *dht) insert(info *dhtInfo, isPeer bool) { - // First update the time on this info - info.recv = time.Now() - // Get the bucket for this node - nodeID := info.getNodeID() - bidx, isOK := t.getBucketIndex(nodeID) - if !isOK { - return - } - b := t.getBucket(bidx) - if !isPeer && !b.containsOther(info) { - // This is a new entry, give it an old age so it's pinged sooner - // This speeds up bootstrapping - info.recv = info.recv.Add(-time.Hour) - } - if isPeer || info.throttle > time.Minute { - info.throttle = time.Minute - } - // First drop any existing entry from the bucket - b.drop(&info.key) - // Now add to the *end* of the bucket - if isPeer { - // TODO make sure we don't duplicate peers in b.other too - b.peers = append(b.peers, info) - return - } - b.other = append(b.other, info) - // Shrink from the *front* to requied size - for len(b.other) > dht_bucket_size { - b.other = b.other[1:] - } -} - -// Gets the bucket index for the bucket where we would put the given NodeID. -func (t *dht) getBucketIndex(nodeID *NodeID) (int, bool) { - for bidx := 0; bidx < t.nBuckets(); bidx++ { - them := nodeID[bidx/8] & (0x80 >> byte(bidx%8)) - me := t.nodeID[bidx/8] & (0x80 >> byte(bidx%8)) - if them != me { - return bidx, true - } - } - return t.nBuckets(), false -} - -// Helper called by containsPeer, containsOther, and contains. -// Returns true if a node with the same ID *and coords* is already in the given part of the bucket. -func dht_bucket_check(newInfo *dhtInfo, infos []*dhtInfo) bool { - // Compares if key and coords match - if newInfo == nil { - panic("Should never happen") - } - for _, info := range infos { - if info == nil { - panic("Should never happen") - } - if info.key != newInfo.key { - continue - } - if len(info.coords) != len(newInfo.coords) { - continue - } - match := true - for idx := 0; idx < len(info.coords); idx++ { - if info.coords[idx] != newInfo.coords[idx] { - match = false - break - } - } - if match { - return true - } - } - return false -} - -// Calls bucket_check over the bucket's peers infos. -func (b *bucket) containsPeer(info *dhtInfo) bool { - return dht_bucket_check(info, b.peers) -} - -// Calls bucket_check over the bucket's other info. -func (b *bucket) containsOther(info *dhtInfo) bool { - return dht_bucket_check(info, b.other) -} - -// returns containsPeer || containsOther -func (b *bucket) contains(info *dhtInfo) bool { - return b.containsPeer(info) || b.containsOther(info) -} - -// Removes a node with the corresponding key, if any, from a bucket. -func (b *bucket) drop(key *boxPubKey) { - clean := func(infos []*dhtInfo) []*dhtInfo { - cleaned := infos[:0] - for _, info := range infos { - if info.key == *key { - continue - } - cleaned = append(cleaned, info) - } - return cleaned - } - b.peers = clean(b.peers) - b.other = clean(b.other) -} - -// Sends a lookup request to the specified node. -func (t *dht) sendReq(req *dhtReq, dest *dhtInfo) { - // Send a dhtReq to the node in dhtInfo - bs := req.encode() - shared := t.core.sessions.getSharedKey(&t.core.boxPriv, &dest.key) - payload, nonce := boxSeal(shared, bs, nil) - p := wire_protoTrafficPacket{ - Coords: dest.coords, - ToKey: dest.key, - FromKey: t.core.boxPub, - Nonce: *nonce, - Payload: payload, - } - packet := p.encode() - t.core.router.out(packet) - reqsToDest, isIn := t.reqs[dest.key] - if !isIn { - t.reqs[dest.key] = make(map[NodeID]time.Time) - reqsToDest, isIn = t.reqs[dest.key] - if !isIn { - panic("This should never happen") - } - } - reqsToDest[req.Dest] = time.Now() } // Sends a lookup response to the specified node. @@ -407,56 +202,73 @@ func (t *dht) sendRes(res *dhtRes, req *dhtReq) { t.core.router.out(packet) } -// Returns true of a bucket contains no peers and no other nodes. -func (b *bucket) isEmpty() bool { - return len(b.peers)+len(b.other) == 0 +type dht_callbackInfo struct { + f func(*dhtRes) + time time.Time } -// Gets the next node that should be pinged from the bucket. -// There's a cooldown of 6 seconds between ping attempts for each node, to give them time to respond. -// It returns the least recently pinged node, subject to that send cooldown. -func (b *bucket) nextToPing() *dhtInfo { - // Check the nodes in the bucket - // Return whichever one responded least recently - // Delay of 6 seconds between pinging the same node - // Gives them time to respond - // And time between traffic loss from short term congestion in the network - var toPing *dhtInfo - update := func(infos []*dhtInfo) { - for _, next := range infos { - if time.Since(next.send) < 6*time.Second { - continue - } - if toPing == nil || next.recv.Before(toPing.recv) { - toPing = next - } - } +// Adds a callback and removes it after some timeout. +func (t *dht) addCallback(rq *dhtReqKey, callback func(*dhtRes)) { + info := dht_callbackInfo{callback, time.Now().Add(6 * time.Second)} + t.callbacks[*rq] = info +} + +// Reads a lookup response, checks that we had sent a matching request, and processes the response info. +// This mainly consists of updating the node we asked in our DHT (they responded, so we know they're still alive), and deciding if we want to do anything with their responses +func (t *dht) handleRes(res *dhtRes) { + rq := dhtReqKey{res.Key, res.Dest} + if callback, isIn := t.callbacks[rq]; isIn { + callback.f(res) + delete(t.callbacks, rq) } - update(b.peers) - update(b.other) - return toPing -} - -// Returns a useful target address to ask about for pings. -// Equal to the our node's ID, except for exactly 1 bit at the bucket index. -func (t *dht) getTarget(bidx int) *NodeID { - targetID := t.nodeID - targetID[bidx/8] ^= 0x80 >> byte(bidx%8) - return &targetID -} - -// Sends a ping to a node, or removes the node if it has failed to respond to too many pings. -// If target is nil, we will ask the node about our own NodeID. -func (t *dht) ping(info *dhtInfo, target *NodeID) { - if info.pings > 2 { - bidx, isOK := t.getBucketIndex(info.getNodeID()) - if !isOK { - panic("This should never happen") - } - b := t.getBucket(bidx) - b.drop(&info.key) + _, isIn := t.reqs[rq] + if !isIn { return } + delete(t.reqs, rq) + rinfo := dhtInfo{ + key: res.Key, + coords: res.Coords, + } + if t.isImportant(&rinfo) { + t.insert(&rinfo) + } + for _, info := range res.Infos { + if *info.getNodeID() == t.nodeID { + continue + } // Skip self + if _, isIn := t.table[*info.getNodeID()]; isIn { + // TODO? don't skip if coords are different? + continue + } + if t.isImportant(info) { + t.ping(info, nil) + } + } +} + +// Sends a lookup request to the specified node. +func (t *dht) sendReq(req *dhtReq, dest *dhtInfo) { + // Send a dhtReq to the node in dhtInfo + bs := req.encode() + shared := t.core.sessions.getSharedKey(&t.core.boxPriv, &dest.key) + payload, nonce := boxSeal(shared, bs, nil) + p := wire_protoTrafficPacket{ + Coords: dest.coords, + ToKey: dest.key, + FromKey: t.core.boxPub, + Nonce: *nonce, + Payload: payload, + } + packet := p.encode() + t.core.router.out(packet) + rq := dhtReqKey{dest.key, req.Dest} + t.reqs[rq] = time.Now() +} + +// Sends a lookup to this info, looking for the target. +func (t *dht) ping(info *dhtInfo, target *NodeID) { + // Creates a req for the node at dhtInfo, asking them about the target (if one is given) or themself (if no target is given) if target == nil { target = &t.nodeID } @@ -467,160 +279,120 @@ func (t *dht) ping(info *dhtInfo, target *NodeID) { Coords: coords, Dest: *target, } - info.pings++ - info.send = time.Now() t.sendReq(&req, info) } -// Adds a node info and target to the rumor mill. -// The node will be asked about the target at a later point, if doing so would still be useful at the time. -func (t *dht) addToMill(info *dhtInfo, target *NodeID) { - rumor := dht_rumor{ - info: info, - target: target, - } - t.rumorMill = append(t.rumorMill, rumor) -} - -// Regular periodic maintenance. -// If the mill is empty, it adds two pings to the rumor mill. -// The first is to the node that responded least recently, provided that it's been at least 1 minute, to make sure we eventually detect and remove unresponsive nodes. -// The second is used for bootstrapping, and attempts to fill some bucket, iterating over buckets and resetting after it hits the last non-empty one. -// If the mill is not empty, it pops nodes from the mill until it finds one that would be useful to ping (see: shouldInsert), and then pings it. +// Periodic maintenance work to keep important DHT nodes alive. func (t *dht) doMaintenance() { - // First clean up reqs - for key, reqs := range t.reqs { - for target, timeout := range reqs { - if time.Since(timeout) > time.Minute { - delete(reqs, target) - } - } - if len(reqs) == 0 { - delete(t.reqs, key) + now := time.Now() + newReqs := make(map[dhtReqKey]time.Time, len(t.reqs)) + for key, start := range t.reqs { + if now.Sub(start) < 6*time.Second { + newReqs[key] = start } } - if len(t.rumorMill) == 0 { - // Ping the least recently contacted node - // This is to make sure we eventually notice when someone times out - var oldest *dhtInfo - last := 0 - for bidx := 0; bidx < t.nBuckets(); bidx++ { - b := t.getBucket(bidx) - if !b.isEmpty() { - last = bidx - toPing := b.nextToPing() - if toPing == nil { - continue - } // We've recently pinged everyone in b - if oldest == nil || toPing.recv.Before(oldest.recv) { - oldest = toPing - } - } + t.reqs = newReqs + newCallbacks := make(map[dhtReqKey]dht_callbackInfo, len(t.callbacks)) + for key, callback := range t.callbacks { + if now.Before(callback.time) { + newCallbacks[key] = callback } - if oldest != nil && time.Since(oldest.recv) > time.Minute { - // Ping the oldest node in the DHT, but don't ping nodes that have been checked within the last minute - t.addToMill(oldest, nil) - } - // Refresh buckets - if t.offset > last { - t.offset = 0 - } - target := t.getTarget(t.offset) - func() { - closer := t.lookup(target, false) - for _, info := range closer { - // Throttled ping of a node that's closer to the destination - if time.Since(info.recv) > info.throttle { - t.addToMill(info, target) - t.offset++ - info.bootstrapSend = time.Now() - info.throttle *= 2 - if info.throttle > time.Minute { - info.throttle = time.Minute - } - return - } - } - if len(closer) == 0 { - // If we don't know of anyone closer at all, then there's a hole in our dht - // Ping the closest node we know and ignore the throttle, to try to fill it - for _, info := range t.lookup(target, true) { - t.addToMill(info, target) - t.offset++ - return - } - } - }() - //t.offset++ } - for len(t.rumorMill) > 0 { - var rumor dht_rumor - rumor, t.rumorMill = t.rumorMill[0], t.rumorMill[1:] - if rumor.target == rumor.info.getNodeID() { - // Note that the above is a pointer comparison, and target can be nil - // This is only for adding new nodes (learned from other lookups) - // It only makes sense to ping if the node isn't already in the table - if !t.shouldInsert(rumor.info) { - continue + t.callbacks = newCallbacks + for infoID, info := range t.table { + if now.Sub(info.recv) > time.Minute || info.pings > 3 { + delete(t.table, infoID) + t.imp = nil + } + } + for _, info := range t.getImportant() { + if now.Sub(info.recv) > info.throttle { + t.ping(info, nil) + info.pings++ + info.throttle += time.Second + if info.throttle > 30*time.Second { + info.throttle = 30 * time.Second } } - t.ping(rumor.info, rumor.target) - break } } -// Returns true if it would be worth pinging the specified node. -// This requires that the bucket doesn't already contain the node, and that either the bucket isn't full yet or the node is closer to us in keyspace than some other node in that bucket. -func (t *dht) shouldInsert(info *dhtInfo) bool { - bidx, isOK := t.getBucketIndex(info.getNodeID()) - if !isOK { +// Gets a list of important nodes, used by isImportant. +func (t *dht) getImportant() []*dhtInfo { + if t.imp == nil { + // Get a list of all known nodes + infos := make([]*dhtInfo, 0, len(t.table)) + for _, info := range t.table { + infos = append(infos, info) + } + // Sort them by increasing order in distance along the ring + sort.SliceStable(infos, func(i, j int) bool { + // Sort in order of predecessors (!), reverse from chord normal, because it plays nicer with zero bits for unknown parts of target addresses + return dht_ordered(infos[j].getNodeID(), infos[i].getNodeID(), &t.nodeID) + }) + // Keep the ones that are no further than the closest seen so far + minDist := ^uint64(0) + loc := t.core.switchTable.getLocator() + important := infos[:0] + for _, info := range infos { + dist := uint64(loc.dist(info.coords)) + if dist < minDist { + minDist = dist + important = append(important, info) + } + } + var temp []*dhtInfo + minDist = ^uint64(0) + for idx := len(infos) - 1; idx >= 0; idx-- { + info := infos[idx] + dist := uint64(loc.dist(info.coords)) + if dist < minDist { + minDist = dist + temp = append(temp, info) + } + } + for idx := len(temp) - 1; idx >= 0; idx-- { + important = append(important, temp[idx]) + } + t.imp = important + } + return t.imp +} + +// Returns true if this is a node we need to keep track of for the DHT to work. +func (t *dht) isImportant(ninfo *dhtInfo) bool { + if ninfo.key == t.core.boxPub { return false } - b := t.getBucket(bidx) - if b.containsOther(info) { - return false - } - if len(b.other) < dht_bucket_size { - return true - } - for _, other := range b.other { - if dht_firstCloserThanThird(info.getNodeID(), &t.nodeID, other.getNodeID()) { + important := t.getImportant() + // Check if ninfo is of equal or greater importance to what we already know + loc := t.core.switchTable.getLocator() + ndist := uint64(loc.dist(ninfo.coords)) + minDist := ^uint64(0) + for _, info := range important { + if (*info.getNodeID() == *ninfo.getNodeID()) || + (ndist < minDist && dht_ordered(info.getNodeID(), ninfo.getNodeID(), &t.nodeID)) { + // Either the same node, or a better one return true } + dist := uint64(loc.dist(info.coords)) + if dist < minDist { + minDist = dist + } } + minDist = ^uint64(0) + for idx := len(important) - 1; idx >= 0; idx-- { + info := important[idx] + if (*info.getNodeID() == *ninfo.getNodeID()) || + (ndist < minDist && dht_ordered(&t.nodeID, ninfo.getNodeID(), info.getNodeID())) { + // Either the same node, or a better one + return true + } + dist := uint64(loc.dist(info.coords)) + if dist < minDist { + minDist = dist + } + } + // We didn't find any important node that ninfo is better than return false } - -// Returns true if the keyspace distance between the first and second node is smaller than the keyspace distance between the second and third node. -func dht_firstCloserThanThird(first *NodeID, - second *NodeID, - third *NodeID) bool { - for idx := 0; idx < NodeIDLen; idx++ { - f := first[idx] ^ second[idx] - t := third[idx] ^ second[idx] - if f == t { - continue - } - return f < t - } - return false -} - -// Resets the DHT in response to coord changes. -// This empties all buckets, resets the bootstrapping cycle to 0, and empties the rumor mill. -// It adds all old "other" node info to the rumor mill, so they'll be pinged quickly. -// If those nodes haven't also changed coords, then this is a relatively quick way to notify those nodes of our new coords and re-add them to our own DHT if they respond. -func (t *dht) reset() { - // This is mostly so bootstrapping will reset to resend coords into the network - t.offset = 0 - t.rumorMill = nil // reset mill - for _, b := range t.buckets_hidden { - b.peers = b.peers[:0] - for _, info := range b.other { - // Add other nodes to the rumor mill so they'll be pinged soon - // This will hopefully tell them our coords and re-learn theirs quickly if they haven't changed - t.addToMill(info, info.getNodeID()) - } - b.other = b.other[:0] - } -} diff --git a/src/yggdrasil/icmpv6.go b/src/yggdrasil/icmpv6.go index 0491f880..957b192e 100644 --- a/src/yggdrasil/icmpv6.go +++ b/src/yggdrasil/icmpv6.go @@ -13,6 +13,7 @@ import ( "encoding/binary" "errors" "net" + "time" "golang.org/x/net/icmp" "golang.org/x/net/ipv6" @@ -23,11 +24,17 @@ type macAddress [6]byte const len_ETHER = 14 type icmpv6 struct { - tun *tunDevice - peermac macAddress - peerlladdr net.IP - mylladdr net.IP - mymac macAddress + tun *tunDevice + mylladdr net.IP + mymac macAddress + peermacs map[address]neighbor +} + +type neighbor struct { + mac macAddress + learned bool + lastadvertisement time.Time + lastsolicitation time.Time } // Marshal returns the binary encoding of h. @@ -52,13 +59,16 @@ func ipv6Header_Marshal(h *ipv6.Header) ([]byte, error) { // addresses. func (i *icmpv6) init(t *tunDevice) { i.tun = t + i.peermacs = make(map[address]neighbor) // Our MAC address and link-local address - copy(i.mymac[:], []byte{ - 0x02, 0x00, 0x00, 0x00, 0x00, 0x02}) + i.mymac = macAddress{ + 0x02, 0x00, 0x00, 0x00, 0x00, 0x02} i.mylladdr = net.IP{ 0xFE, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xFE} + copy(i.mymac[:], i.tun.core.router.addr[:]) + copy(i.mylladdr[9:], i.tun.core.router.addr[1:]) } // Parses an incoming ICMPv6 packet. The packet provided may be either an @@ -73,7 +83,7 @@ func (i *icmpv6) parse_packet(datain []byte) { if i.tun.iface.IsTAP() { response, err = i.parse_packet_tap(datain) } else { - response, err = i.parse_packet_tun(datain) + response, err = i.parse_packet_tun(datain, nil) } if err != nil { @@ -89,16 +99,14 @@ func (i *icmpv6) parse_packet(datain []byte) { // A response buffer is also created for the response message, also complete // with ethernet headers. func (i *icmpv6) parse_packet_tap(datain []byte) ([]byte, error) { - // Store the peer MAC address - copy(i.peermac[:6], datain[6:12]) - // Ignore non-IPv6 frames if binary.BigEndian.Uint16(datain[12:14]) != uint16(0x86DD) { return nil, nil } // Hand over to parse_packet_tun to interpret the IPv6 packet - ipv6packet, err := i.parse_packet_tun(datain[len_ETHER:]) + mac := datain[6:12] + ipv6packet, err := i.parse_packet_tun(datain[len_ETHER:], &mac) if err != nil { return nil, err } @@ -120,7 +128,7 @@ func (i *icmpv6) parse_packet_tap(datain []byte) ([]byte, error) { // sanity checks on the packet - i.e. is the packet an ICMPv6 packet, does the // ICMPv6 message match a known expected type. The relevant handler function // is then called and a response packet may be returned. -func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) { +func (i *icmpv6) parse_packet_tun(datain []byte, datamac *[]byte) ([]byte, error) { // Parse the IPv6 packet headers ipv6Header, err := ipv6.ParseHeader(datain[:ipv6.HeaderLen]) if err != nil { @@ -137,9 +145,6 @@ func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) { return nil, err } - // Store the peer link local address, it will come in useful later - copy(i.peerlladdr[:], ipv6Header.Src[:]) - // Parse the ICMPv6 message contents icmpv6Header, err := icmp.ParseMessage(58, datain[ipv6.HeaderLen:]) if err != nil { @@ -149,24 +154,35 @@ func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) { // Check for a supported message type switch icmpv6Header.Type { case ipv6.ICMPTypeNeighborSolicitation: - { - response, err := i.handle_ndp(datain[ipv6.HeaderLen:]) - if err == nil { - // Create our ICMPv6 response - responsePacket, err := i.create_icmpv6_tun( - ipv6Header.Src, i.mylladdr, - ipv6.ICMPTypeNeighborAdvertisement, 0, - &icmp.DefaultMessageBody{Data: response}) - if err != nil { - return nil, err - } - - // Send it back - return responsePacket, nil - } else { + response, err := i.handle_ndp(datain[ipv6.HeaderLen:]) + if err == nil { + // Create our ICMPv6 response + responsePacket, err := i.create_icmpv6_tun( + ipv6Header.Src, i.mylladdr, + ipv6.ICMPTypeNeighborAdvertisement, 0, + &icmp.DefaultMessageBody{Data: response}) + if err != nil { return nil, err } + + // Send it back + return responsePacket, nil + } else { + return nil, err } + case ipv6.ICMPTypeNeighborAdvertisement: + if datamac != nil { + var addr address + var mac macAddress + copy(addr[:], ipv6Header.Src[:]) + copy(mac[:], (*datamac)[:]) + neighbor := i.peermacs[addr] + neighbor.mac = mac + neighbor.learned = true + neighbor.lastadvertisement = time.Now() + i.peermacs[addr] = neighbor + } + return nil, errors.New("No response needed") } return nil, errors.New("ICMPv6 type not matched") @@ -238,6 +254,42 @@ func (i *icmpv6) create_icmpv6_tun(dst net.IP, src net.IP, mtype ipv6.ICMPType, return responsePacket, nil } +func (i *icmpv6) create_ndp_tap(dst address) ([]byte, error) { + // Create the ND payload + var payload [28]byte + copy(payload[:4], []byte{0x00, 0x00, 0x00, 0x00}) + copy(payload[4:20], dst[:]) + copy(payload[20:22], []byte{0x01, 0x01}) + copy(payload[22:28], i.mymac[:6]) + + // Create the ICMPv6 solicited-node address + var dstaddr address + copy(dstaddr[:13], []byte{ + 0xFF, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0xFF}) + copy(dstaddr[13:], dst[13:16]) + + // Create the multicast MAC + var dstmac macAddress + copy(dstmac[:2], []byte{0x33, 0x33}) + copy(dstmac[2:6], dstaddr[12:16]) + + // Create the ND request + requestPacket, err := i.create_icmpv6_tap( + dstmac, dstaddr[:], i.mylladdr, + ipv6.ICMPTypeNeighborSolicitation, 0, + &icmp.DefaultMessageBody{Data: payload[:]}) + if err != nil { + return nil, err + } + neighbor := i.peermacs[dstaddr] + neighbor.lastsolicitation = time.Now() + i.peermacs[dstaddr] = neighbor + + return requestPacket, nil +} + // Generates a response to an NDP discovery packet. This is effectively called // when the host operating system generates an NDP request for any address in // the fd00::/8 range, so that the operating system knows to route that traffic diff --git a/src/yggdrasil/multicast.go b/src/yggdrasil/multicast.go index 697744cb..749dfcdb 100644 --- a/src/yggdrasil/multicast.go +++ b/src/yggdrasil/multicast.go @@ -1,6 +1,7 @@ package yggdrasil import ( + "context" "fmt" "net" "time" @@ -35,7 +36,10 @@ func (m *multicast) start() error { return err } listenString := fmt.Sprintf("[::]:%v", addr.Port) - conn, err := net.ListenPacket("udp6", listenString) + lc := net.ListenConfig{ + Control: multicastReuse, + } + conn, err := lc.ListenPacket(context.Background(), "udp6", listenString) if err != nil { return err } diff --git a/src/yggdrasil/multicast_other.go b/src/yggdrasil/multicast_other.go new file mode 100644 index 00000000..8a4ce56c --- /dev/null +++ b/src/yggdrasil/multicast_other.go @@ -0,0 +1,9 @@ +// +build !linux,!darwin,!netbsd,!freebsd,!openbsd,!dragonflybsd,!windows + +package yggdrasil + +import "syscall" + +func multicastReuse(network string, address string, c syscall.RawConn) error { + return nil +} diff --git a/src/yggdrasil/multicast_unix.go b/src/yggdrasil/multicast_unix.go new file mode 100644 index 00000000..9c6d1f11 --- /dev/null +++ b/src/yggdrasil/multicast_unix.go @@ -0,0 +1,22 @@ +// +build linux darwin netbsd freebsd openbsd dragonflybsd + +package yggdrasil + +import "syscall" +import "golang.org/x/sys/unix" + +func multicastReuse(network string, address string, c syscall.RawConn) error { + var control error + var reuseport error + + control = c.Control(func(fd uintptr) { + reuseport = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + }) + + switch { + case reuseport != nil: + return reuseport + default: + return control + } +} diff --git a/src/yggdrasil/multicast_windows.go b/src/yggdrasil/multicast_windows.go new file mode 100644 index 00000000..13f20315 --- /dev/null +++ b/src/yggdrasil/multicast_windows.go @@ -0,0 +1,22 @@ +// +build windows + +package yggdrasil + +import "syscall" +import "golang.org/x/sys/windows" + +func multicastReuse(network string, address string, c syscall.RawConn) error { + var control error + var reuseaddr error + + control = c.Control(func(fd uintptr) { + reuseaddr = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) + }) + + switch { + case reuseaddr != nil: + return reuseaddr + default: + return control + } +} diff --git a/src/yggdrasil/peer.go b/src/yggdrasil/peer.go index de463b43..67aa805a 100644 --- a/src/yggdrasil/peer.go +++ b/src/yggdrasil/peer.go @@ -79,27 +79,30 @@ type peer struct { bytesSent uint64 // To track bandwidth usage for getPeers bytesRecvd uint64 // To track bandwidth usage for getPeers // BUG: sync/atomic, 32 bit platforms need the above to be the first element - core *Core - port switchPort - box boxPubKey - sig sigPubKey - shared boxSharedKey - linkShared boxSharedKey - firstSeen time.Time // To track uptime for getPeers - linkOut (chan []byte) // used for protocol traffic (to bypass queues) - doSend (chan struct{}) // tell the linkLoop to send a switchMsg - dinfo *dhtInfo // used to keep the DHT working - out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes - close func() // Called when a peer is removed, to close the underlying connection, or via admin api + core *Core + port switchPort + box boxPubKey + sig sigPubKey + shared boxSharedKey + linkShared boxSharedKey + endpoint string + friendlyName string + firstSeen time.Time // To track uptime for getPeers + linkOut (chan []byte) // used for protocol traffic (to bypass queues) + doSend (chan struct{}) // tell the linkLoop to send a switchMsg + dinfo *dhtInfo // used to keep the DHT working + out func([]byte) // Set up by whatever created the peers struct, used to send packets to other nodes + close func() // Called when a peer is removed, to close the underlying connection, or via admin api } // Creates a new peer with the specified box, sig, and linkShared keys, using the lowest unocupied port number. -func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey) *peer { +func (ps *peers) newPeer(box *boxPubKey, sig *sigPubKey, linkShared *boxSharedKey, endpoint string) *peer { now := time.Now() p := peer{box: *box, sig: *sig, shared: *getSharedKey(&ps.core.boxPriv, box), linkShared: *linkShared, + endpoint: endpoint, firstSeen: now, doSend: make(chan struct{}, 1), core: ps.core} @@ -172,7 +175,6 @@ func (p *peer) doSendSwitchMsgs() { // This must be launched in a separate goroutine by whatever sets up the peer struct. // It handles link protocol traffic. func (p *peer) linkLoop() { - go p.doSendSwitchMsgs() tick := time.NewTicker(time.Second) defer tick.Stop() for { @@ -183,8 +185,11 @@ func (p *peer) linkLoop() { } p.sendSwitchMsg() case _ = <-tick.C: - if p.dinfo != nil { - p.core.dht.peers <- p.dinfo + //break // FIXME disabled the below completely to test something + pdinfo := p.dinfo // FIXME this is a bad workarond NPE on the next line + if pdinfo != nil { + dinfo := *pdinfo + p.core.dht.peers <- &dinfo } } } @@ -311,7 +316,7 @@ func (p *peer) handleSwitchMsg(packet []byte) { sigMsg.Hops = msg.Hops[:idx] loc.coords = append(loc.coords, hop.Port) bs := getBytesForSig(&hop.Next, &sigMsg) - if !p.core.sigs.check(&prevKey, &hop.Sig, bs) { + if !verify(&prevKey, bs, &hop.Sig) { p.core.peers.removePeer(p.port) } prevKey = hop.Next @@ -330,7 +335,7 @@ func (p *peer) handleSwitchMsg(packet []byte) { key: p.box, coords: loc.getCoords(), } - p.core.dht.peers <- &dinfo + //p.core.dht.peers <- &dinfo p.dinfo = &dinfo } diff --git a/src/yggdrasil/router.go b/src/yggdrasil/router.go index d2a8c43b..b4824767 100644 --- a/src/yggdrasil/router.go +++ b/src/yggdrasil/router.go @@ -23,6 +23,7 @@ package yggdrasil // The router then runs some sanity checks before passing it to the tun import ( + "bytes" "time" "golang.org/x/net/icmp" @@ -32,22 +33,32 @@ import ( // The router struct has channels to/from the tun/tap device and a self peer (0), which is how messages are passed between this node and the peers/switch layer. // The router's mainLoop goroutine is responsible for managing all information related to the dht, searches, and crypto sessions. type router struct { - core *Core - addr address - in <-chan []byte // packets we received from the network, link to peer's "out" - out func([]byte) // packets we're sending to the network, link to peer's "in" - recv chan<- []byte // place where the tun pulls received packets from - send <-chan []byte // place where the tun puts outgoing packets - reset chan struct{} // signal that coords changed (re-init sessions/dht) - admin chan func() // pass a lambda for the admin socket to query stuff + core *Core + addr address + subnet subnet + in <-chan []byte // packets we received from the network, link to peer's "out" + out func([]byte) // packets we're sending to the network, link to peer's "in" + toRecv chan router_recvPacket // packets to handle via recvPacket() + recv chan<- []byte // place where the tun pulls received packets from + send <-chan []byte // place where the tun puts outgoing packets + reset chan struct{} // signal that coords changed (re-init sessions/dht) + admin chan func() // pass a lambda for the admin socket to query stuff + cryptokey cryptokey +} + +// Packet and session info, used to check that the packet matches a valid IP range or CKR prefix before sending to the tun. +type router_recvPacket struct { + bs []byte + sinfo *sessionInfo } // Initializes the router struct, which includes setting up channels to/from the tun/tap. func (r *router) init(core *Core) { r.core = core r.addr = *address_addrForNodeID(&r.core.dht.nodeID) + r.subnet = *address_subnetForNodeID(&r.core.dht.nodeID) in := make(chan []byte, 32) // TODO something better than this... - p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{}) + p := r.core.peers.newPeer(&r.core.boxPub, &r.core.sigPub, &boxSharedKey{}, "(self)") p.out = func(packet []byte) { // This is to make very sure it never blocks select { @@ -59,6 +70,7 @@ func (r *router) init(core *Core) { } r.in = in r.out = func(packet []byte) { p.handlePacket(packet) } // The caller is responsible for go-ing if it needs to not block + r.toRecv = make(chan router_recvPacket, 32) recv := make(chan []byte, 32) send := make(chan []byte, 32) r.recv = recv @@ -66,7 +78,8 @@ func (r *router) init(core *Core) { r.core.tun.recv = recv r.core.tun.send = send r.reset = make(chan struct{}, 1) - r.admin = make(chan func()) + r.admin = make(chan func(), 32) + r.cryptokey.init(r.core) // go r.mainLoop() } @@ -86,13 +99,19 @@ func (r *router) mainLoop() { defer ticker.Stop() for { select { + case rp := <-r.toRecv: + r.recvPacket(rp.bs, rp.sinfo) case p := <-r.in: r.handleIn(p) case p := <-r.send: r.sendPacket(p) case info := <-r.core.dht.peers: - r.core.dht.insertIfNew(info, false) // Insert as a normal node - r.core.dht.insertIfNew(info, true) // Insert as a peer + now := time.Now() + oldInfo, isIn := r.core.dht.table[*info.getNodeID()] + r.core.dht.insert(info) + if isIn && now.Sub(oldInfo.recv) < 45*time.Second { + info.recv = oldInfo.recv + } case <-r.reset: r.core.sessions.resetInits() r.core.dht.reset() @@ -102,7 +121,6 @@ func (r *router) mainLoop() { r.core.switchTable.doMaintenance() r.core.dht.doMaintenance() r.core.sessions.cleanup() - r.core.sigs.cleanup() util_getBytes() // To slowly drain things } case f := <-r.admin: @@ -117,30 +135,82 @@ func (r *router) mainLoop() { // If the session hasn't responded recently, it triggers a ping or search to keep things alive or deal with broken coords *relatively* quickly. // It also deals with oversized packets if there are MTU issues by calling into icmpv6.go to spoof PacketTooBig traffic, or DestinationUnreachable if the other side has their tun/tap disabled. func (r *router) sendPacket(bs []byte) { - if len(bs) < 40 { - panic("Tried to send a packet shorter than a header...") - } var sourceAddr address - var sourceSubnet subnet - copy(sourceAddr[:], bs[8:]) - copy(sourceSubnet[:], bs[8:]) - if !sourceAddr.isValid() && !sourceSubnet.isValid() { + var destAddr address + var destSnet subnet + var destPubKey *boxPubKey + var destNodeID *NodeID + var addrlen int + if bs[0]&0xf0 == 0x60 { + // Check if we have a fully-sized header + if len(bs) < 40 { + panic("Tried to send a packet shorter than an IPv6 header...") + } + // IPv6 address + addrlen = 16 + copy(sourceAddr[:addrlen], bs[8:]) + copy(destAddr[:addrlen], bs[24:]) + copy(destSnet[:addrlen/2], bs[24:]) + } else if bs[0]&0xf0 == 0x40 { + // Check if we have a fully-sized header + if len(bs) < 20 { + panic("Tried to send a packet shorter than an IPv4 header...") + } + // IPv4 address + addrlen = 4 + copy(sourceAddr[:addrlen], bs[12:]) + copy(destAddr[:addrlen], bs[16:]) + } else { + // Unknown address length return } - var dest address - copy(dest[:], bs[24:]) - var snet subnet - copy(snet[:], bs[24:]) - if !dest.isValid() && !snet.isValid() { + if !r.cryptokey.isValidSource(sourceAddr, addrlen) { + // The packet had a source address that doesn't belong to us or our + // configured crypto-key routing source subnets return } + if !destAddr.isValid() && !destSnet.isValid() { + // The addresses didn't match valid Yggdrasil node addresses so let's see + // whether it matches a crypto-key routing range instead + if key, err := r.cryptokey.getPublicKeyForAddress(destAddr, addrlen); err == nil { + // A public key was found, get the node ID for the search + destPubKey = &key + destNodeID = getNodeID(destPubKey) + // Do a quick check to ensure that the node ID refers to a vaild Yggdrasil + // address or subnet - this might be superfluous + addr := *address_addrForNodeID(destNodeID) + copy(destAddr[:], addr[:]) + copy(destSnet[:], addr[:]) + if !destAddr.isValid() && !destSnet.isValid() { + return + } + } else { + // No public key was found in the CKR table so we've exhausted our options + return + } + } doSearch := func(packet []byte) { var nodeID, mask *NodeID - if dest.isValid() { - nodeID, mask = dest.getNodeIDandMask() - } - if snet.isValid() { - nodeID, mask = snet.getNodeIDandMask() + switch { + case destNodeID != nil: + // We already know the full node ID, probably because it's from a CKR + // route in which the public key is known ahead of time + nodeID = destNodeID + var m NodeID + for i := range m { + m[i] = 0xFF + } + mask = &m + case destAddr.isValid(): + // We don't know the full node ID - try and use the address to generate + // a truncated node ID + nodeID, mask = destAddr.getNodeIDandMask() + case destSnet.isValid(): + // We don't know the full node ID - try and use the subnet to generate + // a truncated node ID + nodeID, mask = destSnet.getNodeIDandMask() + default: + return } sinfo, isIn := r.core.searches.searches[*nodeID] if !isIn { @@ -153,11 +223,11 @@ func (r *router) sendPacket(bs []byte) { } var sinfo *sessionInfo var isIn bool - if dest.isValid() { - sinfo, isIn = r.core.sessions.getByTheirAddr(&dest) + if destAddr.isValid() { + sinfo, isIn = r.core.sessions.getByTheirAddr(&destAddr) } - if snet.isValid() { - sinfo, isIn = r.core.sessions.getByTheirSubnet(&snet) + if destSnet.isValid() { + sinfo, isIn = r.core.sessions.getByTheirSubnet(&destSnet) } switch { case !isIn || !sinfo.init: @@ -186,6 +256,14 @@ func (r *router) sendPacket(bs []byte) { } fallthrough // Also send the packet default: + // If we know the public key ahead of time (i.e. a CKR route) then check + // if the session perm pub key matches before we send the packet to it + if destPubKey != nil { + if !bytes.Equal((*destPubKey)[:], sinfo.theirPermPub[:]) { + return + } + } + // Drop packets if the session MTU is 0 - this means that one or other // side probably has their TUN adapter disabled if sinfo.getMTU() == 0 { @@ -236,29 +314,55 @@ func (r *router) sendPacket(bs []byte) { // Don't continue - drop the packet return } + sinfo.send <- bs } } // Called for incoming traffic by the session worker for that connection. // Checks that the IP address is correct (matches the session) and passes the packet to the tun/tap. -func (r *router) recvPacket(bs []byte, theirAddr *address, theirSubnet *subnet) { +func (r *router) recvPacket(bs []byte, sinfo *sessionInfo) { // Note: called directly by the session worker, not the router goroutine if len(bs) < 24 { util_putBytes(bs) return } - var source address - copy(source[:], bs[8:]) + var sourceAddr address + var dest address var snet subnet - copy(snet[:], bs[8:]) - switch { - case source.isValid() && source == *theirAddr: - case snet.isValid() && snet == *theirSubnet: - default: + var addrlen int + if bs[0]&0xf0 == 0x60 { + // IPv6 address + addrlen = 16 + copy(sourceAddr[:addrlen], bs[8:]) + copy(dest[:addrlen], bs[24:]) + copy(snet[:addrlen/2], bs[8:]) + } else if bs[0]&0xf0 == 0x40 { + // IPv4 address + addrlen = 4 + copy(sourceAddr[:addrlen], bs[12:]) + copy(dest[:addrlen], bs[16:]) + } else { + // Unknown address length + return + } + // Check that the packet is destined for either our Yggdrasil address or + // subnet, or that it matches one of the crypto-key routing source routes + if !r.cryptokey.isValidSource(dest, addrlen) { util_putBytes(bs) return } + // See whether the packet they sent should have originated from this session + switch { + case sourceAddr.isValid() && sourceAddr == sinfo.theirAddr: + case snet.isValid() && snet == sinfo.theirSubnet: + default: + key, err := r.cryptokey.getPublicKeyForAddress(sourceAddr, addrlen) + if err != nil || key != sinfo.theirPermPub { + util_putBytes(bs) + return + } + } //go func() { r.recv<-bs }() r.recv <- bs } diff --git a/src/yggdrasil/search.go b/src/yggdrasil/search.go index 1b72a63f..be156dc6 100644 --- a/src/yggdrasil/search.go +++ b/src/yggdrasil/search.go @@ -11,6 +11,9 @@ package yggdrasil // A new search packet is sent immediately after receiving a response // A new search packet is sent periodically, once per second, in case a packet was dropped (this slowly causes the search to become parallel if the search doesn't timeout but also doesn't finish within 1 second for whatever reason) +// TODO? +// Some kind of max search steps, in case the node is offline, so we don't crawl through too much of the network looking for a destination that isn't there? + import ( "sort" "time" @@ -88,11 +91,13 @@ func (s *searches) handleDHTRes(res *dhtRes) { func (s *searches) addToSearch(sinfo *searchInfo, res *dhtRes) { // Add responses to toVisit if closer to dest than the res node from := dhtInfo{key: res.Key, coords: res.Coords} + sinfo.visited[*from.getNodeID()] = true for _, info := range res.Infos { - if sinfo.visited[*info.getNodeID()] { + if *info.getNodeID() == s.core.dht.nodeID || sinfo.visited[*info.getNodeID()] { continue } - if dht_firstCloserThanThird(info.getNodeID(), &res.Dest, from.getNodeID()) { + if dht_ordered(&sinfo.dest, info.getNodeID(), from.getNodeID()) { + // Response is closer to the destination sinfo.toVisit = append(sinfo.toVisit, info) } } @@ -107,7 +112,8 @@ func (s *searches) addToSearch(sinfo *searchInfo, res *dhtRes) { } // Sort sort.SliceStable(sinfo.toVisit, func(i, j int) bool { - return dht_firstCloserThanThird(sinfo.toVisit[i].getNodeID(), &res.Dest, sinfo.toVisit[j].getNodeID()) + // Should return true if i is closer to the destination than j + return dht_ordered(&res.Dest, sinfo.toVisit[i].getNodeID(), sinfo.toVisit[j].getNodeID()) }) // Truncate to some maximum size if len(sinfo.toVisit) > search_MAX_SEARCH_SIZE { @@ -126,11 +132,9 @@ func (s *searches) doSearchStep(sinfo *searchInfo) { // Send to the next search target var next *dhtInfo next, sinfo.toVisit = sinfo.toVisit[0], sinfo.toVisit[1:] - var oldPings int - oldPings, next.pings = next.pings, 0 + rq := dhtReqKey{next.key, sinfo.dest} + s.core.dht.addCallback(&rq, s.handleDHTRes) s.core.dht.ping(next, &sinfo.dest) - next.pings = oldPings // Don't evict a node for searching with it too much - sinfo.visited[*next.getNodeID()] = true } } diff --git a/src/yggdrasil/session.go b/src/yggdrasil/session.go index 0bc27a12..92ae262b 100644 --- a/src/yggdrasil/session.go +++ b/src/yggdrasil/session.go @@ -311,6 +311,11 @@ func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo { func (ss *sessions) cleanup() { // Time thresholds almost certainly could use some adjusting + for k := range ss.permShared { + // Delete a key, to make sure this eventually shrinks to 0 + delete(ss.permShared, k) + break + } if time.Since(ss.lastCleanup) < time.Minute { return } @@ -319,6 +324,36 @@ func (ss *sessions) cleanup() { s.close() } } + permShared := make(map[boxPubKey]*boxSharedKey, len(ss.permShared)) + for k, v := range ss.permShared { + permShared[k] = v + } + ss.permShared = permShared + sinfos := make(map[handle]*sessionInfo, len(ss.sinfos)) + for k, v := range ss.sinfos { + sinfos[k] = v + } + ss.sinfos = sinfos + byMySes := make(map[boxPubKey]*handle, len(ss.byMySes)) + for k, v := range ss.byMySes { + byMySes[k] = v + } + ss.byMySes = byMySes + byTheirPerm := make(map[boxPubKey]*handle, len(ss.byTheirPerm)) + for k, v := range ss.byTheirPerm { + byTheirPerm[k] = v + } + ss.byTheirPerm = byTheirPerm + addrToPerm := make(map[address]*boxPubKey, len(ss.addrToPerm)) + for k, v := range ss.addrToPerm { + addrToPerm[k] = v + } + ss.addrToPerm = addrToPerm + subnetToPerm := make(map[subnet]*boxPubKey, len(ss.subnetToPerm)) + for k, v := range ss.subnetToPerm { + subnetToPerm[k] = v + } + ss.subnetToPerm = subnetToPerm ss.lastCleanup = time.Now() } @@ -358,7 +393,7 @@ func (ss *sessions) getSharedKey(myPriv *boxPrivKey, return skey } // First do some cleanup - const maxKeys = dht_bucket_number * dht_bucket_size + const maxKeys = 1024 for key := range ss.permShared { // Remove a random key until the store is small enough if len(ss.permShared) < maxKeys { @@ -589,5 +624,5 @@ func (sinfo *sessionInfo) doRecv(p *wire_trafficPacket) { sinfo.updateNonce(&p.Nonce) sinfo.time = time.Now() sinfo.bytesRecvd += uint64(len(bs)) - sinfo.core.router.recvPacket(bs, &sinfo.theirAddr, &sinfo.theirSubnet) + sinfo.core.router.toRecv <- router_recvPacket{bs, sinfo} } diff --git a/src/yggdrasil/signature.go b/src/yggdrasil/signature.go deleted file mode 100644 index 203c9adc..00000000 --- a/src/yggdrasil/signature.go +++ /dev/null @@ -1,90 +0,0 @@ -package yggdrasil - -// This is where we record which signatures we've previously checked -// It's so we can avoid needlessly checking them again - -import ( - "sync" - "time" -) - -// This keeps track of what signatures have already been checked. -// It's used to skip expensive crypto operations, given that many signatures are likely to be the same for the average node's peers. -type sigManager struct { - mutex sync.RWMutex - checked map[sigBytes]knownSig - lastCleaned time.Time -} - -// Represents a known signature. -// Includes the key, the signature bytes, the bytes that were signed, and the time it was last used. -type knownSig struct { - key sigPubKey - sig sigBytes - bs []byte - time time.Time -} - -// Initializes the signature manager. -func (m *sigManager) init() { - m.checked = make(map[sigBytes]knownSig) -} - -// Checks if a key and signature match the supplied bytes. -// If the same key/sig/bytes have been checked before, it returns true from the cached results. -// If not, it checks the key, updates it in the cache if successful, and returns the checked results. -func (m *sigManager) check(key *sigPubKey, sig *sigBytes, bs []byte) bool { - if m.isChecked(key, sig, bs) { - return true - } - verified := verify(key, bs, sig) - if verified { - m.putChecked(key, sig, bs) - } - return verified -} - -// Checks the cache to see if this key/sig/bytes combination has already been verified. -// Returns true if it finds a match. -func (m *sigManager) isChecked(key *sigPubKey, sig *sigBytes, bs []byte) bool { - m.mutex.RLock() - defer m.mutex.RUnlock() - k, isIn := m.checked[*sig] - if !isIn { - return false - } - if k.key != *key || k.sig != *sig || len(bs) != len(k.bs) { - return false - } - for idx := 0; idx < len(bs); idx++ { - if bs[idx] != k.bs[idx] { - return false - } - } - k.time = time.Now() - return true -} - -// Puts a new result into the cache. -// This result is then used by isChecked to skip the expensive crypto verification if it's needed again. -// This is useful because, for nodes with multiple peers, there is often a lot of overlap between the signatures provided by each peer. -func (m *sigManager) putChecked(key *sigPubKey, newsig *sigBytes, bs []byte) { - m.mutex.Lock() - defer m.mutex.Unlock() - k := knownSig{key: *key, sig: *newsig, bs: bs, time: time.Now()} - m.checked[*newsig] = k -} - -func (m *sigManager) cleanup() { - m.mutex.Lock() - defer m.mutex.Unlock() - if time.Since(m.lastCleaned) < time.Minute { - return - } - for s, k := range m.checked { - if time.Since(k.time) > time.Minute { - delete(m.checked, s) - } - } - m.lastCleaned = time.Now() -} diff --git a/src/yggdrasil/switch.go b/src/yggdrasil/switch.go index 72f17ed2..99ed25b4 100644 --- a/src/yggdrasil/switch.go +++ b/src/yggdrasil/switch.go @@ -18,9 +18,12 @@ import ( "time" ) -const switch_timeout = time.Minute -const switch_updateInterval = switch_timeout / 2 -const switch_throttle = switch_updateInterval / 2 +const ( + switch_timeout = time.Minute + switch_updateInterval = switch_timeout / 2 + switch_throttle = switch_updateInterval / 2 + switch_faster_threshold = 240 //Number of switch updates before switching to a faster parent +) // The switch locator represents the topology and network state dependent info about a node, minus the signatures that go with it. // Nodes will pick the best root they see, provided that the root continues to push out updates with new timestamps. @@ -118,13 +121,13 @@ func (x *switchLocator) isAncestorOf(y *switchLocator) bool { // Information about a peer, used by the switch to build the tree and eventually make routing decisions. type peerInfo struct { - key sigPubKey // ID of this peer - locator switchLocator // Should be able to respond with signatures upon request - degree uint64 // Self-reported degree - time time.Time // Time this node was last seen - firstSeen time.Time - port switchPort // Interface number of this peer - msg switchMsg // The wire switchMsg used + key sigPubKey // ID of this peer + locator switchLocator // Should be able to respond with signatures upon request + degree uint64 // Self-reported degree + time time.Time // Time this node was last seen + faster map[switchPort]uint64 // Counter of how often a node is faster than the current parent, penalized extra if slower + port switchPort // Interface number of this peer + msg switchMsg // The wire switchMsg used } // This is just a uint64 with a named type for clarity reasons. @@ -155,21 +158,25 @@ type switchData struct { // All the information stored by the switch. type switchTable struct { - core *Core - key sigPubKey // Our own key - time time.Time // Time when locator.tstamp was last updated - parent switchPort // Port of whatever peer is our parent, or self if we're root - drop map[sigPubKey]int64 // Tstamp associated with a dropped root - mutex sync.RWMutex // Lock for reads/writes of switchData - data switchData // - updater atomic.Value // *sync.Once - table atomic.Value // lookupTable - packetIn chan []byte // Incoming packets for the worker to handle - idleIn chan switchPort // Incoming idle notifications from peer links - admin chan func() // Pass a lambda for the admin socket to query stuff - queues switch_buffers // Queues - not atomic so ONLY use through admin chan + core *Core + key sigPubKey // Our own key + time time.Time // Time when locator.tstamp was last updated + drop map[sigPubKey]int64 // Tstamp associated with a dropped root + mutex sync.RWMutex // Lock for reads/writes of switchData + parent switchPort // Port of whatever peer is our parent, or self if we're root + data switchData // + updater atomic.Value // *sync.Once + table atomic.Value // lookupTable + packetIn chan []byte // Incoming packets for the worker to handle + idleIn chan switchPort // Incoming idle notifications from peer links + admin chan func() // Pass a lambda for the admin socket to query stuff + queues switch_buffers // Queues - not atomic so ONLY use through admin chan + queueTotalMaxSize uint64 // Maximum combined size of queues } +// Minimum allowed total size of switch queues. +const SwitchQueueTotalMinSize = 4 * 1024 * 1024 + // Initializes the switchTable struct. func (t *switchTable) init(core *Core, key sigPubKey) { now := time.Now() @@ -184,6 +191,7 @@ func (t *switchTable) init(core *Core, key sigPubKey) { t.packetIn = make(chan []byte, 1024) t.idleIn = make(chan switchPort, 1024) t.admin = make(chan func()) + t.queueTotalMaxSize = SwitchQueueTotalMinSize } // Safely gets a copy of this node's locator. @@ -200,6 +208,7 @@ func (t *switchTable) doMaintenance() { defer t.mutex.Unlock() // Release lock when we're done t.cleanRoot() t.cleanDropped() + t.cleanPeers() } // Updates the root periodically if it is ourself, or promotes ourself to root if we're better than the current root or if the current root has timed out. @@ -250,8 +259,30 @@ func (t *switchTable) forgetPeer(port switchPort) { if port != t.parent { return } + t.parent = 0 for _, info := range t.data.peers { - t.unlockedHandleMsg(&info.msg, info.port) + t.unlockedHandleMsg(&info.msg, info.port, true) + } +} + +// Clean all unresponsive peers from the table, needed in case a peer stops updating. +// Needed in case a non-parent peer keeps the connection open but stops sending updates. +// Also reclaims space from deleted peers by copying the map. +func (t *switchTable) cleanPeers() { + now := time.Now() + for port, peer := range t.data.peers { + if now.Sub(peer.time) > switch_timeout+switch_throttle { + // Longer than switch_timeout to make sure we don't remove a working peer because the root stopped responding. + delete(t.data.peers, port) + } + } + if _, isIn := t.data.peers[t.parent]; !isIn { + // The root timestamp would probably time out before this happens, but better safe than sorry. + // We removed the current parent, so find a new one. + t.parent = 0 + for _, peer := range t.data.peers { + t.unlockedHandleMsg(&peer.msg, peer.port, true) + } } } @@ -325,7 +356,7 @@ func (t *switchTable) checkRoot(msg *switchMsg) bool { func (t *switchTable) handleMsg(msg *switchMsg, fromPort switchPort) { t.mutex.Lock() defer t.mutex.Unlock() - t.unlockedHandleMsg(msg, fromPort) + t.unlockedHandleMsg(msg, fromPort, false) } // This updates the switch with information about a peer. @@ -333,7 +364,8 @@ func (t *switchTable) handleMsg(msg *switchMsg, fromPort switchPort) { // That happens if this node is already our parent, or is advertising a better root, or is advertising a better path to the same root, etc... // There are a lot of very delicate order sensitive checks here, so its' best to just read the code if you need to understand what it's doing. // It's very important to not change the order of the statements in the case function unless you're absolutely sure that it's safe, including safe if used along side nodes that used the previous order. -func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) { +// Set the third arg to true if you're reprocessing an old message, e.g. to find a new parent after one disconnects, to avoid updating some timing related things. +func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort, reprocessing bool) { // TODO directly use a switchMsg instead of switchMessage + sigs now := time.Now() // Set up the sender peerInfo @@ -348,11 +380,6 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) { prevKey = hop.Next } sender.msg = *msg - oldSender, isIn := t.data.peers[fromPort] - if !isIn { - oldSender.firstSeen = now - } - sender.firstSeen = oldSender.firstSeen sender.port = fromPort sender.time = now // Decide what to do @@ -371,11 +398,40 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) { return true } doUpdate := false + oldSender := t.data.peers[fromPort] if !equiv(&sender.locator, &oldSender.locator) { + // Reset faster info, we'll start refilling it right after this + sender.faster = nil doUpdate = true - sender.firstSeen = now } + // Update the matrix of peer "faster" thresholds + if reprocessing { + sender.faster = oldSender.faster + } else { + sender.faster = make(map[switchPort]uint64, len(oldSender.faster)) + for port, peer := range t.data.peers { + if port == fromPort { + continue + } else if sender.locator.root != peer.locator.root || sender.locator.tstamp > peer.locator.tstamp { + // We were faster than this node, so increment, as long as we don't overflow because of it + if oldSender.faster[peer.port] < switch_faster_threshold { + sender.faster[port] = oldSender.faster[peer.port] + 1 + } else { + sender.faster[port] = switch_faster_threshold + } + } else { + // Slower than this node, penalize (more than the reward amount) + if oldSender.faster[port] > 1 { + sender.faster[port] = oldSender.faster[peer.port] - 2 + } else { + sender.faster[port] = 0 + } + } + } + } + // Update sender t.data.peers[fromPort] = sender + // Decide if we should also update our root info to make the sender our parent updateRoot := false oldParent, isIn := t.data.peers[t.parent] noParent := !isIn @@ -390,39 +446,49 @@ func (t *switchTable) unlockedHandleMsg(msg *switchMsg, fromPort switchPort) { } return true }() - sTime := now.Sub(sender.firstSeen) - pTime := oldParent.time.Sub(oldParent.firstSeen) + switch_timeout - // Really want to compare sLen/sTime and pLen/pTime - // Cross multiplied to avoid divide-by-zero - cost := len(sender.locator.coords) * int(pTime.Seconds()) - pCost := len(t.data.locator.coords) * int(sTime.Seconds()) dropTstamp, isIn := t.drop[sender.locator.root] - // Here be dragons + // Decide if we need to update info about the root or change parents. switch { - case !noLoop: // do nothing - case isIn && dropTstamp >= sender.locator.tstamp: // do nothing + case !noLoop: + // This route loops, so we can't use the sender as our parent. + case isIn && dropTstamp >= sender.locator.tstamp: + // This is a known root with a timestamp older than a known timeout, so we can't trust it to be a new announcement. case firstIsBetter(&sender.locator.root, &t.data.locator.root): + // This is a better root than what we're currently using, so we should update. updateRoot = true - case t.data.locator.root != sender.locator.root: // do nothing - case t.data.locator.tstamp > sender.locator.tstamp: // do nothing + case t.data.locator.root != sender.locator.root: + // This is not the same root, and it's apparently not better (from the above), so we should ignore it. + case t.data.locator.tstamp > sender.locator.tstamp: + // This timetsamp is older than the most recently seen one from this root, so we should ignore it. case noParent: + // We currently have no working parent, and at this point in the switch statement, anything is better than nothing. updateRoot = true - case cost < pCost: + case sender.faster[t.parent] >= switch_faster_threshold: + // The is reliably faster than the current parent. updateRoot = true - case sender.port != t.parent: // do nothing - case !equiv(&sender.locator, &t.data.locator): - // Special case - // If coords changed, then this may now be a worse parent than before - // Re-parent the node (de-parent and reprocess the message) - // Then reprocess *all* messages to look for a better parent - // This is so we don't keep using this node as our parent if there's something better + case reprocessing && sender.faster[t.parent] > oldParent.faster[sender.port]: + // The sender seems to be reliably faster than the current parent, so switch to them instead. + updateRoot = true + case sender.port != t.parent: + // Ignore further cases if the sender isn't our parent. + case !reprocessing && !equiv(&sender.locator, &t.data.locator): + // Special case: + // If coords changed, then we need to penalize this node somehow, to prevent flapping. + // First, reset all faster-related info to 0. + // Then, de-parent the node and reprocess all messages to find a new parent. t.parent = 0 - t.unlockedHandleMsg(msg, fromPort) - for _, info := range t.data.peers { - t.unlockedHandleMsg(&info.msg, info.port) + for _, peer := range t.data.peers { + if peer.port == sender.port { + continue + } + t.unlockedHandleMsg(&peer.msg, peer.port, true) } - case now.Sub(t.time) < switch_throttle: // do nothing + // Process the sender last, to avoid keeping them as a parent if at all possible. + t.unlockedHandleMsg(&sender.msg, sender.port, true) + case now.Sub(t.time) < switch_throttle: + // We've already gotten an update from this root recently, so ignore this one to avoid flooding. case sender.locator.tstamp > t.data.locator.tstamp: + // The timestamp was updated, so we need to update locally and send to our peers. updateRoot = true } if updateRoot { @@ -603,8 +669,6 @@ type switch_packetInfo struct { time time.Time // Timestamp of when the packet arrived } -const switch_buffer_maxSize = 4 * 1048576 // Maximum 4 MB - // Used to keep track of buffered packets type switch_buffer struct { packets []switch_packetInfo // Currently buffered packets, which may be dropped if it grows too large @@ -612,10 +676,11 @@ type switch_buffer struct { } type switch_buffers struct { - bufs map[string]switch_buffer // Buffers indexed by StreamID - size uint64 // Total size of all buffers, in bytes - maxbufs int - maxsize uint64 + switchTable *switchTable + bufs map[string]switch_buffer // Buffers indexed by StreamID + size uint64 // Total size of all buffers, in bytes + maxbufs int + maxsize uint64 } func (b *switch_buffers) cleanup(t *switchTable) { @@ -632,7 +697,7 @@ func (b *switch_buffers) cleanup(t *switchTable) { } } - for b.size > switch_buffer_maxSize { + for b.size > b.switchTable.queueTotalMaxSize { // Drop a random queue target := rand.Uint64() % b.size var size uint64 // running total @@ -702,6 +767,7 @@ func (t *switchTable) handleIdle(port switchPort) bool { // The switch worker does routing lookups and sends packets to where they need to be func (t *switchTable) doWorker() { + t.queues.switchTable = t t.queues.bufs = make(map[string]switch_buffer) // Packets per PacketStreamID (string) idle := make(map[switchPort]struct{}) // this is to deduplicate things for { diff --git a/src/yggdrasil/tcp.go b/src/yggdrasil/tcp.go index 0bc5802b..5ca66304 100644 --- a/src/yggdrasil/tcp.go +++ b/src/yggdrasil/tcp.go @@ -287,7 +287,7 @@ func (iface *tcpInterface) handler(sock net.Conn, incoming bool) { }() // Note that multiple connections to the same node are allowed // E.g. over different interfaces - p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link)) + p := iface.core.peers.newPeer(&info.box, &info.sig, getSharedKey(myLinkPriv, &meta.link), sock.RemoteAddr().String()) p.linkOut = make(chan []byte, 1) in := func(bs []byte) { p.handlePacket(bs) diff --git a/src/yggdrasil/tun.go b/src/yggdrasil/tun.go index cbbcdea7..e4625020 100644 --- a/src/yggdrasil/tun.go +++ b/src/yggdrasil/tun.go @@ -3,10 +3,14 @@ package yggdrasil // This manages the tun driver to send/recv packets to/from applications import ( - "yggdrasil/defaults" + "bytes" + "errors" + "time" "github.com/songgao/packets/ethernet" "github.com/yggdrasil-network/water" + + "github.com/yggdrasil-network/yggdrasil-go/src/defaults" ) const tun_IPv6_HEADER_LENGTH = 40 @@ -48,6 +52,23 @@ func (tun *tunDevice) start(ifname string, iftapmode bool, addr string, mtu int) } go func() { panic(tun.read()) }() go func() { panic(tun.write()) }() + if iftapmode { + go func() { + for { + if _, ok := tun.icmpv6.peermacs[tun.core.router.addr]; ok { + break + } + request, err := tun.icmpv6.create_ndp_tap(tun.core.router.addr) + if err != nil { + panic(err) + } + if _, err := tun.iface.Write(request); err != nil { + panic(err) + } + time.Sleep(time.Second) + } + }() + } return nil } @@ -61,16 +82,74 @@ func (tun *tunDevice) write() error { continue } if tun.iface.IsTAP() { - var frame ethernet.Frame - frame.Prepare( - tun.icmpv6.peermac[:6], // Destination MAC address - tun.icmpv6.mymac[:6], // Source MAC address - ethernet.NotTagged, // VLAN tagging - ethernet.IPv6, // Ethertype - len(data)) // Payload length - copy(frame[tun_ETHER_HEADER_LENGTH:], data[:]) - if _, err := tun.iface.Write(frame); err != nil { - panic(err) + var destAddr address + if data[0]&0xf0 == 0x60 { + if len(data) < 40 { + panic("Tried to send a packet shorter than an IPv6 header...") + } + copy(destAddr[:16], data[24:]) + } else if data[0]&0xf0 == 0x40 { + if len(data) < 20 { + panic("Tried to send a packet shorter than an IPv4 header...") + } + copy(destAddr[:4], data[16:]) + } else { + return errors.New("Invalid address family") + } + sendndp := func(destAddr address) { + neigh, known := tun.icmpv6.peermacs[destAddr] + known = known && (time.Since(neigh.lastsolicitation).Seconds() < 30) + if !known { + request, err := tun.icmpv6.create_ndp_tap(destAddr) + if err != nil { + panic(err) + } + if _, err := tun.iface.Write(request); err != nil { + panic(err) + } + tun.icmpv6.peermacs[destAddr] = neighbor{ + lastsolicitation: time.Now(), + } + } + } + var peermac macAddress + var peerknown bool + if data[0]&0xf0 == 0x40 { + destAddr = tun.core.router.addr + } else if data[0]&0xf0 == 0x60 { + if !bytes.Equal(tun.core.router.addr[:16], destAddr[:16]) && !bytes.Equal(tun.core.router.subnet[:8], destAddr[:8]) { + destAddr = tun.core.router.addr + } + } + if neighbor, ok := tun.icmpv6.peermacs[destAddr]; ok && neighbor.learned { + peermac = neighbor.mac + peerknown = true + } else if neighbor, ok := tun.icmpv6.peermacs[tun.core.router.addr]; ok && neighbor.learned { + peermac = neighbor.mac + peerknown = true + sendndp(destAddr) + } else { + sendndp(tun.core.router.addr) + } + if peerknown { + var proto ethernet.Ethertype + switch { + case data[0]&0xf0 == 0x60: + proto = ethernet.IPv6 + case data[0]&0xf0 == 0x40: + proto = ethernet.IPv4 + } + var frame ethernet.Frame + frame.Prepare( + peermac[:6], // Destination MAC address + tun.icmpv6.mymac[:6], // Source MAC address + ethernet.NotTagged, // VLAN tagging + proto, // Ethertype + len(data)) // Payload length + copy(frame[tun_ETHER_HEADER_LENGTH:], data[:]) + if _, err := tun.iface.Write(frame); err != nil { + panic(err) + } } } else { if _, err := tun.iface.Write(data); err != nil { @@ -101,10 +180,10 @@ func (tun *tunDevice) read() error { if tun.iface.IsTAP() { o = tun_ETHER_HEADER_LENGTH } - if buf[o]&0xf0 != 0x60 || - n != 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o { - // Either not an IPv6 packet or not the complete packet for some reason - //panic("Should not happen in testing") + switch { + case buf[o]&0xf0 == 0x60 && n == 256*int(buf[o+4])+int(buf[o+5])+tun_IPv6_HEADER_LENGTH+o: + case buf[o]&0xf0 == 0x40 && n == 256*int(buf[o+2])+int(buf[o+3])+o: + default: continue } if buf[o+6] == 58 {