5
0
mirror of https://github.com/cwinfo/yggdrasil-go.git synced 2024-11-26 01:21:37 +00:00

Merge pull request #231 from yggdrasil-network/develop

Version 0.3
This commit is contained in:
Neil Alexander 2018-12-12 10:02:00 +00:00 committed by GitHub
commit 7c51d9e971
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2099 additions and 1129 deletions

View File

@ -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

View File

@ -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

1
Dockerfile Normal file
View File

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

View File

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

View File

@ -15,7 +15,7 @@ You're encouraged to play with it, but it is strongly advised not to use it for
## Building
1. Install Go (tested on 1.9+, [godeb](https://github.com/niemeyer/godeb) is recommended for debian-based linux distributions).
1. Install Go (requires 1.11 or later, [godeb](https://github.com/niemeyer/godeb) is recommended for Debian-based Linux distributions).
2. Clone this repository.
2. `./build`

View File

@ -1 +0,0 @@
0.2

33
build
View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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"

22
contrib/docker/Dockerfile Normal file
View File

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

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

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

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

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

View File

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

View File

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

View File

@ -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 'v%d.%d.%d' "$MAJOR" "$MINOR" "$PATCH"
printf '%s%d.%d' "$PREPEND" "$MAJOR" "$MINOR"
fi
else
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

View File

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

View File

@ -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: `<uptime + timeout> / <distance to the root>`.
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:

14
go.mod Normal file
View File

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

20
go.sum Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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)."`
}

View File

@ -7,6 +7,9 @@ type platformDefaultParameters struct {
// Admin socket
DefaultAdminListen string
// Configuration (used for yggdrasilctl)
DefaultConfigFile string
// TUN/TAP
MaximumIfMTU int
DefaultIfMTU int

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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 {
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,27 +669,26 @@ 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 {
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)},
{"bucket", i},
{"peer_only", isPeer},
{"last_seen", int(now.Sub(v.recv).Seconds())},
{"box_pub_key", hex.EncodeToString(v.key[:])},
}
infos = append(infos, info)
}
}
getInfo(b.other, false)
getInfo(b.peers, true)
}
}
a.core.router.doAdmin(getDHT)
return infos
}
@ -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.

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

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

View File

@ -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

View File

@ -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)
goWorkers := func(source, dest *peer) {
source.linkOut = make(chan []byte, 1)
send := make(chan []byte, 1)
source.out = func(bs []byte) {
send <- bs
}
go source.linkLoop()
go func() {
for bs := range p.linkOut {
q.handlePacket(bs)
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
}
}
}()
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)
}
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
}

View File

@ -1,38 +1,15 @@
package yggdrasil
/*
This part has the (kademlia-like) distributed hash table
It's used to look up coords for a NodeID
Every node participates in the DHT, and the DHT stores no real keys/values
(Only the peer relationships / lookups are needed)
This version is intentionally fragile, by being recursive instead of iterative
(it's also not parallel, as a result)
This is to make sure that DHT black holes are visible if they exist
(the iterative parallel approach tends to get around them sometimes)
I haven't seen this get stuck on blackholes, but I also haven't proven it can't
Slight changes *do* make it blackhole hard, bootstrapping isn't an easy problem
*/
// A chord-like Distributed Hash Table (DHT).
// Used to look up coords given a NodeID and bitmask (taken from an IPv6 address).
// Keeps track of immediate successor, predecessor, and all peers.
// Also keeps track of other nodes if they're closer in tree space than all other known nodes encountered when heading in either direction to that point, under the hypothesis that, for the kinds of networks we care about, this should probabilistically include the node needed to keep lookups to near O(logn) steps.
import (
"sort"
"time"
)
// Number of DHT buckets, equal to the number of bits in a NodeID.
// Note that, in practice, nearly all of these will be empty.
const dht_bucket_number = 8 * NodeIDLen
// Number of nodes to keep in each DHT bucket.
// Additional entries may be kept for peers, for bootstrapping reasons, if they don't already have an entry in the bucket.
const dht_bucket_size = 2
// Number of responses to include in a lookup.
// If extras are given, they will be truncated from the response handler to prevent abuse.
const dht_lookup_size = 16
// dhtInfo represents everything we know about a node in the DHT.
@ -41,11 +18,9 @@ type dhtInfo struct {
nodeID_hidden *NodeID
key boxPubKey
coords []byte
send time.Time // When we last sent a message
recv time.Time // When we last received a message
pings int // Decide when to drop
throttle time.Duration // Time to wait before pinging a node to bootstrap buckets, increases exponentially from 1 second to 1 minute
bootstrapSend time.Time // The time checked/updated as part of throttle checks
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
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)
if _, isIn := t.table[*info.getNodeID()]; !isIn && t.isImportant(&info) {
t.ping(&info, nil)
}
// 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
}
_, 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
}
}
}
update(b.peers)
update(b.other)
return toPing
// 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
}
// 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
// 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)
}
// 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)
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(reqs) == 0 {
delete(t.reqs, key)
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 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.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
}
}
}
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
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
}
}
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.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 {
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
}
for _, other := range b.other {
if dht_firstCloserThanThird(info.getNodeID(), &t.nodeID, other.getNodeID()) {
dist := uint64(loc.dist(info.coords))
if dist < minDist {
minDist = dist
}
}
minDist = ^uint64(0)
for idx := len(important) - 1; idx >= 0; idx-- {
info := important[idx]
if (*info.getNodeID() == *ninfo.getNodeID()) ||
(ndist < minDist && dht_ordered(&t.nodeID, ninfo.getNodeID(), info.getNodeID())) {
// Either the same node, or a better one
return true
}
dist := uint64(loc.dist(info.coords))
if dist < minDist {
minDist = dist
}
}
// We didn't find any important node that ninfo is better than
return false
}
// Returns true if the keyspace distance between the first and second node is smaller than the keyspace distance between the second and third node.
func dht_firstCloserThanThird(first *NodeID,
second *NodeID,
third *NodeID) bool {
for idx := 0; idx < NodeIDLen; idx++ {
f := first[idx] ^ second[idx]
t := third[idx] ^ second[idx]
if f == t {
continue
}
return f < t
}
return false
}
// Resets the DHT in response to coord changes.
// This empties all buckets, resets the bootstrapping cycle to 0, and empties the rumor mill.
// It adds all old "other" node info to the rumor mill, so they'll be pinged quickly.
// If those nodes haven't also changed coords, then this is a relatively quick way to notify those nodes of our new coords and re-add them to our own DHT if they respond.
func (t *dht) reset() {
// This is mostly so bootstrapping will reset to resend coords into the network
t.offset = 0
t.rumorMill = nil // reset mill
for _, b := range t.buckets_hidden {
b.peers = b.peers[:0]
for _, info := range b.other {
// Add other nodes to the rumor mill so they'll be pinged soon
// This will hopefully tell them our coords and re-learn theirs quickly if they haven't changed
t.addToMill(info, info.getNodeID())
}
b.other = b.other[:0]
}
}

View File

@ -13,6 +13,7 @@ import (
"encoding/binary"
"errors"
"net"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv6"
@ -24,10 +25,16 @@ const len_ETHER = 14
type icmpv6 struct {
tun *tunDevice
peermac macAddress
peerlladdr net.IP
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,7 +154,6 @@ 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
@ -166,7 +170,19 @@ func (i *icmpv6) parse_packet_tun(datain []byte) ([]byte, error) {
} 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

View File

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

View File

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

View File

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

View File

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

View File

@ -85,6 +85,8 @@ type peer struct {
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
@ -94,12 +96,13 @@ type peer struct {
}
// 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
}

View File

@ -23,6 +23,7 @@ package yggdrasil
// The router then runs some sanity checks before passing it to the tun
import (
"bytes"
"time"
"golang.org/x/net/icmp"
@ -34,20 +35,30 @@ import (
type router struct {
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()
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
}
if snet.isValid() {
nodeID, mask = snet.getNodeIDandMask()
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
}

View File

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

View File

@ -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}
}

View File

@ -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()
}

View File

@ -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.
@ -122,7 +125,7 @@ type peerInfo struct {
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
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
}
@ -158,9 +161,9 @@ 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
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
@ -168,8 +171,12 @@ type switchTable struct {
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
}
case now.Sub(t.time) < switch_throttle: // do nothing
t.unlockedHandleMsg(&peer.msg, peer.port, true)
}
// Process the sender last, to avoid keeping them as a parent if at all possible.
t.unlockedHandleMsg(&sender.msg, sender.port, true)
case now.Sub(t.time) < switch_throttle:
// We've already gotten an update from this root recently, so ignore this one to avoid flooding.
case sender.locator.tstamp > t.data.locator.tstamp:
// The timestamp was updated, so we need to update locally and send to our peers.
updateRoot = true
}
if updateRoot {
@ -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,6 +676,7 @@ type switch_buffer struct {
}
type switch_buffers struct {
switchTable *switchTable
bufs map[string]switch_buffer // Buffers indexed by StreamID
size uint64 // Total size of all buffers, in bytes
maxbufs int
@ -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 {

View File

@ -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)

View File

@ -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,17 +82,75 @@ func (tun *tunDevice) write() error {
continue
}
if tun.iface.IsTAP() {
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(
tun.icmpv6.peermac[:6], // Destination MAC address
peermac[:6], // Destination MAC address
tun.icmpv6.mymac[:6], // Source MAC address
ethernet.NotTagged, // VLAN tagging
ethernet.IPv6, // Ethertype
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 {
panic(err)
@ -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 {