Compare commits

..

No commits in common. "develop" and "v0.3.16" have entirely different histories.

155 changed files with 14438 additions and 8619 deletions

257
.circleci/config.yml Normal file
View file

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

View file

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

View file

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

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "doc/yggdrasil-network.github.io"]
path = doc/yggdrasil-network.github.io
url = https://github.com/yggdrasil-network/yggdrasil-network.github.io/

View file

@ -2,10 +2,9 @@ run:
build-tags:
- lint
issues-exit-code: 0 # TODO: change this to 1 when we want it to fail builds
issues:
exclude-dirs:
skip-dirs:
- contrib/
- misc/
linters:
disable:
- gocyclo
- gocyclo

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
# Yggdrasil
[![Build status](https://github.com/yggdrasil-network/yggdrasil-go/actions/workflows/ci.yml/badge.svg)](https://github.com/yggdrasil-network/yggdrasil-go/actions/workflows/ci.yml)
[![CircleCI](https://circleci.com/gh/yggdrasil-network/yggdrasil-go.svg?style=shield&circle-token=:circle-token
)](https://circleci.com/gh/yggdrasil-network/yggdrasil-go)
## Introduction
@ -10,21 +11,44 @@ allows pretty much any IPv6-capable application to communicate securely with
other Yggdrasil nodes. Yggdrasil does not require you to have IPv6 Internet
connectivity - it also works over IPv4.
Although Yggdrasil shares many similarities with
[cjdns](https://github.com/cjdelisle/cjdns), it employs a different routing
algorithm based on a globally-agreed spanning tree and greedy routing in a
metric space, and aims to implement some novel local backpressure routing
techniques. In theory, Yggdrasil should scale well on networks with
internet-like topologies.
## Supported Platforms
Yggdrasil works on a number of platforms, including Linux, macOS, Ubiquiti
EdgeRouter, VyOS, Windows, FreeBSD, OpenBSD and OpenWrt.
We actively support the following platforms, and packages are available for
some of the below:
Please see our [Installation](https://yggdrasil-network.github.io/installation.html)
page for more information. You may also find other platform-specific wrappers, scripts
or tools in the `contrib` folder.
- Linux
- `.deb` and `.rpm` packages are built by CI for Debian and Red Hat-based
distributions
- Arch, Nix, Void packages also available within their respective repositories
- macOS
- `.pkg` packages are built by CI
- Ubiquiti EdgeOS
- `.deb` Vyatta packages are built by CI
- Windows
- FreeBSD
- OpenBSD
- OpenWrt
Please see our [Platforms](https://yggdrasil-network.github.io/platforms.html) pages for more
specific information about each of our supported platforms, including
installation steps and caveats.
You may also find other platform-specific wrappers, scripts or tools in the
`contrib` folder.
## Building
If you want to build from source, as opposed to installing one of the pre-built
packages:
1. Install [Go](https://golang.org) (requires Go 1.22 or later)
1. Install [Go](https://golang.org) (requires Go 1.16 or later)
2. Clone this repository
2. Run `./build`
@ -56,7 +80,6 @@ other configuration such as listen addresses or multicast addresses, etc.
### Run Yggdrasil
To run with the generated static configuration:
```
./yggdrasil -useconffile /path/to/yggdrasil.conf
```
@ -74,18 +97,21 @@ by giving the Yggdrasil binary the `CAP_NET_ADMIN` capability.
## Documentation
Documentation is available [on our website](https://yggdrasil-network.github.io).
Documentation is available on our [GitHub
Pages](https://yggdrasil-network.github.io) site, or in the base submodule
repository within `doc/yggdrasil-network.github.io`.
- [Installing Yggdrasil](https://yggdrasil-network.github.io/installation.html)
- [Configuring Yggdrasil](https://yggdrasil-network.github.io/configuration.html)
- [Configuration file options](https://yggdrasil-network.github.io/configuration.html)
- [Platform-specific documentation](https://yggdrasil-network.github.io/platforms.html)
- [Frequently asked questions](https://yggdrasil-network.github.io/faq.html)
- [Admin API documentation](https://yggdrasil-network.github.io/admin.html)
- [Version changelog](CHANGELOG.md)
## Community
Feel free to join us on our [Matrix
channel](https://matrix.to/#/#yggdrasil:matrix.org) at `#yggdrasil:matrix.org`
or in the `#yggdrasil` IRC channel on [libera.chat](https://libera.chat).
or in the `#yggdrasil` IRC channel on Freenode.
## License

34
build
View file

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

View file

@ -1,4 +1,5 @@
/*
This file generates crypto keys.
It prints out a new set of keys each time if finds a "better" one.
By default, "better" means a higher NodeID (-> higher IP address).
@ -7,87 +8,121 @@ This is because the IP address format can compress leading 1s in the address, to
If run with the "-sig" flag, it generates signing keys instead.
A "better" signing key means one with a higher TreeID.
This only matters if it's high enough to make you the root of the tree.
*/
package main
import (
"crypto/ed25519"
"encoding/hex"
"flag"
"fmt"
"net"
"runtime"
"time"
"suah.dev/protect"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
)
var doSig = flag.Bool("sig", false, "generate new signing keys instead")
type keySet struct {
priv ed25519.PrivateKey
pub ed25519.PublicKey
count uint64
priv []byte
pub []byte
id []byte
ip string
}
func main() {
if err := protect.Pledge("stdio"); err != nil {
panic(err)
threads := runtime.GOMAXPROCS(0)
var threadChannels []chan []byte
var currentBest []byte
newKeys := make(chan keySet, threads)
flag.Parse()
for i := 0; i < threads; i++ {
threadChannels = append(threadChannels, make(chan []byte, threads))
switch {
case *doSig:
go doSigKeys(newKeys, threadChannels[i])
default:
go doBoxKeys(newKeys, threadChannels[i])
}
}
threads := runtime.GOMAXPROCS(0)
fmt.Println("Threads:", threads)
start := time.Now()
var totalKeys uint64
totalKeys = 0
var currentBest ed25519.PublicKey
newKeys := make(chan keySet, threads)
for i := 0; i < threads; i++ {
go doKeys(newKeys)
}
for {
newKey := <-newKeys
if isBetter(currentBest, newKey.pub) || len(currentBest) == 0 {
totalKeys += newKey.count
currentBest = newKey.pub
fmt.Println("-----", time.Since(start), "---", totalKeys, "keys tried")
fmt.Println("Priv:", hex.EncodeToString(newKey.priv))
fmt.Println("Pub:", hex.EncodeToString(newKey.pub))
addr := address.AddrForKey(newKey.pub)
fmt.Println("IP:", net.IP(addr[:]).String())
if isBetter(currentBest, newKey.id[:]) || len(currentBest) == 0 {
currentBest = newKey.id
for _, channel := range threadChannels {
select {
case channel <- newKey.id:
}
}
fmt.Println("--------------------------------------------------------------------------------")
switch {
case *doSig:
fmt.Println("sigPriv:", hex.EncodeToString(newKey.priv))
fmt.Println("sigPub:", hex.EncodeToString(newKey.pub))
fmt.Println("TreeID:", hex.EncodeToString(newKey.id))
default:
fmt.Println("boxPriv:", hex.EncodeToString(newKey.priv))
fmt.Println("boxPub:", hex.EncodeToString(newKey.pub))
fmt.Println("NodeID:", hex.EncodeToString(newKey.id))
fmt.Println("IP:", newKey.ip)
}
}
}
}
func isBetter(oldPub, newPub ed25519.PublicKey) bool {
for idx := range oldPub {
if newPub[idx] < oldPub[idx] {
return true
}
if newPub[idx] > oldPub[idx] {
break
func isBetter(oldID, newID []byte) bool {
for idx := range oldID {
if newID[idx] != oldID[idx] {
return newID[idx] > oldID[idx]
}
}
return false
}
func doKeys(out chan<- keySet) {
bestKey := make(ed25519.PublicKey, ed25519.PublicKeySize)
var count uint64
count = 0
for idx := range bestKey {
bestKey[idx] = 0xff
}
func doBoxKeys(out chan<- keySet, in <-chan []byte) {
var bestID crypto.NodeID
for {
pub, priv, err := ed25519.GenerateKey(nil)
count++
if err != nil {
panic(err)
select {
case newBestID := <-in:
if isBetter(bestID[:], newBestID) {
copy(bestID[:], newBestID)
}
default:
pub, priv := crypto.NewBoxKeys()
id := crypto.GetNodeID(pub)
if !isBetter(bestID[:], id[:]) {
continue
}
bestID = *id
ip := net.IP(address.AddrForNodeID(id)[:]).String()
out <- keySet{priv[:], pub[:], id[:], ip}
}
if !isBetter(bestKey, pub) {
continue
}
bestKey = pub
out <- keySet{priv, pub, count}
count = 0
}
}
func doSigKeys(out chan<- keySet, in <-chan []byte) {
var bestID crypto.TreeID
for idx := range bestID {
bestID[idx] = 0
}
for {
select {
case newBestID := <-in:
if isBetter(bestID[:], newBestID) {
copy(bestID[:], newBestID)
}
default:
}
pub, priv := crypto.NewSigKeys()
id := crypto.GetTreeID(pub)
if !isBetter(bestID[:], id[:]) {
continue
}
bestID = *id
out <- keySet{priv[:], pub[:], id[:], ""}
}
}

View file

@ -1,10 +0,0 @@
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris
// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris
package main
import "errors"
func chuser(user string) error {
return errors.New("setting uid/gid is not supported on this platform")
}

View file

@ -1,63 +0,0 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package main
import (
"fmt"
"os/user"
"strconv"
"strings"
"golang.org/x/sys/unix"
)
func chuser(input string) error {
givenUser, givenGroup, _ := strings.Cut(input, ":")
if givenUser == "" {
return fmt.Errorf("user is empty")
}
if strings.Contains(input, ":") && givenGroup == "" {
return fmt.Errorf("group is empty")
}
var (
err error
usr *user.User
grp *user.Group
uid, gid int
)
if usr, err = user.LookupId(givenUser); err != nil {
if usr, err = user.Lookup(givenUser); err != nil {
return err
}
}
if uid, err = strconv.Atoi(usr.Uid); err != nil {
return err
}
if givenGroup != "" {
if grp, err = user.LookupGroupId(givenGroup); err != nil {
if grp, err = user.LookupGroup(givenGroup); err != nil {
return err
}
}
gid, _ = strconv.Atoi(grp.Gid)
} else {
gid, _ = strconv.Atoi(usr.Gid)
}
if err := unix.Setgroups([]int{gid}); err != nil {
return fmt.Errorf("setgroups: %d: %v", gid, err)
}
if err := unix.Setgid(gid); err != nil {
return fmt.Errorf("setgid: %d: %v", gid, err)
}
if err := unix.Setuid(uid); err != nil {
return fmt.Errorf("setuid: %d: %v", uid, err)
}
return nil
}

View file

@ -1,80 +0,0 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
package main
import (
"os/user"
"testing"
)
// Usernames must not contain a number sign.
func TestEmptyString(t *testing.T) {
if chuser("") == nil {
t.Fatal("the empty string is not a valid user")
}
}
// Either omit delimiter and group, or omit both.
func TestEmptyGroup(t *testing.T) {
if chuser("0:") == nil {
t.Fatal("the empty group is not allowed")
}
}
// Either user only or user and group.
func TestGroupOnly(t *testing.T) {
if chuser(":0") == nil {
t.Fatal("group only is not allowed")
}
}
// Usenames must not contain the number sign.
func TestInvalidUsername(t *testing.T) {
const username = "#user"
if chuser(username) == nil {
t.Fatalf("'%s' is not a valid username", username)
}
}
// User IDs must be non-negative.
func TestInvalidUserid(t *testing.T) {
if chuser("-1") == nil {
t.Fatal("User ID cannot be negative")
}
}
// Change to the current user by ID.
func TestCurrentUserid(t *testing.T) {
usr, err := user.Current()
if err != nil {
t.Fatal(err)
}
if usr.Uid != "0" {
t.Skip("setgroups(2): Only the superuser may set new groups.")
}
if err = chuser(usr.Uid); err != nil {
t.Fatal(err)
}
}
// Change to a common user by name.
func TestCommonUsername(t *testing.T) {
usr, err := user.Current()
if err != nil {
t.Fatal(err)
}
if usr.Uid != "0" {
t.Skip("setgroups(2): Only the superuser may set new groups.")
}
if err := chuser("nobody"); err != nil {
if _, ok := err.(user.UnknownUserError); ok {
t.Skip(err)
}
t.Fatal(err)
}
}

View file

@ -1,340 +1,135 @@
package main
import (
"context"
"crypto/ed25519"
"bytes"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"suah.dev/protect"
"golang.org/x/text/encoding/unicode"
"github.com/gologme/log"
gsyslog "github.com/hashicorp/go-syslog"
"github.com/hjson/hjson-go/v4"
"github.com/hjson/hjson-go"
"github.com/kardianos/minwinsvc"
"github.com/mitchellh/mapstructure"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/ipv6rwc"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
"github.com/yggdrasil-network/yggdrasil-go/src/module"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
"github.com/yggdrasil-network/yggdrasil-go/src/tuntap"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
)
type node struct {
core *core.Core
tun *tun.TunAdapter
multicast *multicast.Multicast
admin *admin.AdminSocket
core yggdrasil.Core
state *config.NodeState
tuntap module.Module // tuntap.TunAdapter
multicast module.Module // multicast.Multicast
admin module.Module // admin.AdminSocket
}
// The main function is responsible for configuring and starting Yggdrasil.
func main() {
// Not all operations are coverable with pledge(2), so immediately
// limit file system access with unveil(2), effectively preventing
// "proc exec" promises right from the start:
//
// - read arbitrary config file
// - create/write arbitrary log file
// - read/write/chmod/remove admin socket, if at all
if err := protect.Unveil("/", "rwc"); err != nil {
panic(fmt.Sprintf("unveil: / rwc: %v", err))
}
if err := protect.UnveilBlock(); err != nil {
panic(fmt.Sprintf("unveil: %v", err))
}
genconf := flag.Bool("genconf", false, "print a new config to stdout")
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
exportkey := flag.Bool("exportkey", false, "use in combination with either -useconf or -useconffile, outputs your private key in PEM format")
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
ver := flag.Bool("version", false, "prints the version of this build")
logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
getaddr := flag.Bool("address", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 address")
getsnet := flag.Bool("subnet", false, "use in combination with either -useconf or -useconffile, outputs your IPv6 subnet")
getpkey := flag.Bool("publickey", false, "use in combination with either -useconf or -useconffile, outputs your public key")
loglevel := flag.String("loglevel", "info", "loglevel to enable")
chuserto := flag.String("user", "", "user (and, optionally, group) to set UID/GID to")
flag.Parse()
done := make(chan struct{})
defer close(done)
// Catch interrupts from the operating system to exit gracefully.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch *logto {
case "stdout":
logger = log.New(os.Stdout, "", log.Flags())
case "syslog":
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
logger = log.New(syslogger, "", log.Flags()&^(log.Ldate|log.Ltime))
}
default:
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
logger = log.New(logfd, "", log.Flags())
}
}
if logger == nil {
logger = log.New(os.Stdout, "", log.Flags())
logger.Warnln("Logging defaulting to stdout")
}
if *normaliseconf {
setLogLevel("error", logger)
} else {
setLogLevel(*loglevel, logger)
}
cfg := config.GenerateConfig()
func readConfig(useconf *bool, useconffile *string, normaliseconf *bool) *config.NodeConfig {
// Use a configuration file. If -useconf, the configuration will be read
// from stdin. If -useconffile, the configuration will be read from the
// filesystem.
var conf []byte
var err error
switch {
case *ver:
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
return
case *autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN interface.
case *useconf:
if _, err := cfg.ReadFrom(os.Stdin); err != nil {
panic(err)
}
case *useconffile != "":
f, err := os.Open(*useconffile)
if err != nil {
panic(err)
}
if _, err := cfg.ReadFrom(f); err != nil {
panic(err)
}
_ = f.Close()
case *genconf:
cfg.AdminListen = ""
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
default:
fmt.Println("Usage:")
flag.PrintDefaults()
if *getaddr || *getsnet {
fmt.Println("\nError: You need to specify some config data using -useconf or -useconffile.")
}
return
if *useconffile != "" {
// Read the file from the filesystem
conf, err = ioutil.ReadFile(*useconffile)
} else {
// Read the file from stdin.
conf, err = ioutil.ReadAll(os.Stdin)
}
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
publicKey := privateKey.Public().(ed25519.PublicKey)
switch {
case *getaddr:
addr := address.AddrForKey(publicKey)
ip := net.IP(addr[:])
fmt.Println(ip.String())
return
case *getsnet:
snet := address.SubnetForKey(publicKey)
ipnet := net.IPNet{
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
Mask: net.CIDRMask(len(snet)*8, 128),
}
fmt.Println(ipnet.String())
return
case *getpkey:
fmt.Println(hex.EncodeToString(publicKey))
return
case *normaliseconf:
cfg.AdminListen = ""
if cfg.PrivateKeyPath != "" {
cfg.PrivateKey = nil
}
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
case *exportkey:
pem, err := cfg.MarshalPEMPrivateKey()
if err != nil {
panic(err)
}
fmt.Println(string(pem))
return
if err != nil {
panic(err)
}
n := &node{}
// Set up the Yggdrasil node itself.
{
iprange := net.IPNet{
IP: net.ParseIP("200::"),
Mask: net.CIDRMask(7, 128),
}
options := []core.SetupOption{
core.NodeInfo(cfg.NodeInfo),
core.NodeInfoPrivacy(cfg.NodeInfoPrivacy),
core.PeerFilter(func(ip net.IP) bool {
return !iprange.Contains(ip)
}),
}
for _, addr := range cfg.Listen {
options = append(options, core.ListenAddress(addr))
}
for _, peer := range cfg.Peers {
options = append(options, core.Peer{URI: peer})
}
for intf, peers := range cfg.InterfacePeers {
for _, peer := range peers {
options = append(options, core.Peer{URI: peer, SourceInterface: intf})
}
}
for _, allowed := range cfg.AllowedPublicKeys {
k, err := hex.DecodeString(allowed)
if err != nil {
panic(err)
}
options = append(options, core.AllowedPublicKey(k[:]))
}
if n.core, err = core.New(cfg.Certificate, logger, options...); err != nil {
panic(err)
}
address, subnet := n.core.Address(), n.core.Subnet()
logger.Printf("Your public key is %s", hex.EncodeToString(n.core.PublicKey()))
logger.Printf("Your IPv6 address is %s", address.String())
logger.Printf("Your IPv6 subnet is %s", subnet.String())
}
// Set up the admin socket.
{
options := []admin.SetupOption{
admin.ListenAddress(cfg.AdminListen),
}
if cfg.LogLookups {
options = append(options, admin.LogLookups{})
}
if n.admin, err = admin.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil {
n.admin.SetupAdminHandlers()
}
}
// Set up the multicast module.
{
options := []multicast.SetupOption{}
for _, intf := range cfg.MulticastInterfaces {
options = append(options, multicast.MulticastInterface{
Regex: regexp.MustCompile(intf.Regex),
Beacon: intf.Beacon,
Listen: intf.Listen,
Port: intf.Port,
Priority: uint8(intf.Priority),
Password: intf.Password,
})
}
if n.multicast, err = multicast.New(n.core, logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.multicast != nil {
n.multicast.SetupAdminHandlers(n.admin)
}
}
// Set up the TUN module.
{
options := []tun.SetupOption{
tun.InterfaceName(cfg.IfName),
tun.InterfaceMTU(cfg.IfMTU),
}
if n.tun, err = tun.New(ipv6rwc.NewReadWriteCloser(n.core), logger, options...); err != nil {
panic(err)
}
if n.admin != nil && n.tun != nil {
n.tun.SetupAdminHandlers(n.admin)
}
}
//Windows service shutdown
minwinsvc.SetOnExit(func() {
logger.Infof("Shutting down service ...")
cancel()
// Wait for all parts to shutdown properly
<-done
})
// Change user if requested
if *chuserto != "" {
err = chuser(*chuserto)
// If there's a byte order mark - which Windows 10 is now incredibly fond of
// throwing everywhere when it's converting things into UTF-16 for the hell
// of it - remove it and decode back down into UTF-8. This is necessary
// because hjson doesn't know what to do with UTF-16 and will panic
if bytes.Equal(conf[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
conf, err = decoder.Bytes(conf)
if err != nil {
panic(err)
}
}
// Promise final modes of operation. At this point, if at all:
// - raw socket is created/open
// - admin socket is created/open
// - privileges are dropped to non-root user
//
// Peers, InterfacePeers, Listen can be UNIX sockets;
// Go's net.Listen.Close() deletes files on shutdown.
promises := []string{"stdio", "cpath", "inet", "unix", "dns"}
if len(cfg.MulticastInterfaces) > 0 {
promises = append(promises, "mcast")
// Generate a new configuration - this gives us a set of sane defaults -
// then parse the configuration we loaded above on top of it. The effect
// of this is that any configuration item that is missing from the provided
// configuration will use a sane default.
cfg := config.GenerateConfig()
var dat map[string]interface{}
if err := hjson.Unmarshal(conf, &dat); err != nil {
panic(err)
}
if err := protect.Pledge(strings.Join(promises, " ")); err != nil {
panic(fmt.Sprintf("pledge: %v: %v", promises, err))
// Check for fields that have changed type recently, e.g. the Listen config
// option is now a []string rather than a string
if listen, ok := dat["Listen"].(string); ok {
dat["Listen"] = []string{listen}
}
if tunnelrouting, ok := dat["TunnelRouting"].(map[string]interface{}); ok {
if c, ok := tunnelrouting["IPv4Sources"]; ok {
delete(tunnelrouting, "IPv4Sources")
tunnelrouting["IPv4LocalSubnets"] = c
}
if c, ok := tunnelrouting["IPv6Sources"]; ok {
delete(tunnelrouting, "IPv6Sources")
tunnelrouting["IPv6LocalSubnets"] = c
}
if c, ok := tunnelrouting["IPv4Destinations"]; ok {
delete(tunnelrouting, "IPv4Destinations")
tunnelrouting["IPv4RemoteSubnets"] = c
}
if c, ok := tunnelrouting["IPv6Destinations"]; ok {
delete(tunnelrouting, "IPv6Destinations")
tunnelrouting["IPv6RemoteSubnets"] = c
}
}
// Sanitise the config
confJson, err := json.Marshal(dat)
if err != nil {
panic(err)
}
json.Unmarshal(confJson, &cfg)
// Overlay our newly mapped configuration onto the autoconf node config that
// we generated above.
if err = mapstructure.Decode(dat, &cfg); err != nil {
panic(err)
}
return cfg
}
// Block until we are told to shut down.
<-ctx.Done()
// Shut down the node.
_ = n.admin.Stop()
_ = n.multicast.Stop()
_ = n.tun.Stop()
n.core.Stop()
// Generates a new configuration and returns it in HJSON format. This is used
// with -genconf.
func doGenconf(isjson bool) string {
cfg := config.GenerateConfig()
var bs []byte
var err error
if isjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
return string(bs)
}
func setLogLevel(loglevel string, logger *log.Logger) {
@ -362,3 +157,260 @@ func setLogLevel(loglevel string, logger *log.Logger) {
}
}
}
// The main function is responsible for configuring and starting Yggdrasil.
func main() {
// Configure the command line parameters.
genconf := flag.Bool("genconf", false, "print a new config to stdout")
useconf := flag.Bool("useconf", false, "read HJSON/JSON config from stdin")
useconffile := flag.String("useconffile", "", "read HJSON/JSON config from specified file path")
normaliseconf := flag.Bool("normaliseconf", false, "use in combination with either -useconf or -useconffile, outputs your configuration normalised")
confjson := flag.Bool("json", false, "print configuration from -genconf or -normaliseconf as JSON instead of HJSON")
autoconf := flag.Bool("autoconf", false, "automatic mode (dynamic IP, peer with IPv6 neighbors)")
ver := flag.Bool("version", false, "prints the version of this build")
logto := flag.String("logto", "stdout", "file path to log to, \"syslog\" or \"stdout\"")
getaddr := flag.Bool("address", false, "returns the IPv6 address as derived from the supplied configuration")
getsnet := flag.Bool("subnet", false, "returns the IPv6 subnet as derived from the supplied configuration")
loglevel := flag.String("loglevel", "info", "loglevel to enable")
flag.Parse()
var cfg *config.NodeConfig
var err error
switch {
case *ver:
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
return
case *autoconf:
// Use an autoconf-generated config, this will give us random keys and
// port numbers, and will use an automatically selected TUN/TAP interface.
cfg = config.GenerateConfig()
case *useconffile != "" || *useconf:
// Read the configuration from either stdin or from the filesystem
cfg = readConfig(useconf, useconffile, normaliseconf)
// If the -normaliseconf option was specified then remarshal the above
// configuration and print it back to stdout. This lets the user update
// their configuration file with newly mapped names (like above) or to
// convert from plain JSON to commented HJSON.
if *normaliseconf {
var bs []byte
if *confjson {
bs, err = json.MarshalIndent(cfg, "", " ")
} else {
bs, err = hjson.Marshal(cfg)
}
if err != nil {
panic(err)
}
fmt.Println(string(bs))
return
}
case *genconf:
// Generate a new configuration and print it to stdout.
fmt.Println(doGenconf(*confjson))
default:
// No flags were provided, therefore print the list of flags to stdout.
flag.PrintDefaults()
}
// Have we got a working configuration? If we don't then it probably means
// that neither -autoconf, -useconf or -useconffile were set above. Stop
// if we don't.
if cfg == nil {
return
}
// Have we been asked for the node address yet? If so, print it and then stop.
getNodeID := func() *crypto.NodeID {
if pubkey, err := hex.DecodeString(cfg.EncryptionPublicKey); err == nil {
var box crypto.BoxPubKey
copy(box[:], pubkey)
return crypto.GetNodeID(&box)
}
return nil
}
switch {
case *getaddr:
if nodeid := getNodeID(); nodeid != nil {
addr := *address.AddrForNodeID(nodeid)
ip := net.IP(addr[:])
fmt.Println(ip.String())
}
return
case *getsnet:
if nodeid := getNodeID(); nodeid != nil {
snet := *address.SubnetForNodeID(nodeid)
ipnet := net.IPNet{
IP: append(snet[:], 0, 0, 0, 0, 0, 0, 0, 0),
Mask: net.CIDRMask(len(snet)*8, 128),
}
fmt.Println(ipnet.String())
}
return
default:
}
// Create a new logger that logs output to stdout.
var logger *log.Logger
switch *logto {
case "stdout":
logger = log.New(os.Stdout, "", log.Flags())
case "syslog":
if syslogger, err := gsyslog.NewLogger(gsyslog.LOG_NOTICE, "DAEMON", version.BuildName()); err == nil {
logger = log.New(syslogger, "", log.Flags())
}
default:
if logfd, err := os.OpenFile(*logto, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err == nil {
logger = log.New(logfd, "", log.Flags())
}
}
if logger == nil {
logger = log.New(os.Stdout, "", log.Flags())
logger.Warnln("Logging defaulting to stdout")
}
setLogLevel(*loglevel, logger)
// Setup the Yggdrasil node itself. The node{} type includes a Core, so we
// don't need to create this manually.
n := node{}
// Now start Yggdrasil - this starts the DHT, router, switch and other core
// components needed for Yggdrasil to operate
n.state, err = n.core.Start(cfg, logger)
if err != nil {
logger.Errorln("An error occurred during startup")
panic(err)
}
// Register the session firewall gatekeeper function
n.core.SetSessionGatekeeper(n.sessionFirewall)
// Allocate our modules
n.admin = &admin.AdminSocket{}
n.multicast = &multicast.Multicast{}
n.tuntap = &tuntap.TunAdapter{}
// Start the admin socket
n.admin.Init(&n.core, n.state, logger, nil)
if err := n.admin.Start(); err != nil {
logger.Errorln("An error occurred starting admin socket:", err)
}
n.admin.SetupAdminHandlers(n.admin.(*admin.AdminSocket))
// Start the multicast interface
n.multicast.Init(&n.core, n.state, logger, nil)
if err := n.multicast.Start(); err != nil {
logger.Errorln("An error occurred starting multicast:", err)
}
n.multicast.SetupAdminHandlers(n.admin.(*admin.AdminSocket))
// Start the TUN/TAP interface
if listener, err := n.core.ConnListen(); err == nil {
if dialer, err := n.core.ConnDialer(); err == nil {
n.tuntap.Init(&n.core, n.state, logger, tuntap.TunOptions{Listener: listener, Dialer: dialer})
if err := n.tuntap.Start(); err != nil {
logger.Errorln("An error occurred starting TUN/TAP:", err)
}
n.tuntap.SetupAdminHandlers(n.admin.(*admin.AdminSocket))
} else {
logger.Errorln("Unable to get Dialer:", err)
}
} else {
logger.Errorln("Unable to get Listener:", err)
}
// Make some nice output that tells us what our IPv6 address and subnet are.
// This is just logged to stdout for the user.
address := n.core.Address()
subnet := n.core.Subnet()
logger.Infof("Your IPv6 address is %s", address.String())
logger.Infof("Your IPv6 subnet is %s", subnet.String())
// Catch interrupts from the operating system to exit gracefully.
c := make(chan os.Signal, 1)
r := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
signal.Notify(r, os.Interrupt, syscall.SIGHUP)
// Capture the service being stopped on Windows.
minwinsvc.SetOnExit(n.shutdown)
defer n.shutdown()
// Wait for the terminate/interrupt signal. Once a signal is received, the
// deferred Stop function above will run which will shut down TUN/TAP.
for {
select {
case <-c:
goto exit
case <-r:
if *useconffile != "" {
cfg = readConfig(useconf, useconffile, normaliseconf)
logger.Infoln("Reloading configuration from", *useconffile)
n.core.UpdateConfig(cfg)
n.tuntap.UpdateConfig(cfg)
n.multicast.UpdateConfig(cfg)
} else {
logger.Errorln("Reloading config at runtime is only possible with -useconffile")
}
}
}
exit:
}
func (n *node) shutdown() {
n.admin.Stop()
n.multicast.Stop()
n.tuntap.Stop()
n.core.Stop()
}
func (n *node) sessionFirewall(pubkey *crypto.BoxPubKey, initiator bool) bool {
n.state.Mutex.RLock()
defer n.state.Mutex.RUnlock()
// Allow by default if the session firewall is disabled
if !n.state.Current.SessionFirewall.Enable {
return true
}
// Prepare for checking whitelist/blacklist
var box crypto.BoxPubKey
// Reject blacklisted nodes
for _, b := range n.state.Current.SessionFirewall.BlacklistEncryptionPublicKeys {
key, err := hex.DecodeString(b)
if err == nil {
copy(box[:crypto.BoxPubKeyLen], key)
if box == *pubkey {
return false
}
}
}
// Allow whitelisted nodes
for _, b := range n.state.Current.SessionFirewall.WhitelistEncryptionPublicKeys {
key, err := hex.DecodeString(b)
if err == nil {
copy(box[:crypto.BoxPubKeyLen], key)
if box == *pubkey {
return true
}
}
}
// Allow outbound sessions if appropriate
if n.state.Current.SessionFirewall.AlwaysAllowOutbound {
if initiator {
return true
}
}
// Look and see if the pubkey is that of a direct peer
var isDirectPeer bool
for _, peer := range n.core.GetPeers() {
if peer.PublicKey == *pubkey {
isDirectPeer = true
break
}
}
// Allow direct peers if appropriate
if n.state.Current.SessionFirewall.AllowFromDirect && isDirectPeer {
return true
}
// Allow remote nodes if appropriate
if n.state.Current.SessionFirewall.AllowFromRemote && !isDirectPeer {
return true
}
// Finally, default-deny if not matching any of the above rules
return false
}

View file

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

View file

@ -6,29 +6,25 @@ import (
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"net"
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
"suah.dev/protect"
"golang.org/x/text/encoding/unicode"
"github.com/olekukonko/tablewriter"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/yggdrasil-network/yggdrasil-go/src/multicast"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
"github.com/hjson/hjson-go"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
func main() {
// read config, speak DNS/TCP and/or over a UNIX socket
if err := protect.Pledge("stdio rpath inet unix dns"); err != nil {
panic(err)
}
type admin_info map[string]interface{}
func main() {
// makes sure we can use defer and still return an error code to the OS
os.Exit(run())
}
@ -36,7 +32,6 @@ func main() {
func run() int {
logbuffer := &bytes.Buffer{}
logger := log.New(logbuffer, "", log.Flags())
defer func() int {
if r := recover(); r != nil {
logger.Println("Fatal error:", r)
@ -46,30 +41,80 @@ func run() int {
return 0
}()
cmdLineEnv := newCmdLineEnv()
cmdLineEnv.parseFlagsAndArgs()
endpoint := defaults.GetDefaults().DefaultAdminListen
if cmdLineEnv.ver {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [options] command [key=value] [key=value] ...\n\n", os.Args[0])
fmt.Println("Options:")
flag.PrintDefaults()
fmt.Println("\nPlease note that options must always specified BEFORE the command\non the command line or they will be ignored.\n")
fmt.Println("Commands:\n - Use \"list\" for a list of available commands\n")
fmt.Println("Examples:")
fmt.Println(" - ", os.Args[0], "list")
fmt.Println(" - ", os.Args[0], "getPeers")
fmt.Println(" - ", os.Args[0], "-v getSelf")
fmt.Println(" - ", os.Args[0], "setTunTap name=auto mtu=1500 tap_mode=false")
fmt.Println(" - ", os.Args[0], "-endpoint=tcp://localhost:9001 getDHT")
fmt.Println(" - ", os.Args[0], "-endpoint=unix:///var/run/ygg.sock getDHT")
}
server := flag.String("endpoint", endpoint, "Admin socket endpoint")
injson := flag.Bool("json", false, "Output in JSON format (as opposed to pretty-print)")
verbose := flag.Bool("v", false, "Verbose output (includes public keys)")
ver := flag.Bool("version", false, "Prints the version of this build")
flag.Parse()
args := flag.Args()
if *ver {
fmt.Println("Build name:", version.BuildName())
fmt.Println("Build version:", version.BuildVersion())
fmt.Println("To get the version number of the running Yggdrasil node, run", os.Args[0], "getSelf")
return 0
}
if len(cmdLineEnv.args) == 0 {
if len(args) == 0 {
flag.Usage()
return 0
}
cmdLineEnv.setEndpoint(logger)
if *server == endpoint {
if config, err := ioutil.ReadFile(defaults.GetDefaults().DefaultConfigFile); err == nil {
if bytes.Equal(config[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(config[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
config, err = decoder.Bytes(config)
if err != nil {
panic(err)
}
}
var dat map[string]interface{}
if err := hjson.Unmarshal(config, &dat); err != nil {
panic(err)
}
if ep, ok := dat["AdminListen"].(string); ok && (ep != "none" && ep != "") {
endpoint = ep
logger.Println("Found platform default config file", defaults.GetDefaults().DefaultConfigFile)
logger.Println("Using endpoint", endpoint, "from AdminListen")
} else {
logger.Println("Configuration file doesn't contain appropriate AdminListen option")
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen)
}
} else {
logger.Println("Can't open config file from default location", defaults.GetDefaults().DefaultConfigFile)
logger.Println("Falling back to platform default", defaults.GetDefaults().DefaultAdminListen)
}
} else {
endpoint = *server
logger.Println("Using endpoint", endpoint, "from command line")
}
var conn net.Conn
u, err := url.Parse(cmdLineEnv.endpoint)
u, err := url.Parse(endpoint)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
logger.Println("Connecting to UNIX socket", cmdLineEnv.endpoint[7:])
conn, err = net.Dial("unix", cmdLineEnv.endpoint[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)
@ -79,255 +124,323 @@ func run() int {
}
} else {
logger.Println("Connecting to TCP socket", u.Host)
conn, err = net.Dial("tcp", cmdLineEnv.endpoint)
conn, err = net.Dial("tcp", endpoint)
}
if err != nil {
panic(err)
}
// config and socket are done, work without unprivileges
if err := protect.Pledge("stdio"); err != nil {
panic(err)
}
logger.Println("Connected")
defer conn.Close()
decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn)
send := &admin.AdminSocketRequest{}
recv := &admin.AdminSocketResponse{}
args := map[string]string{}
for c, a := range cmdLineEnv.args {
send := make(admin_info)
recv := make(admin_info)
for c, a := range args {
if c == 0 {
if strings.HasPrefix(a, "-") {
logger.Printf("Ignoring flag %s as it should be specified before other parameters\n", a)
continue
}
logger.Printf("Sending request: %v\n", a)
send.Name = a
send["request"] = a
continue
}
tokens := strings.SplitN(a, "=", 2)
switch {
case len(tokens) == 1:
logger.Println("Ignoring invalid argument:", a)
default:
args[tokens[0]] = tokens[1]
tokens := strings.Split(a, "=")
if len(tokens) == 1 {
send[tokens[0]] = true
} else if len(tokens) > 2 {
send[tokens[0]] = strings.Join(tokens[1:], "=")
} else if len(tokens) == 2 {
if i, err := strconv.Atoi(tokens[1]); err == nil {
logger.Printf("Sending parameter %s: %d\n", tokens[0], i)
send[tokens[0]] = i
} else {
switch strings.ToLower(tokens[1]) {
case "true":
send[tokens[0]] = true
case "false":
send[tokens[0]] = false
default:
send[tokens[0]] = tokens[1]
}
logger.Printf("Sending parameter %s: %v\n", tokens[0], send[tokens[0]])
}
}
}
if send.Arguments, err = json.Marshal(args); err != nil {
panic(err)
}
if err := encoder.Encode(&send); err != nil {
panic(err)
}
logger.Printf("Request sent")
if err := decoder.Decode(&recv); err != nil {
panic(err)
}
if recv.Status == "error" {
if err := recv.Error; err != "" {
fmt.Println("Admin socket returned an error:", err)
} else {
fmt.Println("Admin socket returned an error but didn't specify any error text")
if err := decoder.Decode(&recv); err == nil {
logger.Printf("Response received")
if recv["status"] == "error" {
if err, ok := recv["error"]; ok {
fmt.Println("Admin socket returned an error:", err)
} else {
fmt.Println("Admin socket returned an error but didn't specify any error text")
}
return 1
}
if _, ok := recv["request"]; !ok {
fmt.Println("Missing request in response (malformed response?)")
return 1
}
if _, ok := recv["response"]; !ok {
fmt.Println("Missing response body (malformed response?)")
return 1
}
req := recv["request"].(map[string]interface{})
res := recv["response"].(map[string]interface{})
if *injson {
if json, err := json.MarshalIndent(res, "", " "); err == nil {
fmt.Println(string(json))
}
return 0
}
switch strings.ToLower(req["request"].(string)) {
case "dot":
fmt.Println(res["dot"])
case "list", "getpeers", "getswitchpeers", "getdht", "getsessions", "dhtping":
maxWidths := make(map[string]int)
var keyOrder []string
keysOrdered := false
for _, tlv := range res {
for slk, slv := range tlv.(map[string]interface{}) {
if !keysOrdered {
for k := range slv.(map[string]interface{}) {
if !*verbose {
if k == "box_pub_key" || k == "box_sig_key" || k == "nodeinfo" || k == "was_mtu_fixed" {
continue
}
}
keyOrder = append(keyOrder, fmt.Sprint(k))
}
sort.Strings(keyOrder)
keysOrdered = true
}
for k, v := range slv.(map[string]interface{}) {
if len(fmt.Sprint(slk)) > maxWidths["key"] {
maxWidths["key"] = len(fmt.Sprint(slk))
}
if len(fmt.Sprint(v)) > maxWidths[k] {
maxWidths[k] = len(fmt.Sprint(v))
if maxWidths[k] < len(k) {
maxWidths[k] = len(k)
}
}
}
}
if len(keyOrder) > 0 {
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", "")
for _, v := range keyOrder {
fmt.Printf("%-"+fmt.Sprint(maxWidths[v])+"s ", v)
}
fmt.Println()
}
for slk, slv := range tlv.(map[string]interface{}) {
fmt.Printf("%-"+fmt.Sprint(maxWidths["key"])+"s ", slk)
for _, k := range keyOrder {
preformatted := slv.(map[string]interface{})[k]
var formatted string
switch k {
case "bytes_sent", "bytes_recvd":
formatted = fmt.Sprintf("%d", uint(preformatted.(float64)))
case "uptime", "last_seen":
seconds := uint(preformatted.(float64)) % 60
minutes := uint(preformatted.(float64)/60) % 60
hours := uint(preformatted.(float64) / 60 / 60)
formatted = fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
default:
formatted = fmt.Sprint(preformatted)
}
fmt.Printf("%-"+fmt.Sprint(maxWidths[k])+"s ", formatted)
}
fmt.Println()
}
}
case "gettuntap", "settuntap":
for k, v := range res {
fmt.Println("Interface name:", k)
if mtu, ok := v.(map[string]interface{})["mtu"].(float64); ok {
fmt.Println("Interface MTU:", mtu)
}
if tap_mode, ok := v.(map[string]interface{})["tap_mode"].(bool); ok {
fmt.Println("TAP mode:", tap_mode)
}
}
case "getself":
for k, v := range res["self"].(map[string]interface{}) {
if buildname, ok := v.(map[string]interface{})["build_name"].(string); ok && buildname != "unknown" {
fmt.Println("Build name:", buildname)
}
if buildversion, ok := v.(map[string]interface{})["build_version"].(string); ok && buildversion != "unknown" {
fmt.Println("Build version:", buildversion)
}
fmt.Println("IPv6 address:", k)
if subnet, ok := v.(map[string]interface{})["subnet"].(string); ok {
fmt.Println("IPv6 subnet:", subnet)
}
if coords, ok := v.(map[string]interface{})["coords"].(string); ok {
fmt.Println("Coords:", coords)
}
if *verbose {
if nodeID, ok := v.(map[string]interface{})["node_id"].(string); ok {
fmt.Println("Node ID:", nodeID)
}
if boxPubKey, ok := v.(map[string]interface{})["box_pub_key"].(string); ok {
fmt.Println("Public encryption key:", boxPubKey)
}
if boxSigKey, ok := v.(map[string]interface{})["box_sig_key"].(string); ok {
fmt.Println("Public signing key:", boxSigKey)
}
}
}
case "getswitchqueues":
maximumqueuesize := float64(4194304)
portqueues := make(map[float64]float64)
portqueuesize := make(map[float64]float64)
portqueuepackets := make(map[float64]float64)
v := res["switchqueues"].(map[string]interface{})
if queuecount, ok := v["queues_count"].(float64); ok {
fmt.Printf("Active queue count: %d queues\n", uint(queuecount))
}
if queuesize, ok := v["queues_size"].(float64); ok {
fmt.Printf("Active queue size: %d bytes\n", uint(queuesize))
}
if highestqueuecount, ok := v["highest_queues_count"].(float64); ok {
fmt.Printf("Highest queue count: %d queues\n", uint(highestqueuecount))
}
if highestqueuesize, ok := v["highest_queues_size"].(float64); ok {
fmt.Printf("Highest queue size: %d bytes\n", uint(highestqueuesize))
}
if m, ok := v["maximum_queues_size"].(float64); ok {
maximumqueuesize = m
fmt.Printf("Maximum queue size: %d bytes\n", uint(maximumqueuesize))
}
if queues, ok := v["queues"].([]interface{}); ok {
if len(queues) != 0 {
fmt.Println("Active queues:")
for _, v := range queues {
queueport := v.(map[string]interface{})["queue_port"].(float64)
queuesize := v.(map[string]interface{})["queue_size"].(float64)
queuepackets := v.(map[string]interface{})["queue_packets"].(float64)
queueid := v.(map[string]interface{})["queue_id"].(string)
portqueues[queueport]++
portqueuesize[queueport] += queuesize
portqueuepackets[queueport] += queuepackets
queuesizepercent := (100 / maximumqueuesize) * queuesize
fmt.Printf("- Switch port %d, Stream ID: %v, size: %d bytes (%d%% full), %d packets\n",
uint(queueport), []byte(queueid), uint(queuesize),
uint(queuesizepercent), uint(queuepackets))
}
}
}
if len(portqueuesize) > 0 && len(portqueuepackets) > 0 {
fmt.Println("Aggregated statistics by switchport:")
for k, v := range portqueuesize {
queuesizepercent := (100 / (portqueues[k] * maximumqueuesize)) * v
fmt.Printf("- Switch port %d, size: %d bytes (%d%% full), %d packets\n",
uint(k), uint(v), uint(queuesizepercent), uint(portqueuepackets[k]))
}
}
case "addpeer", "removepeer", "addallowedencryptionpublickey", "removeallowedencryptionpublickey", "addsourcesubnet", "addroute", "removesourcesubnet", "removeroute":
if _, ok := res["added"]; ok {
for _, v := range res["added"].([]interface{}) {
fmt.Println("Added:", fmt.Sprint(v))
}
}
if _, ok := res["not_added"]; ok {
for _, v := range res["not_added"].([]interface{}) {
fmt.Println("Not added:", fmt.Sprint(v))
}
}
if _, ok := res["removed"]; ok {
for _, v := range res["removed"].([]interface{}) {
fmt.Println("Removed:", fmt.Sprint(v))
}
}
if _, ok := res["not_removed"]; ok {
for _, v := range res["not_removed"].([]interface{}) {
fmt.Println("Not removed:", fmt.Sprint(v))
}
}
case "getallowedencryptionpublickeys":
if _, ok := res["allowed_box_pubs"]; !ok {
fmt.Println("All connections are allowed")
} else if res["allowed_box_pubs"] == nil {
fmt.Println("All connections are allowed")
} else {
fmt.Println("Connections are allowed only from the following public box keys:")
for _, v := range res["allowed_box_pubs"].([]interface{}) {
fmt.Println("-", v)
}
}
case "getmulticastinterfaces":
if _, ok := res["multicast_interfaces"]; !ok {
fmt.Println("No multicast interfaces found")
} else if res["multicast_interfaces"] == nil {
fmt.Println("No multicast interfaces found")
} else {
fmt.Println("Multicast peer discovery is active on:")
for _, v := range res["multicast_interfaces"].([]interface{}) {
fmt.Println("-", v)
}
}
case "getsourcesubnets":
if _, ok := res["source_subnets"]; !ok {
fmt.Println("No source subnets found")
} else if res["source_subnets"] == nil {
fmt.Println("No source subnets found")
} else {
fmt.Println("Source subnets:")
for _, v := range res["source_subnets"].([]interface{}) {
fmt.Println("-", v)
}
}
case "getroutes":
if routes, ok := res["routes"].(map[string]interface{}); !ok {
fmt.Println("No routes found")
} else {
if res["routes"] == nil || len(routes) == 0 {
fmt.Println("No routes found")
} else {
fmt.Println("Routes:")
for k, v := range routes {
if pv, ok := v.(string); ok {
fmt.Println("-", k, " via ", pv)
}
}
}
}
case "settunnelrouting":
fallthrough
case "gettunnelrouting":
if enabled, ok := res["enabled"].(bool); !ok {
fmt.Println("Tunnel routing is disabled")
} else if !enabled {
fmt.Println("Tunnel routing is disabled")
} else {
fmt.Println("Tunnel routing is enabled")
}
default:
if json, err := json.MarshalIndent(recv["response"], "", " "); err == nil {
fmt.Println(string(json))
}
}
} else {
logger.Println("Error receiving response:", err)
}
if v, ok := recv["status"]; ok && v != "success" {
return 1
}
if cmdLineEnv.injson {
if json, err := json.MarshalIndent(recv.Response, "", " "); err == nil {
fmt.Println(string(json))
}
return 0
}
table := tablewriter.NewWriter(os.Stdout)
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetAutoFormatHeaders(false)
table.SetCenterSeparator("")
table.SetColumnSeparator("")
table.SetRowSeparator("")
table.SetHeaderLine(false)
table.SetBorder(false)
table.SetTablePadding("\t") // pad with tabs
table.SetNoWhiteSpace(true)
table.SetAutoWrapText(false)
switch strings.ToLower(send.Name) {
case "list":
var resp admin.ListResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Command", "Arguments", "Description"})
for _, entry := range resp.List {
for i := range entry.Fields {
entry.Fields[i] = entry.Fields[i] + "=..."
}
table.Append([]string{entry.Command, strings.Join(entry.Fields, ", "), entry.Description})
}
table.Render()
case "getself":
var resp admin.GetSelfResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Append([]string{"Build name:", resp.BuildName})
table.Append([]string{"Build version:", resp.BuildVersion})
table.Append([]string{"IPv6 address:", resp.IPAddress})
table.Append([]string{"IPv6 subnet:", resp.Subnet})
table.Append([]string{"Routing table size:", fmt.Sprintf("%d", resp.RoutingEntries)})
table.Append([]string{"Public key:", resp.PublicKey})
table.Render()
case "getpeers":
var resp admin.GetPeersResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"URI", "State", "Dir", "IP Address", "Uptime", "RTT", "RX", "TX", "Down", "Up", "Pr", "Cost", "Last Error"})
for _, peer := range resp.Peers {
state, lasterr, dir, rtt, rxr, txr := "Up", "-", "Out", "-", "-", "-"
if !peer.Up {
state, lasterr = "Down", fmt.Sprintf("%s ago: %s", peer.LastErrorTime.Round(time.Second), peer.LastError)
} else if rttms := float64(peer.Latency.Microseconds()) / 1000; rttms > 0 {
rtt = fmt.Sprintf("%.02fms", rttms)
}
if peer.Inbound {
dir = "In"
}
uristring := peer.URI
if uri, err := url.Parse(peer.URI); err == nil {
uri.RawQuery = ""
uristring = uri.String()
}
if peer.RXRate > 0 {
rxr = peer.RXRate.String() + "/s"
}
if peer.TXRate > 0 {
txr = peer.TXRate.String() + "/s"
}
table.Append([]string{
uristring,
state,
dir,
peer.IPAddress,
(time.Duration(peer.Uptime) * time.Second).String(),
rtt,
peer.RXBytes.String(),
peer.TXBytes.String(),
rxr,
txr,
fmt.Sprintf("%d", peer.Priority),
fmt.Sprintf("%d", peer.Cost),
lasterr,
})
}
table.Render()
case "gettree":
var resp admin.GetTreeResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
//table.SetHeader([]string{"Public Key", "IP Address", "Port", "Rest"})
table.SetHeader([]string{"Public Key", "IP Address", "Parent", "Sequence"})
for _, tree := range resp.Tree {
table.Append([]string{
tree.PublicKey,
tree.IPAddress,
tree.Parent,
fmt.Sprintf("%d", tree.Sequence),
//fmt.Sprintf("%d", dht.Port),
//fmt.Sprintf("%d", dht.Rest),
})
}
table.Render()
case "getpaths":
var resp admin.GetPathsResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Public Key", "IP Address", "Path", "Seq"})
for _, p := range resp.Paths {
table.Append([]string{
p.PublicKey,
p.IPAddress,
fmt.Sprintf("%v", p.Path),
fmt.Sprintf("%d", p.Sequence),
})
}
table.Render()
case "getsessions":
var resp admin.GetSessionsResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.SetHeader([]string{"Public Key", "IP Address", "Uptime", "RX", "TX"})
for _, p := range resp.Sessions {
table.Append([]string{
p.PublicKey,
p.IPAddress,
(time.Duration(p.Uptime) * time.Second).String(),
p.RXBytes.String(),
p.TXBytes.String(),
})
}
table.Render()
case "getnodeinfo":
var resp core.GetNodeInfoResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
for _, v := range resp {
fmt.Println(string(v))
break
}
case "getmulticastinterfaces":
var resp multicast.GetMulticastInterfacesResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
fmtBool := func(b bool) string {
if b {
return "Yes"
}
return "-"
}
table.SetHeader([]string{"Name", "Listen Address", "Beacon", "Listen", "Password"})
for _, p := range resp.Interfaces {
table.Append([]string{
p.Name,
p.Address,
fmtBool(p.Beacon),
fmtBool(p.Listen),
fmtBool(p.Password),
})
}
table.Render()
case "gettun":
var resp tun.GetTUNResponse
if err := json.Unmarshal(recv.Response, &resp); err != nil {
panic(err)
}
table.Append([]string{"TUN enabled:", fmt.Sprintf("%#v", resp.Enabled)})
if resp.Enabled {
table.Append([]string{"Interface name:", resp.Name})
table.Append([]string{"Interface MTU:", fmt.Sprintf("%d", resp.MTU)})
}
table.Render()
case "addpeer", "removepeer":
default:
fmt.Println(string(recv.Response))
}
return 0
}

61
cmd/yggdrasilsim/dial.go Normal file
View file

@ -0,0 +1,61 @@
package main
import (
"fmt"
"sort"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
)
func doListen(recvNode *simNode) {
// TODO be able to stop the listeners somehow so they don't leak across different tests
for {
c, err := recvNode.listener.Accept()
if err != nil {
panic(err)
}
c.Close()
}
}
func dialTest(sendNode, recvNode *simNode) {
if sendNode.id == recvNode.id {
fmt.Println("Skipping dial to self")
return
}
var mask crypto.NodeID
for idx := range mask {
mask[idx] = 0xff
}
for {
c, err := sendNode.dialer.DialByNodeIDandMask(nil, &recvNode.nodeID, &mask)
if c != nil {
c.Close()
return
}
if err != nil {
fmt.Println("Dial failed:", err)
}
time.Sleep(time.Second)
}
}
func dialStore(store nodeStore) {
var nodeIdxs []int
for idx, n := range store {
nodeIdxs = append(nodeIdxs, idx)
go doListen(n)
}
sort.Slice(nodeIdxs, func(i, j int) bool {
return nodeIdxs[i] < nodeIdxs[j]
})
for _, idx := range nodeIdxs {
sendNode := store[idx]
for _, jdx := range nodeIdxs {
recvNode := store[jdx]
fmt.Printf("Dialing from node %d to node %d / %d...\n", idx, jdx, len(store))
dialTest(sendNode, recvNode)
}
}
}

6
cmd/yggdrasilsim/main.go Normal file
View file

@ -0,0 +1,6 @@
package main
func main() {
store := makeStoreSquareGrid(4)
dialStore(store)
}

28
cmd/yggdrasilsim/node.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"io/ioutil"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
)
type simNode struct {
core yggdrasil.Core
id int
nodeID crypto.NodeID
dialer *yggdrasil.Dialer
listener *yggdrasil.Listener
}
func newNode(id int) *simNode {
n := simNode{id: id}
n.core.Start(config.GenerateConfig(), log.New(ioutil.Discard, "", 0))
n.nodeID = *n.core.NodeID()
n.dialer, _ = n.core.ConnDialer()
n.listener, _ = n.core.ConnListen()
return &n
}

41
cmd/yggdrasilsim/store.go Normal file
View file

@ -0,0 +1,41 @@
package main
type nodeStore map[int]*simNode
func makeStoreSingle() nodeStore {
s := make(nodeStore)
s[0] = newNode(0)
return s
}
func linkNodes(a *simNode, b *simNode) {
la := a.core.NewSimlink()
lb := b.core.NewSimlink()
la.SetDestination(lb)
lb.SetDestination(la)
la.Start()
lb.Start()
}
func makeStoreSquareGrid(sideLength int) nodeStore {
store := make(nodeStore)
nNodes := sideLength * sideLength
idxs := make([]int, 0, nNodes)
// TODO shuffle nodeIDs
for idx := 1; idx <= nNodes; idx++ {
idxs = append(idxs, idx)
}
for _, idx := range idxs {
n := newNode(idx)
store[idx] = n
}
for idx := 0; idx < nNodes; idx++ {
if (idx % sideLength) != 0 {
linkNodes(store[idxs[idx]], store[idxs[idx-1]])
}
if idx >= sideLength {
linkNodes(store[idxs[idx]], store[idxs[idx-sideLength]])
}
}
return store
}

BIN
contrib/.DS_Store vendored

Binary file not shown.

View file

@ -1,10 +1,11 @@
/*
This file generates crypto keys for [ansible-yggdrasil](https://github.com/jcgruenhage/ansible-yggdrasil/)
*/
package main
import (
"crypto/ed25519"
"encoding/hex"
"flag"
"fmt"
@ -13,6 +14,7 @@ import (
"github.com/cheggaaa/pb/v3"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
)
var numHosts = flag.Int("hosts", 1, "number of host vars to generate")
@ -21,6 +23,7 @@ var keyTries = flag.Int("tries", 1000, "number of tries before taking the best k
type keySet struct {
priv []byte
pub []byte
id []byte
ip string
}
@ -34,15 +37,27 @@ func main() {
return
}
var keys []keySet
var encryptionKeys []keySet
for i := 0; i < *numHosts+1; i++ {
keys = append(keys, newKey())
encryptionKeys = append(encryptionKeys, newBoxKey())
bar.Increment()
}
keys = sortKeySetArray(keys)
encryptionKeys = sortKeySetArray(encryptionKeys)
for i := 0; i < *keyTries-*numHosts-1; i++ {
keys[0] = newKey()
keys = bubbleUpTo(keys, 0)
encryptionKeys[0] = newBoxKey()
encryptionKeys = bubbleUpTo(encryptionKeys, 0)
bar.Increment()
}
var signatureKeys []keySet
for i := 0; i < *numHosts+1; i++ {
signatureKeys = append(signatureKeys, newSigKey())
bar.Increment()
}
signatureKeys = sortKeySetArray(signatureKeys)
for i := 0; i < *keyTries-*numHosts-1; i++ {
signatureKeys[0] = newSigKey()
signatureKeys = bubbleUpTo(signatureKeys, 0)
bar.Increment()
}
@ -55,36 +70,43 @@ func main() {
return
}
defer file.Close()
file.WriteString(fmt.Sprintf("yggdrasil_public_key: %v\n", hex.EncodeToString(keys[i].pub)))
file.WriteString("yggdrasil_private_key: \"{{ vault_yggdrasil_private_key }}\"\n")
file.WriteString(fmt.Sprintf("ansible_host: %v\n", keys[i].ip))
file.WriteString(fmt.Sprintf("yggdrasil_encryption_public_key: %v\n", hex.EncodeToString(encryptionKeys[i].pub)))
file.WriteString("yggdrasil_encryption_private_key: \"{{ vault_yggdrasil_encryption_private_key }}\"\n")
file.WriteString(fmt.Sprintf("yggdrasil_signing_public_key: %v\n", hex.EncodeToString(signatureKeys[i].pub)))
file.WriteString("yggdrasil_signing_private_key: \"{{ vault_yggdrasil_signing_private_key }}\"\n")
file.WriteString(fmt.Sprintf("ansible_host: %v\n", encryptionKeys[i].ip))
file, err = os.Create(fmt.Sprintf("host_vars/%x/vault", i))
if err != nil {
return
}
defer file.Close()
file.WriteString(fmt.Sprintf("vault_yggdrasil_private_key: %v\n", hex.EncodeToString(keys[i].priv)))
file.WriteString(fmt.Sprintf("vault_yggdrasil_encryption_private_key: %v\n", hex.EncodeToString(encryptionKeys[i].priv)))
file.WriteString(fmt.Sprintf("vault_yggdrasil_signing_private_key: %v\n", hex.EncodeToString(signatureKeys[i].priv)))
bar.Increment()
}
bar.Finish()
}
func newKey() keySet {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
ip := net.IP(address.AddrForKey(pub)[:]).String()
return keySet{priv[:], pub[:], ip}
func newBoxKey() keySet {
pub, priv := crypto.NewBoxKeys()
id := crypto.GetNodeID(pub)
ip := net.IP(address.AddrForNodeID(id)[:]).String()
return keySet{priv[:], pub[:], id[:], ip}
}
func newSigKey() keySet {
pub, priv := crypto.NewSigKeys()
id := crypto.GetTreeID(pub)
return keySet{priv[:], pub[:], id[:], ""}
}
func isBetter(oldID, newID []byte) bool {
for idx := range oldID {
if newID[idx] < oldID[idx] {
if newID[idx] > oldID[idx] {
return true
}
if newID[idx] > oldID[idx] {
if newID[idx] < oldID[idx] {
return false
}
}
@ -100,7 +122,7 @@ func sortKeySetArray(sets []keySet) []keySet {
func bubbleUpTo(sets []keySet, num int) []keySet {
for i := 0; i < len(sets)-num-1; i++ {
if isBetter(sets[i+1].pub, sets[i].pub) {
if isBetter(sets[i+1].id, sets[i].id) {
var tmp = sets[i]
sets[i] = sets[i+1]
sets[i+1] = tmp

View file

@ -1,11 +0,0 @@
# Last Modified: Mon Feb 3 22:19:45 2025
include <tunables/global>
/usr/bin/yggdrasilctl {
include <abstractions/base>
/etc/yggdrasil.conf rw,
/run/yggdrasil.sock rw,
owner /sys/kernel/mm/transparent_hugepage/hpage_pmd_size r,
}

View file

@ -21,16 +21,13 @@ if [ $PKGBRANCH = "master" ]; then
PKGREPLACES=yggdrasil-develop
fi
GOLDFLAGS="-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultConfig=/etc/yggdrasil/yggdrasil.conf"
GOLDFLAGS="${GOLDFLAGS} -X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultAdminListen=unix:///var/run/yggdrasil/yggdrasil.sock"
if [ $PKGARCH = "amd64" ]; then GOARCH=amd64 GOOS=linux ./build -l "${GOLDFLAGS}"
elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build -l "${GOLDFLAGS}"
elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build -l "${GOLDFLAGS}"
elif [ $PKGARCH = "mips" ]; then GOARCH=mips64 GOOS=linux ./build -l "${GOLDFLAGS}"
elif [ $PKGARCH = "armhf" ]; then GOARCH=arm GOOS=linux GOARM=6 ./build -l "${GOLDFLAGS}"
elif [ $PKGARCH = "arm64" ]; then GOARCH=arm64 GOOS=linux ./build -l "${GOLDFLAGS}"
elif [ $PKGARCH = "armel" ]; then GOARCH=arm GOOS=linux GOARM=5 ./build -l "${GOLDFLAGS}"
if [ $PKGARCH = "amd64" ]; then GOARCH=amd64 GOOS=linux ./build
elif [ $PKGARCH = "i386" ]; then GOARCH=386 GOOS=linux ./build
elif [ $PKGARCH = "mipsel" ]; then GOARCH=mipsle GOOS=linux ./build
elif [ $PKGARCH = "mips" ]; then GOARCH=mips64 GOOS=linux ./build
elif [ $PKGARCH = "armhf" ]; then GOARCH=arm GOOS=linux GOARM=6 ./build
elif [ $PKGARCH = "arm64" ]; then GOARCH=arm64 GOOS=linux ./build
elif [ $PKGARCH = "armel" ]; then GOARCH=arm GOOS=linux GOARM=5 ./build
else
echo "Specify PKGARCH=amd64,i386,mips,mipsel,armhf,arm64,armel"
exit 1
@ -41,7 +38,7 @@ echo "Building $PKGFILE"
mkdir -p /tmp/$PKGNAME/
mkdir -p /tmp/$PKGNAME/debian/
mkdir -p /tmp/$PKGNAME/usr/bin/
mkdir -p /tmp/$PKGNAME/lib/systemd/system/
mkdir -p /tmp/$PKGNAME/etc/systemd/system/
cat > /tmp/$PKGNAME/debian/changelog << EOF
Please see https://github.com/yggdrasil-network/yggdrasil-go/
@ -50,12 +47,11 @@ echo 9 > /tmp/$PKGNAME/debian/compat
cat > /tmp/$PKGNAME/debian/control << EOF
Package: $PKGNAME
Version: $PKGVERSION
Section: golang
Priority: optional
Section: contrib/net
Priority: extra
Architecture: $PKGARCH
Replaces: $PKGREPLACES
Conflicts: $PKGREPLACES
Depends: systemd
Maintainer: Neil Alexander <neilalexander@users.noreply.github.com>
Description: Yggdrasil Network
Yggdrasil is an early-stage implementation of a fully end-to-end encrypted IPv6
@ -72,52 +68,35 @@ EOF
cat > /tmp/$PKGNAME/debian/install << EOF
usr/bin/yggdrasil usr/bin
usr/bin/yggdrasilctl usr/bin
lib/systemd/system/*.service lib/systemd/system
etc/systemd/system/*.service etc/systemd/system
EOF
cat > /tmp/$PKGNAME/debian/postinst << EOF
#!/bin/sh
systemctl daemon-reload
if ! getent group yggdrasil 2>&1 > /dev/null; then
groupadd --system --force yggdrasil
groupadd --system --force yggdrasil || echo "Failed to create group 'yggdrasil' - please create it manually and reinstall"
fi
if [ ! -d /etc/yggdrasil ];
then
mkdir -p /etc/yggdrasil
chown root:yggdrasil /etc/yggdrasil
chmod 750 /etc/yggdrasil
fi
if [ ! -f /etc/yggdrasil/yggdrasil.conf ];
then
test -f /etc/yggdrasil.conf && mv /etc/yggdrasil.conf /etc/yggdrasil/yggdrasil.conf
fi
if [ -f /etc/yggdrasil/yggdrasil.conf ];
if [ -f /etc/yggdrasil.conf ];
then
mkdir -p /var/backups
echo "Backing up configuration file to /var/backups/yggdrasil.conf.`date +%Y%m%d`"
cp /etc/yggdrasil/yggdrasil.conf /var/backups/yggdrasil.conf.`date +%Y%m%d`
cp /etc/yggdrasil.conf /var/backups/yggdrasil.conf.`date +%Y%m%d`
echo "Normalising and updating /etc/yggdrasil.conf"
/usr/bin/yggdrasil -useconf -normaliseconf < /var/backups/yggdrasil.conf.`date +%Y%m%d` > /etc/yggdrasil.conf
chgrp yggdrasil /etc/yggdrasil.conf
echo "Normalising and updating /etc/yggdrasil/yggdrasil.conf"
/usr/bin/yggdrasil -useconf -normaliseconf < /var/backups/yggdrasil.conf.`date +%Y%m%d` > /etc/yggdrasil/yggdrasil.conf
chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf
chmod 640 /etc/yggdrasil/yggdrasil.conf
if command -v systemctl >/dev/null; then
systemctl daemon-reload >/dev/null || true
systemctl enable yggdrasil || true
systemctl start yggdrasil || true
fi
else
echo "Generating initial configuration file /etc/yggdrasil/yggdrasil.conf"
/usr/bin/yggdrasil -genconf > /etc/yggdrasil/yggdrasil.conf
chown root:yggdrasil /etc/yggdrasil/yggdrasil.conf
chmod 640 /etc/yggdrasil/yggdrasil.conf
echo "Generating initial configuration file /etc/yggdrasil.conf"
echo "Please familiarise yourself with this file before starting Yggdrasil"
sh -c 'umask 0027 && /usr/bin/yggdrasil -genconf > /etc/yggdrasil.conf'
chgrp yggdrasil /etc/yggdrasil.conf
fi
systemctl enable yggdrasil
systemctl restart yggdrasil
exit 0
EOF
cat > /tmp/$PKGNAME/debian/prerm << EOF
#!/bin/sh
@ -131,14 +110,13 @@ EOF
cp yggdrasil /tmp/$PKGNAME/usr/bin/
cp yggdrasilctl /tmp/$PKGNAME/usr/bin/
cp contrib/systemd/yggdrasil-default-config.service.debian /tmp/$PKGNAME/lib/systemd/system/yggdrasil-default-config.service
cp contrib/systemd/yggdrasil.service.debian /tmp/$PKGNAME/lib/systemd/system/yggdrasil.service
cp contrib/systemd/*.service /tmp/$PKGNAME/etc/systemd/system/
tar --no-xattrs -czvf /tmp/$PKGNAME/data.tar.gz -C /tmp/$PKGNAME/ \
tar -czvf /tmp/$PKGNAME/data.tar.gz -C /tmp/$PKGNAME/ \
usr/bin/yggdrasil usr/bin/yggdrasilctl \
lib/systemd/system/yggdrasil.service \
lib/systemd/system/yggdrasil-default-config.service
tar --no-xattrs -czvf /tmp/$PKGNAME/control.tar.gz -C /tmp/$PKGNAME/debian .
etc/systemd/system/yggdrasil.service \
etc/systemd/system/yggdrasil-default-config.service
tar -czvf /tmp/$PKGNAME/control.tar.gz -C /tmp/$PKGNAME/debian .
echo 2.0 > /tmp/$PKGNAME/debian-binary
ar -r $PKGFILE \

View file

@ -8,6 +8,7 @@ ENV CGO_ENABLED=0
RUN apk add git && ./build && go build -o /src/genkeys cmd/genkeys/main.go
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

View file

@ -15,10 +15,6 @@ command -v mkbom >/dev/null 2>&1 || (
sudo make install || (echo "Failed to build mkbom"; exit 1)
)
# Build Yggdrasil
echo "running GO111MODULE=on GOOS=darwin GOARCH=${PKGARCH-amd64} ./build"
GO111MODULE=on GOOS=darwin GOARCH=${PKGARCH-amd64} ./build
# Check if we can find the files we need - they should
# exist if you are running this script from the root of
# the yggdrasil-go repo and you have ran ./build
@ -79,7 +75,6 @@ PKGNAME=$(sh contrib/semver/name.sh)
PKGVERSION=$(sh contrib/semver/version.sh --bare)
PKGARCH=${PKGARCH-amd64}
PAYLOADSIZE=$(( $(wc -c pkgbuild/flat/base.pkg/Payload | awk '{ print $1 }') / 1024 ))
[ "$PKGARCH" = "amd64" ] && PKGHOSTARCH="x86_64" || PKGHOSTARCH=${PKGARCH}
# Create the PackageInfo file
cat > pkgbuild/flat/base.pkg/PackageInfo << EOF
@ -99,7 +94,7 @@ cat > pkgbuild/flat/Distribution << EOF
<?xml version="1.0" encoding="utf-8"?>
<installer-script minSpecVersion="1.000000" authoringTool="com.apple.PackageMaker" authoringToolVersion="3.0.3" authoringToolBuild="174">
<title>Yggdrasil (${PKGNAME}-${PKGVERSION})</title>
<options customize="never" allow-external-scripts="no" hostArchitectures="${PKGHOSTARCH}" />
<options customize="never" allow-external-scripts="no"/>
<domains enable_anywhere="true"/>
<installation-check script="pm_install_check();"/>
<script>

View file

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

View file

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

View file

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

View file

@ -1,40 +0,0 @@
//go:build ios || darwin
// +build ios darwin
package mobile
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
void Log(const char *text) {
NSString *nss = [NSString stringWithUTF8String:text];
NSLog(@"%@", nss);
}
*/
import "C"
import (
"unsafe"
"github.com/yggdrasil-network/yggdrasil-go/src/tun"
)
type MobileLogger struct {
}
func (nsl MobileLogger) Write(p []byte) (n int, err error) {
p = append(p, 0)
cstr := (*C.char)(unsafe.Pointer(&p[0]))
C.Log(cstr)
return len(p), nil
}
func (m *Yggdrasil) TakeOverTUN(fd int32) error {
options := []tun.SetupOption{
tun.FileDescriptor(fd),
tun.InterfaceMTU(m.iprwc.MTU()),
}
var err error
m.tun, err = tun.New(m.iprwc, m.logger, options...)
return err
}

View file

@ -1,10 +0,0 @@
//go:build go1.20
// +build go1.20
package mobile
import "runtime/debug"
func setMemLimitIfPossible() {
debug.SetMemoryLimit(1024 * 1024 * 40)
}

View file

@ -1,8 +0,0 @@
//go:build !go1.20
// +build !go1.20
package mobile
func setMemLimitIfPossible() {
// not supported by this Go version
}

View file

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

View file

@ -1,28 +0,0 @@
package mobile
import (
"os"
"testing"
"github.com/gologme/log"
)
func TestStartYggdrasil(t *testing.T) {
logger := log.New(os.Stdout, "", 0)
logger.EnableLevel("error")
logger.EnableLevel("warn")
logger.EnableLevel("info")
ygg := &Yggdrasil{
logger: logger,
}
if err := ygg.StartAutoconfigure(); err != nil {
t.Fatalf("Failed to start Yggdrasil: %s", err)
}
t.Log("Address:", ygg.GetAddressString())
t.Log("Subnet:", ygg.GetSubnetString())
t.Log("Routing entries:", ygg.GetRoutingEntries())
if err := ygg.Stop(); err != nil {
t.Fatalf("Failed to stop Yggdrasil: %s", err)
}
}

View file

@ -1,9 +1,7 @@
#!/bin/sh
#!/bin/bash
# This script generates an MSI file for Yggdrasil for a given architecture. It
# needs to run on Windows within MSYS2 and Go 1.21 or later must be installed on
# the system and within the PATH. This is ran currently by GitHub Actions (see
# the workflows in the repository).
# needs to run on Linux or macOS with Go 1.16, wixl and msitools installed.
#
# Author: Neil Alexander <neilalexander@users.noreply.github.com>
@ -11,18 +9,28 @@
PKGARCH=$1
if [ "${PKGARCH}" == "" ];
then
echo "tell me the architecture: x86, x64, arm or arm64"
echo "tell me the architecture: x86, x64 or arm"
exit 1
fi
# Download the wix tools!
dotnet tool install --global wix --version 5.0.0
# Get the rest of the repository history. This is needed within Appveyor because
# otherwise we don't get all of the branch histories and therefore the semver
# scripts don't work properly.
if [ "${APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH}" != "" ];
then
git fetch --all
git checkout ${APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH}
elif [ "${APPVEYOR_REPO_BRANCH}" != "" ];
then
git fetch --all
git checkout ${APPVEYOR_REPO_BRANCH}
fi
# Build Yggdrasil!
[ "${PKGARCH}" == "x64" ] && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "x86" ] && GOOS=windows GOARCH=386 CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "arm" ] && GOOS=windows GOARCH=arm CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "arm64" ] && GOOS=windows GOARCH=arm64 CGO_ENABLED=0 ./build
[ "${PKGARCH}" == "x64" ] && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 ./build -p -l "-aslr"
[ "${PKGARCH}" == "x86" ] && GOOS=windows GOARCH=386 CGO_ENABLED=0 ./build -p -l "-aslr"
[ "${PKGARCH}" == "arm" ] && GOOS=windows GOARCH=arm CGO_ENABLED=0 ./build -p -l "-aslr"
#[ "${PKGARCH}" == "arm64" ] && GOOS=windows GOARCH=arm64 CGO_ENABLED=0 ./build
# Create the postinstall script
cat > updateconfig.bat << EOF
@ -31,38 +39,32 @@ if not exist %ALLUSERSPROFILE%\\Yggdrasil (
)
if not exist %ALLUSERSPROFILE%\\Yggdrasil\\yggdrasil.conf (
if exist yggdrasil.exe (
yggdrasil.exe -genconf > %ALLUSERSPROFILE%\\Yggdrasil\\yggdrasil.conf
if not exist %ALLUSERSPROFILE%\\Yggdrasil\\yggdrasil.conf (
yggdrasil.exe -genconf > %ALLUSERSPROFILE%\\Yggdrasil\\yggdrasil.conf
)
)
)
EOF
# Work out metadata for the package info
PKGNAME=$(sh contrib/semver/name.sh)
PKGVERSION=$(sh contrib/msi/msversion.sh --bare)
PKGVERSION=$(sh contrib/semver/version.sh --bare)
PKGVERSIONMS=$(echo $PKGVERSION | tr - .)
([ "${PKGARCH}" == "x64" ] || [ "${PKGARCH}" == "arm64" ]) && \
[ "${PKGARCH}" == "x64" ] && \
PKGGUID="77757838-1a23-40a5-a720-c3b43e0260cc" PKGINSTFOLDER="ProgramFiles64Folder" || \
PKGGUID="54a3294e-a441-4322-aefb-3bb40dd022bb" PKGINSTFOLDER="ProgramFilesFolder"
# Download the Wintun driver
if [ ! -d wintun ];
then
curl -o wintun.zip https://www.wintun.net/builds/wintun-0.14.1.zip
if [ `sha256sum wintun.zip | cut -f 1 -d " "` != "07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51" ];
then
echo "wintun package didn't match expected checksum"
exit 1
fi
unzip wintun.zip
fi
curl -o wintun.zip https://www.wintun.net/builds/wintun-0.10.2.zip
unzip wintun.zip
if [ $PKGARCH = "x64" ]; then
PKGWINTUNDLL=wintun/bin/amd64/wintun.dll
elif [ $PKGARCH = "x86" ]; then
PKGWINTUNDLL=wintun/bin/x86/wintun.dll
elif [ $PKGARCH = "arm" ]; then
PKGWINTUNDLL=wintun/bin/arm/wintun.dll
elif [ $PKGARCH = "arm64" ]; then
PKGWINTUNDLL=wintun/bin/arm64/wintun.dll
#elif [ $PKGARCH = "arm64" ]; then
# PKGWINTUNDLL=wintun/bin/arm64/wintun.dll
else
echo "wasn't sure which architecture to get wintun for"
exit 1
@ -85,6 +87,7 @@ cat > wix.xml << EOF
Language="1033"
Codepage="1252"
Version="${PKGVERSIONMS}"
Platform="${PKGARCH}"
Manufacturer="github.com/yggdrasil-network">
<Package
@ -93,10 +96,11 @@ cat > wix.xml << EOF
Description="Yggdrasil Network Installer"
Comments="Yggdrasil Network standalone router for Windows."
Manufacturer="github.com/yggdrasil-network"
InstallerVersion="500"
InstallerVersion="200"
InstallScope="perMachine"
Languages="1033"
Compressed="yes"
Platform="${PKGARCH}"
SummaryCodepage="1252" />
<MajorUpgrade
@ -185,9 +189,7 @@ cat > wix.xml << EOF
<InstallExecuteSequence>
<Custom
Action="UpdateGenerateConfig"
Before="StartServices">
NOT Installed AND NOT REMOVE
</Custom>
Before="StartServices" />
</InstallExecuteSequence>
</Product>
@ -195,7 +197,4 @@ cat > wix.xml << EOF
EOF
# Generate the MSI
CANDLEFLAGS="-nologo"
LIGHTFLAGS="-nologo -spdb -sice:ICE71 -sice:ICE61"
candle $CANDLEFLAGS -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj -arch ${PKGARCH} wix.xml && \
light $LIGHTFLAGS -ext WixUtilExtension.dll -out ${PKGNAME}-${PKGVERSION}-${PKGARCH}.msi ${PKGNAME}-${PKGVERSION}-${PKGARCH}.wixobj
wixl -v wix.xml -a ${PKGARCH} -o ${PKGNAME}-${PKGVERSION}-${PKGARCH}.msi

View file

@ -1,46 +0,0 @@
#!/bin/sh
# Get the last tag
TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*\.[0-9]*" 2>/dev/null)
# Did getting the tag succeed?
if [ $? != 0 ] || [ -z "$TAG" ]; then
printf -- "unknown"
exit 0
fi
# Get the current branch
BRANCH=$(git symbolic-ref -q HEAD --short 2>/dev/null)
# Did getting the branch succeed?
if [ $? != 0 ] || [ -z "$BRANCH" ]; then
BRANCH="master"
fi
# Split out into major, minor and patch numbers
MAJOR=$(echo $TAG | cut -c 2- | cut -d "." -f 1)
MINOR=$(echo $TAG | cut -c 2- | cut -d "." -f 2)
PATCH=$(echo $TAG | cut -c 2- | cut -d "." -f 3 | awk -F"rc" '{print $1}')
# Output in the desired format
if [ $((PATCH)) -eq 0 ]; then
printf '%s%d.%d' "$PREPEND" "$((MAJOR))" "$((MINOR))"
else
printf '%s%d.%d.%d' "$PREPEND" "$((MAJOR))" "$((MINOR))" "$((PATCH))"
fi
# Add the build tag on non-master branches
if [ "$BRANCH" != "master" ]; then
BUILD=$(git rev-list --count $TAG..HEAD 2>/dev/null)
# Did getting the count of commits since the tag succeed?
if [ $? != 0 ] || [ -z "$BUILD" ]; then
printf -- "-unknown"
exit 0
fi
# Is the build greater than zero?
if [ $((BUILD)) -gt 0 ]; then
printf -- "-%04d" "$((BUILD))"
fi
fi

View file

@ -6,6 +6,7 @@ CONFFILE="/etc/yggdrasil.conf"
pidfile="/run/${RC_SVCNAME}.pid"
command="/usr/bin/yggdrasil"
extra_started_commands="reload"
depend() {
use net dns logger
@ -41,6 +42,12 @@ start() {
eend $?
}
reload() {
ebegin "Reloading ${RC_SVCNAME}"
start-stop-daemon --signal HUP --pidfile "${pidfile}"
eend $?
}
stop() {
ebegin "Stopping ${RC_SVCNAME}"
start-stop-daemon --stop --pidfile "${pidfile}" --exec "${command}"

View file

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

View file

@ -1,11 +1,46 @@
#!/bin/sh
case "$*" in
*--bare*)
# Remove the "v" prefix
git describe --tags --match="v[0-9]*\.[0-9]*\.[0-9]*" | cut -c 2-
;;
*)
git describe --tags --match="v[0-9]*\.[0-9]*\.[0-9]*"
;;
esac
# Get the last tag
TAG=$(git describe --abbrev=0 --tags --match="v[0-9]*\.[0-9]*\.[0-9]*" 2>/dev/null)
# Did getting the tag succeed?
if [ $? != 0 ] || [ -z "$TAG" ]; then
printf -- "unknown"
exit 0
fi
# Get the current branch
BRANCH=$(git symbolic-ref -q HEAD --short 2>/dev/null)
# Did getting the branch succeed?
if [ $? != 0 ] || [ -z "$BRANCH" ]; then
BRANCH="master"
fi
# Split out into major, minor and patch numbers
MAJOR=$(echo $TAG | cut -c 2- | cut -d "." -f 1)
MINOR=$(echo $TAG | cut -c 2- | cut -d "." -f 2)
PATCH=$(echo $TAG | cut -c 2- | cut -d "." -f 3)
# Output in the desired format
if [ $((PATCH)) -eq 0 ]; then
printf '%s%d.%d' "$PREPEND" "$((MAJOR))" "$((MINOR))"
else
printf '%s%d.%d.%d' "$PREPEND" "$((MAJOR))" "$((MINOR))" "$((PATCH))"
fi
# Add the build tag on non-master branches
if [ "$BRANCH" != "master" ]; then
BUILD=$(git rev-list --count $TAG..HEAD 2>/dev/null)
# Did getting the count of commits since the tag succeed?
if [ $? != 0 ] || [ -z "$BUILD" ]; then
printf -- "-unknown"
exit 0
fi
# Is the build greater than zero?
if [ $((BUILD)) -gt 0 ]; then
printf -- "-%04d" "$((BUILD))"
fi
fi

View file

@ -1,13 +0,0 @@
[Unit]
Description=Yggdrasil default config generator
ConditionPathExists=|!/etc/yggdrasil/yggdrasil.conf
ConditionFileNotEmpty=|!/etc/yggdrasil/yggdrasil.conf
Wants=local-fs.target
After=local-fs.target
[Service]
Type=oneshot
Group=yggdrasil
ExecStartPre=/usr/bin/mkdir -p /etc/yggdrasil
ExecStart=/usr/bin/yggdrasil -genconf > /etc/yggdrasil/yggdrasil.conf
ExecStartPost=/usr/bin/chmod -R 0640 /etc/yggdrasil

View file

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

View file

@ -1,25 +0,0 @@
[Unit]
Description=Yggdrasil Network
Wants=network-online.target
Wants=yggdrasil-default-config.service
After=network-online.target
After=yggdrasil-default-config.service
[Service]
Group=yggdrasil
ProtectHome=true
ProtectSystem=strict
NoNewPrivileges=true
RuntimeDirectory=yggdrasil
ReadWritePaths=/var/run/yggdrasil/ /run/yggdrasil/
SyslogIdentifier=yggdrasil
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
ExecStartPre=+-/sbin/modprobe tun
ExecStart=/usr/bin/yggdrasil -useconffile /etc/yggdrasil/yggdrasil.conf
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
TimeoutStopSec=5
[Install]
WantedBy=multi-user.target

148
doc/Whitepaper.md Normal file
View file

@ -0,0 +1,148 @@
# Yggdrasil
Note: This is a very rough early draft.
Yggdrasil is an encrypted IPv6 network running in the [`200::/7` address range](https://en.wikipedia.org/wiki/Unique_local_address).
It is an experimental/toy network, so failure is acceptable, as long as it's instructive to see how it breaks if/when everything falls apart.
IP addresses are derived from cryptographic keys, to reduce the need for public key infrastructure.
A form of locator/identifier separation (similar in goal to [LISP](https://en.wikipedia.org/wiki/Locator/Identifier_Separation_Protocol)) is used to map static identifiers (IP addresses) onto dynamic routing information (locators), using a [distributed hash table](https://en.wikipedia.org/wiki/Distributed_hash_table) (DHT).
Locators are used to approximate the distance between nodes in the network, where the approximate distance is the length of a real worst-case-scenario path through the network.
This is (arguably) easier to secure and requires less information about the network than commonly used routing schemes.
While not technically a [compact routing scheme](https://arxiv.org/abs/0708.2309), tests on real-world networks suggest that routing in this style incurs stretch comparable to the name-dependent compact routing schemes designed for static networks.
Compared to compact routing schemes, Yggdrasil appears to have smaller average routing table sizes, works on dynamic networks, and is name-independent.
It currently lacks the provable bounds of compact routing schemes, and there's a serious argument to be made that it cheats by stretching the definition of some of the above terms, but the main point to be emphasized is that there are trade-offs between different concerns when trying to route traffic, and we'd rather make every part *good* than try to make any one part *perfect*.
In that sense, Yggdrasil seems to be competitive, on what are supposedly realistic networks, with compact routing schemes.
## Addressing
Yggdrasil uses a truncated version of a `NodeID` to assign addresses.
An address is assigned from the `200::/7` prefix, according to the following:
1. Begin with `0x02` as the first byte of the address, or `0x03` if it's a `/64` prefix.
2. Count the number of leading `1` bits in the NodeID.
3. Set the second byte of the address to the number of leading `1` bits in the NodeID (8 bit unsigned integer, at most 255).
4. Append the NodeID to the remaining bits of the address, truncating the leading `1` bits and the first `0` bit, to a total address size of 128 bits.
The last bit of the first byte is used to flag if an address is for a router (`200::/8`), or part of an advertised prefix (`300::/8`), where each router owns a `/64` that matches their address (except with the eight bit set to 1 instead of 0).
This allows the prefix to be advertised to the router's LAN, so unsupported devices can still connect to the network (e.g. network printers).
The NodeID is a [sha512sum](https://en.wikipedia.org/wiki/SHA-512) of a node's public encryption key.
Addresses are checked that they match NodeID, to prevent address spoofing.
As such, while a 128 bit IPv6 address is likely too short to be considered secure by cryptographer standards, there is a significant cost in attempting to cause an address collision.
Addresses can be made more secure by brute force generating a large number of leading `1` bits in the NodeID.
When connecting to a node, the IP address is unpacked into the known bits of the NodeID and a matching bitmask to track which bits are significant.
A node is only communicated with if its `NodeID` matches its public key and the known `NodeID` bits from the address.
It is important to note that only `NodeID` is used internally for routing, so the addressing scheme could in theory be changed without breaking compatibility with intermediate routers.
This has been done once, when moving the address range from the `fd00::/8` ULA range to the reserved-but-[deprecated](https://tools.ietf.org/html/rfc4048) `200::/7` range.
Further addressing scheme changes could occur if, for example, an IPv7 format ever emerges.
### Cryptography
Public key encryption is done using the `golang.org/x/crypto/nacl/box`, which uses [Curve25519](https://en.wikipedia.org/wiki/Curve25519), [XSalsa20](https://en.wikipedia.org/wiki/Salsa20), and [Poly1305](https://en.wikipedia.org/wiki/Poly1305) for key exchange, encryption, and authentication (interoperable with [NaCl](https://en.wikipedia.org/wiki/NaCl_(software))).
Permanent keys are used only for protocol traffic, with random nonces generated on a per-packet basis using `crypto/rand` from Go's standard library.
Ephemeral session keys (for [forward secrecy](https://en.wikipedia.org/wiki/Forward_secrecy)) are generated for encapsulated IPv6 traffic, using the same set of primitives, with random initial nonces that are subsequently incremented.
A list of recently received session nonces is kept (as a bitmask) and checked to reject duplicated packets, in an effort to block duplicate packets and replay attacks.
A separate set of keys are generated and used for signing with [Ed25519](https://en.wikipedia.org/wiki/Ed25519), which is used by the routing layer to secure construction of a spanning tree.
### Prefixes
Recall that each node's address is in the lower half of the address range, I.e. `200::/8`. A `/64` prefix is made available to each node under `300::/8`, where the remaining bits of the prefix match the node's address under `200::/8`.
A node may optionally advertise a prefix on their local area network, which allows unsupported or legacy devices with IPv6 support to connect to the network.
Note that there are 64 fewer bits of `NodeID` available to check in each address from a routing prefix, so it makes sense to brute force a `NodeID` with more significant bits in the address if this approach is to be used.
Running `genkeys.go` will do this by default.
## Locators and Routing
Locators are generated using information from a spanning tree (described below).
The result is that each node has a set of [coordinates in a greedy metric space](https://en.wikipedia.org/wiki/Greedy_embedding).
These coordinates are used as a distance label.
Given the coordinates of any two nodes, it is possible to calculate the length of some real path through the network between the two nodes.
Traffic is forwarded using a [greedy routing](https://en.wikipedia.org/wiki/Small-world_routing#Greedy_routing) scheme, where each node forwards the packet to a one-hop neighbor that is closer to the destination (according to this distance metric) than the current node.
In particular, when a packet needs to be forwarded, a node will forward it to whatever peer is closest to the destination in the greedy [metric space](https://en.wikipedia.org/wiki/Metric_space) used by the network, provided that the peer is closer to the destination than the current node.
If no closer peers are idle, then the packet is queued in FIFO order, with separate queues per destination coords (currently, as a bit of a hack, IPv6 flow labels are embedded after the end of the significant part of the coords, so queues distinguish between different traffic streams with the same destination).
Whenever the node finishes forwarding a packet to a peer, it checks the queues, and will forward the first packet from the queue with the maximum `<age of first packet>/<queue size in bytes>`, i.e. the bandwidth the queue is attempting to use, subject to the constraint that the peer is a valid next hop (i.e. closer to the destination than the current node).
If no non-empty queue is available, then the peer is added to the idle set, forward packets when the need arises.
This acts as a crude approximation of backpressure routing, where the remote queue sizes are assumed to be equal to the distance of a node from a destination (rather than communicating queue size information), and packets are never forwarded "backwards" through the network, but congestion on a local link is routed around when possible.
The queue selection strategy behaves similar to shortest-queue-first, in that a larger fraction of available bandwidth to sessions that attempt to use less bandwidth, and is loosely based on the rationale behind some proposed solutions to the [cake-cutting](https://en.wikipedia.org/wiki/Fair_cake-cutting) problem.
The queue size is limited to 4 MB. If a packet is added to a queue and the total size of all queues is larger than this threshold, then a random queue is selected (with odds proportional to relative queue sizes), and the first packet from that queue is dropped, with the process repeated until the total queue size drops below the allowed threshold.
Note that this forwarding procedure generalizes to nodes that are not one-hop neighbors, but the current implementation omits the use of more distant neighbors, as this is expected to be a minor optimization (it would add per-link control traffic to pass path-vector-like information about a subset of the network, which is a lot of overhead compared to the current setup).
### Spanning Tree
A [spanning tree](https://en.wikipedia.org/wiki/Spanning_tree) is constructed with the tree rooted at the highest TreeID, where TreeID is equal to a sha512sum of a node's public [Ed25519](https://en.wikipedia.org/wiki/Ed25519) key (used for signing).
A node sends periodic advertisement messages to each neighbor.
The advertisement contains the coords that match the path from the root through the node, plus one additional hop from the node to the neighbor being advertised to.
Each hop in this advertisement includes a matching ed25519 signature.
These signatures prevent nodes from forging arbitrary routing advertisements.
The first hop, from the root, also includes a sequence number, which must be updated periodically.
A node will blacklist the current root (keeping a record of the last sequence number observed) if the root fails to update for longer than some timeout (currently hard coded at 1 minute).
Normally, a root node will update their sequence number for frequently than this (once every 30 seconds).
Nodes are throttled to ignore updates with a new sequence number for some period after updating their most recently seen sequence number (currently this cooldown is 15 seconds).
The implementation chooses to set the sequence number equal to the unix time on the root's clock, so that a new (higher) sequence number will be selected if the root is restarted and the clock is not set back.
Other than the root node, every other node in the network must select one of its neighbors to use as their parent.
This selection is done by tracking when each neighbor first sends us a message with a new timestamp from the root, to determine the ordering of the latency of each path from the root, to each neighbor, and then to the node that's searching for a parent.
These relative latencies are tracked by, for each neighbor, keeping a score vs each other neighbor.
If a neighbor sends a message with an updated timestamp before another neighbor, then the faster neighbor's score is increased by 1.
If the neighbor sends a message slower, then the score is decreased by 2, to make sure that a node must be reliably faster (at least 2/3 of the time) to see a net score increase over time.
If a node begins to advertise new coordinates, then its score vs all other nodes is reset to 0.
A node switches to a new parent if a neighbor's score (vs the current parent) reaches some threshold, currently 240, which corresponds to about 2 hours of being a reliably faster path.
The intended outcome of this process is that stable connections from fixed infrastructure near the "core" of the network should (eventually) select parents that minimize latency from the root to themselves, while the more dynamic parts of the network, presumably more towards the edges, will try to favor reliability when selecting a parent.
The distance metric between nodes is simply the distance between the nodes if they routed on the spanning tree.
This is equal to the sum of the distance from each node to the last common ancestor of the two nodes being compared.
The locator then consists of a root's key, timestamp, and coordinates representing each hop in the path from the root to the node.
In practice, only the coords are used for routing, while the root and timestamp, along with all the per-hop signatures, are needed to securely construct the spanning tree.
## Name-independent routing
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.
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:
1. The address is unpacked into the known bits of a NodeID and a bitmask to signal which bits of the NodeID are known (the unknown bits are ignored).
2. A DHT search is performed, which normally results in a response from the node closest in the DHT keyspace to the target `NodeID`. The response contains the node's curve25519 public key, which is checked to match the `NodeID` (and therefore the address), as well as the node's coordinates.
3. Using the keys and coords from the above step, an ephemeral key exchange occurs between the source and destination nodes. These ephemeral session keys are used to encrypt any ordinary IPv6 traffic that may be encapsulated and sent between the nodes.
From that point, the session keys and coords are cached and used to encrypt and send traffic between nodes. This is *mostly* transparent to the user: the initial DHT lookup and key exchange takes at least 2 round trips, so there's some delay before session setup completes and normal IPv6 traffic can flow. This is similar to the delay caused by a DNS lookup, although it generally takes longer, as a DHT lookup requires multiple iterations to reach the destination.
## Project Status and Plans
The current (Go) implementation is considered alpha, so compatibility with future versions is neither guaranteed nor expected.
While users are discouraged from running anything truly critical on top of it, as of writing, it seems reliable enough for day-to-day use.
As an "alpha" quality release, Yggdrasil *should* at least be able to detect incompatible versions when it sees them, and warn the users that an update may be needed.
A "beta" quality release should know enough to be compatible in the face of wire format changes, and reasonably feature complete.
A "stable" 1.0 release, if it ever happens, would probably be feature complete, with no expectation of future wire format changes, and free of known critical bugs.
Roughly speaking, there are a few obvious ways the project could turn out:
1. The developers could lose interest before it goes anywhere.
2. The project could be reasonably complete (beta or stable), but never gain a significant number of users.
3. The network may grow large enough that fundamental (non-fixable) design problems appear, which is hopefully a learning experience, but the project may die as a result.
4. The network may grow large, but never hit any design problems, in which case we need to think about either moving the important parts into other projects ([cjdns](https://github.com/cjdelisle/cjdns)) or rewriting compatible implementations that are better optimized for the target platforms (e.g. a linux kernel module).
That last one is probably impossible, because the speed of light would *eventually* become a problem, for a sufficiently large network.
If the only thing limiting network growth turns out to be the underlying physics, then that arguably counts as a win.
Also, note that some design decisions were made for ease-of-programming or ease-of-testing reasons, and likely need to be reconsidered at some point.
In particular, Yggdrasil currently uses TCP for connections with one-hop neighbors, which introduces an additional layer of buffering that can lead to increased and/or unstable latency in congested areas of the network.

@ -0,0 +1 @@
Subproject commit c876890a51d9140e68d5cec7fbeb2146c2562792

59
go.mod
View file

@ -1,49 +1,24 @@
module github.com/yggdrasil-network/yggdrasil-go
go 1.22
go 1.16
require (
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d
github.com/cheggaaa/pb/v3 v3.1.5
github.com/coder/websocket v1.8.12
github.com/gologme/log v1.3.0
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979
github.com/cheggaaa/pb/v3 v3.0.6
github.com/fatih/color v1.10.0 // indirect
github.com/gologme/log v1.2.0
github.com/hashicorp/go-syslog v1.0.0
github.com/hjson/hjson-go/v4 v4.4.0
github.com/kardianos/minwinsvc v1.0.2
github.com/quic-go/quic-go v0.48.2
github.com/vishvananda/netlink v1.3.0
github.com/wlynxg/anet v0.0.5
golang.org/x/crypto v0.33.0
golang.org/x/net v0.35.0
golang.org/x/sys v0.30.0
golang.org/x/text v0.22.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
golang.zx2c4.com/wireguard/windows v0.5.3
)
require (
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.7.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
github.com/hjson/hjson-go v3.1.0+incompatible
github.com/kardianos/minwinsvc v1.0.0
github.com/mattn/go-runewidth v0.0.10 // indirect
github.com/mitchellh/mapstructure v1.4.1
github.com/rivo/uniseg v0.2.0 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/tools v0.23.0 // indirect
)
require (
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/olekukonko/tablewriter v0.0.5
github.com/vishvananda/netns v0.0.5 // indirect
suah.dev/protect v1.2.4
github.com/vishvananda/netlink v1.1.0
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f // indirect
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b
golang.org/x/text v0.3.6-0.20210220033129-8f690f22cf1c
golang.zx2c4.com/wireguard v0.0.0-20210306175010-7e3b8371a1bf
golang.zx2c4.com/wireguard/windows v0.3.8
)

183
go.sum
View file

@ -1,119 +1,72 @@
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3 h1:d8N0z+udAnbU5PdjpLSNPTWlqeU/nnYsQ42B6+879aw=
github.com/Arceliar/ironwood v0.0.0-20241213013129-743fe2fccbd3/go.mod h1:SrrElc3FFMpYCODSr11jWbLFeOM8WsY+DbDY/l2AXF0=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d h1:UK9fsWbWqwIQkMCz1CP+v5pGbsGoWAw6g4AyvMpm1EM=
github.com/Arceliar/phony v0.0.0-20220903101357-530938a4b13d/go.mod h1:BCnxhRf47C/dy/e/D2pmB8NkB3dQVIrkD98b220rx5Q=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bloom/v3 v3.7.0 h1:VfknkqV4xI+PsaDIsoHueyxVDZrfvMn56jeWUzvzdls=
github.com/bits-and-blooms/bloom/v3 v3.7.0/go.mod h1:VKlUSvp0lFIYqxJjzdnSsZEw4iHb1kOL2tfHTgyJBHg=
github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk=
github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gologme/log v1.3.0 h1:l781G4dE+pbigClDSDzSaaYKtiueHCILUa/qSDsmHAo=
github.com/gologme/log v1.3.0/go.mod h1:yKT+DvIPdDdDoPtqFrFxheooyVmoqi0BAsw+erN3wA4=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 h1:WndgpSW13S32VLQ3ugUxx2EnnWmgba1kCqPkd4Gk1yQ=
github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979/go.mod h1:6Lkn+/zJilRMsKmbmG1RPoamiArC6HS73xbwRyp3UyI=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/cheggaaa/pb/v3 v3.0.6 h1:ULPm1wpzvj60FvmCrX7bIaB80UgbhI+zSaQJKRfCbAs=
github.com/cheggaaa/pb/v3 v3.0.6/go.mod h1:X1L61/+36nz9bjIsrDU52qHKOQukUQe2Ge+YvGuquCw=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/gologme/log v1.2.0 h1:Ya5Ip/KD6FX7uH0S31QO87nCCSucKtF44TLbTtO7V4c=
github.com/gologme/log v1.2.0/go.mod h1:gq31gQ8wEHkR+WekdWsqDuf8pXTUZA9BnnzTuPz1Y9U=
github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hjson/hjson-go/v4 v4.4.0 h1:D/NPvqOCH6/eisTb5/ztuIS8GUvmpHaLOcNk1Bjr298=
github.com/hjson/hjson-go/v4 v4.4.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/kardianos/minwinsvc v1.0.2 h1:JmZKFJQrmTGa/WiW+vkJXKmfzdjabuEW4Tirj5lLdR0=
github.com/kardianos/minwinsvc v1.0.2/go.mod h1:LUZNYhNmxujx2tR7FbdxqYJ9XDDoCd3MQcl1o//FWl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/hjson/hjson-go v3.1.0+incompatible h1:DY/9yE8ey8Zv22bY+mHV1uk2yRy0h8tKhZ77hEdi0Aw=
github.com/hjson/hjson-go v3.1.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio=
github.com/kardianos/minwinsvc v1.0.0 h1:+JfAi8IBJna0jY2dJGZqi7o15z13JelFIklJCAENALA=
github.com/kardianos/minwinsvc v1.0.0/go.mod h1:Bgd0oc+D0Qo3bBytmNtyRKVlp85dAloLKhfxanPFFRc=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
suah.dev/protect v1.2.4 h1:iVZG/zQB63FKNpITDYM/cXoAeCTIjCiXHuFVByJFDzg=
suah.dev/protect v1.2.4/go.mod h1:vVrquYO3u1Ep9Ez2z8x+6N6/czm+TBmWKZfiXU2tb54=
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f h1:p4VB7kIXpOQvVn1ZaTIVp+3vuYAXFe3OJEvjbUYJLaA=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225014209-683adc9d29d7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305215415-5cdee2b1b5a0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b h1:ggRgirZABFolTmi3sn6Ivd9SipZwLedQ5wR0aAKnFxU=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6-0.20210220033129-8f690f22cf1c h1:SW/oilbeWd6f32u3ZvuYGqZ+wivcp//I3Dy/gByk7Wk=
golang.org/x/text v0.3.6-0.20210220033129-8f690f22cf1c/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.zx2c4.com/wireguard v0.0.0-20210225140808-70b7b7158fc9/go.mod h1:39ZQQ95hUxDxT7opsWy/rtfgvXXc8s30qfZ02df69Fo=
golang.zx2c4.com/wireguard v0.0.0-20210306175010-7e3b8371a1bf h1:AtdIMfzvVNPXN4kVY/yWS8mvpQogSwtCRJk2y/LBPpg=
golang.zx2c4.com/wireguard v0.0.0-20210306175010-7e3b8371a1bf/go.mod h1:ojGPy+9W6ZSM8anL+xC67fvh8zPQJwA6KpFOHyDWLX4=
golang.zx2c4.com/wireguard/windows v0.3.8 h1:FvfBEhdZZTwthLuPHdyP6zpivYL3enopxd4XpggAufM=
golang.zx2c4.com/wireguard/windows v0.3.8/go.mod h1:lm7dxHcBuzMNq706Ge1tZKZKw4+19vG9dLOhoDX05HQ=

1593
misc/sim/fc00-2017-08-12.txt Normal file

File diff suppressed because it is too large Load diff

62
misc/sim/merge.py Normal file
View file

@ -0,0 +1,62 @@
import glob
import sys
inputDirPath = sys.argv[1]
inputFilePaths = glob.glob(inputDirPath+"/*")
inputFilePaths.sort()
merged = dict()
stretches = []
total = 0
for inputFilePath in inputFilePaths:
print "Processing file {}".format(inputFilePath)
with open(inputFilePath, 'r') as f:
inData = f.readlines()
pathsChecked = 0.
avgStretch = 0.
for line in inData:
dat = line.rstrip('\n').split(' ')
eHops = int(dat[0])
nHops = int(dat[1])
count = int(dat[2])
if eHops not in merged: merged[eHops] = dict()
if nHops not in merged[eHops]: merged[eHops][nHops] = 0
merged[eHops][nHops] += count
total += count
pathsChecked += count
stretch = float(nHops)/eHops
avgStretch += stretch*count
finStretch = avgStretch / max(1, pathsChecked)
stretches.append(str(finStretch))
hopsUsed = 0.
hopsNeeded = 0.
avgStretch = 0.
results = []
for eHops in sorted(merged.keys()):
for nHops in sorted(merged[eHops].keys()):
count = merged[eHops][nHops]
result = "{} {} {}".format(eHops, nHops, count)
results.append(result)
hopsUsed += nHops*count
hopsNeeded += eHops*count
stretch = float(nHops)/eHops
avgStretch += stretch*count
print result
bandwidthUsage = hopsUsed/max(1, hopsNeeded)
avgStretch /= max(1, total)
with open("results.txt", "w") as f:
f.write('\n'.join(results))
with open("stretches.txt", "w") as f:
f.write('\n'.join(stretches))
print "Total files processed: {}".format(len(inputFilePaths))
print "Total paths found: {}".format(total)
print "Bandwidth usage: {}".format(bandwidthUsage)
print "Average stretch: {}".format(avgStretch)

2
misc/sim/run-sim Executable file
View file

@ -0,0 +1,2 @@
#!/bin/bash
go run -tags debug misc/sim/treesim.go "$@"

901
misc/sim/treesim-forward.py Normal file
View file

@ -0,0 +1,901 @@
# Tree routing scheme (named Yggdrasil, after the world tree from Norse mythology)
# Steps:
# 1: Pick any node, here I'm using highest nodeID
# 2: Build spanning tree, each node stores path back to root
# Optionally with weights for each hop
# Ties broken by preferring a parent with higher degree
# 3: Distance metric: self->peer + (via tree) peer->dest
# 4: Perform (modified) greedy lookup via this metric for each direction (A->B and B->A)
# 5: Source-route traffic using the better of those two paths
# Note: This makes no attempt to simulate a dynamic network
# E.g. A node's peers cannot be disconnected
# TODO:
# Make better use of drop?
# In particular, we should be ignoring *all* recently dropped *paths* to the root
# To minimize route flapping
# Not really an issue in the sim, but probably needed for a real network
import array
import gc
import glob
import gzip
import heapq
import os
import random
import time
#############
# Constants #
#############
# Reminder of where link cost comes in
LINK_COST = 1
# Timeout before dropping something, in simulated seconds
TIMEOUT = 60
###########
# Classes #
###########
class PathInfo:
def __init__(self, nodeID):
self.nodeID = nodeID # e.g. IP
self.coords = [] # Position in tree
self.tstamp = 0 # Timestamp from sender, to keep track of old vs new info
self.degree = 0 # Number of peers the sender has, used to break ties
# The above should be signed
self.path = [nodeID] # Path to node (in path-vector route)
self.time = 0 # Time info was updated, to keep track of e.g. timeouts
self.treeID = nodeID # Hack, let tree use different ID than IP, used so we can dijkstra once and test many roots
def clone(self):
# Return a deep-enough copy of the path
clone = PathInfo(None)
clone.nodeID = self.nodeID
clone.coords = self.coords[:]
clone.tstamp = self.tstamp
clone.degree = self.degree
clone.path = self.path[:]
clone.time = self.time
clone.treeID = self.treeID
return clone
# End class PathInfo
class Node:
def __init__(self, nodeID):
self.info = PathInfo(nodeID) # Self NodeInfo
self.root = None # PathInfo to node at root of tree
self.drop = dict() # PathInfo to nodes from clus that have timed out
self.peers = dict() # PathInfo to peers
self.links = dict() # Links to peers (to pass messages)
self.msgs = [] # Said messages
self.table = dict() # Pre-computed lookup table of peer info
def tick(self):
# Do periodic maintenance stuff, including push updates
self.info.time += 1
if self.info.time > self.info.tstamp + TIMEOUT/4:
# Update timestamp at least once every 1/4 timeout period
# This should probably be randomized in a real implementation
self.info.tstamp = self.info.time
self.info.degree = 0# TODO decide if degree should be used, len(self.peers)
changed = False # Used to track when the network has converged
changed |= self.cleanRoot()
self.cleanDropped()
# Should probably send messages infrequently if there's nothing new to report
if self.info.tstamp == self.info.time:
msg = self.createMessage()
self.sendMessage(msg)
return changed
def cleanRoot(self):
changed = False
if self.root and self.info.time - self.root.time > TIMEOUT:
print "DEBUG: clean root,", self.root.path
self.drop[self.root.treeID] = self.root
self.root = None
changed = True
if not self.root or self.root.treeID < self.info.treeID:
# No need to drop someone who'se worse than us
self.info.coords = [self.info.nodeID]
self.root = self.info.clone()
changed = True
elif self.root.treeID == self.info.treeID:
self.root = self.info.clone()
return changed
def cleanDropped(self):
# May actually be a treeID... better to iterate over keys explicitly
nodeIDs = sorted(self.drop.keys())
for nodeID in nodeIDs:
node = self.drop[nodeID]
if self.info.time - node.time > 4*TIMEOUT:
del self.drop[nodeID]
return None
def createMessage(self):
# Message is just a tuple
# First element is the sender
# Second element is the root
# We will .clone() everything during the send operation
msg = (self.info, self.root)
return msg
def sendMessage(self, msg):
for link in self.links.values():
newMsg = (msg[0].clone(), msg[1].clone())
link.msgs.append(newMsg)
return None
def handleMessages(self):
changed = False
while self.msgs:
changed |= self.handleMessage(self.msgs.pop())
return changed
def handleMessage(self, msg):
changed = False
for node in msg:
# Update the path and timestamp for the sender and root info
node.path.append(self.info.nodeID)
node.time = self.info.time
# Update the sender's info in our list of peers
sender = msg[0]
self.peers[sender.nodeID] = sender
# Decide if we want to update the root
root = msg[1]
updateRoot = False
isSameParent = False
isBetterParent = False
if len(self.root.path) > 1 and len(root.path) > 1:
parent = self.peers[self.root.path[-2]]
if parent.nodeID == sender.nodeID: isSameParent = True
if sender.degree > parent.degree:
# This would also be where you check path uptime/reliability/whatever
# All else being equal, we prefer parents with high degree
# We are trusting peers to report degree correctly in this case
# So expect some performance reduction if your peers aren't trustworthy
# (Lies can increase average stretch by a few %)
isBetterParent = True
if self.info.nodeID in root.path[:-1]: pass # No loopy routes allowed
elif root.treeID in self.drop and self.drop[root.treeID].tstamp >= root.tstamp: pass
elif not self.root: updateRoot = True
elif self.root.treeID < root.treeID: updateRoot = True
elif self.root.treeID != root.treeID: pass
elif self.root.tstamp > root.tstamp: pass
elif len(root.path) < len(self.root.path): updateRoot = True
elif isBetterParent and len(root.path) == len(self.root.path): updateRoot = True
elif isSameParent and self.root.tstamp < root.tstamp: updateRoot = True
if updateRoot:
if not self.root or self.root.path != root.path: changed = True
self.root = root
self.info.coords = self.root.path
return changed
def lookup(self, dest):
# Note: Can loop in an unconverged network
# The person looking up the route is responsible for checking for loops
best = None
bestDist = 0
for node in self.peers.itervalues():
# dist = distance to node + dist (on tree) from node to dest
dist = len(node.path)-1 + treeDist(node.coords, dest.coords)
if not best or dist < bestDist:
best = node
bestDist = dist
if best:
next = best.path[-2]
assert next in self.peers
return next
else:
# We failed to look something up
# TODO some way to signal this which doesn't crash
assert False
def initTable(self):
# Pre-computes a lookup table for destination coords
# Insert parent first so you prefer them as a next-hop
self.table.clear()
parent = self.info.nodeID
if len(self.info.coords) >= 2: parent = self.info.coords[-2]
for peer in self.peers.itervalues():
current = self.table
for coord in peer.coords:
if coord not in current: current[coord] = (peer.nodeID, dict())
old = current[coord]
next = old[1]
oldPeer = self.peers[old[0]]
oldDist = len(oldPeer.coords)
oldDeg = oldPeer.degree
newDist = len(peer.coords)
newDeg = peer.degree
# Prefer parent
# Else prefer short distance from root
# If equal distance, prefer high degree
if peer.nodeID == parent: current[coord] = (peer.nodeID, next)
elif newDist < oldDist: current[coord] = (peer.nodeID, next)
elif newDist == oldDist and newDeg > oldDeg: current[coord] = (peer.nodeID, next)
current = next
return None
def lookup_new(self, dest):
# Use pre-computed lookup table to look up next hop for dest coords
assert self.table
if len(self.info.coords) >= 2: parent = self.info.coords[-2]
else: parent = None
current = (parent, self.table)
c = None
for coord in dest.coords:
c = coord
if coord not in current[1]: break
current = current[1][coord]
next = current[0]
if c in self.peers: next = c
if next not in self.peers:
assert next == None
# You're the root of a different connected component
# You'd drop the packet in this case
# To make the path cache not die, need to return a valid next hop...
# Returning self for that reason
next = self.info.nodeID
return next
# End class Node
####################
# Helper Functions #
####################
def getIndexOfLCA(source, dest):
# Return index of last common ancestor in source/dest coords
# -1 if no common ancestor (e.g. different roots)
lcaIdx = -1
minLen = min(len(source), len(dest))
for idx in xrange(minLen):
if source[idx] == dest[idx]: lcaIdx = idx
else: break
return lcaIdx
def treePath(source, dest):
# Return path with source at head and dest at tail
lastMatch = getIndexOfLCA(source, dest)
path = dest[-1:lastMatch:-1] + source[lastMatch:]
assert path[0] == dest[-1]
assert path[-1] == source[-1]
return path
def treeDist(source, dest):
dist = len(source) + len(dest)
lcaIdx = getIndexOfLCA(source, dest)
dist -= 2*(lcaIdx+1)
return dist
def dijkstra(nodestore, startingNodeID):
# Idea to use heapq and basic implementation taken from stackexchange post
# http://codereview.stackexchange.com/questions/79025/dijkstras-algorithm-in-python
nodeIDs = sorted(nodestore.keys())
nNodes = len(nodeIDs)
idxs = dict()
for nodeIdx in xrange(nNodes):
nodeID = nodeIDs[nodeIdx]
idxs[nodeID] = nodeIdx
dists = array.array("H", [0]*nNodes)
queue = [(0, startingNodeID)]
while queue:
dist, nodeID = heapq.heappop(queue)
idx = idxs[nodeID]
if not dists[idx]: # Unvisited, otherwise we skip it
dists[idx] = dist
for peer in nodestore[nodeID].links:
if not dists[idxs[peer]]:
# Peer is also unvisited, so add to queue
heapq.heappush(queue, (dist+LINK_COST, peer))
return dists
def dijkstrall(nodestore):
# Idea to use heapq and basic implementation taken from stackexchange post
# http://codereview.stackexchange.com/questions/79025/dijkstras-algorithm-in-python
nodeIDs = sorted(nodestore.keys())
nNodes = len(nodeIDs)
idxs = dict()
for nodeIdx in xrange(nNodes):
nodeID = nodeIDs[nodeIdx]
idxs[nodeID] = nodeIdx
dists = array.array("H", [0]*nNodes*nNodes) # use GetCacheIndex(nNodes, start, end)
for sourceIdx in xrange(nNodes):
print "Finding shortest paths for node {} / {} ({})".format(sourceIdx+1, nNodes, nodeIDs[sourceIdx])
queue = [(0, sourceIdx)]
while queue:
dist, nodeIdx = heapq.heappop(queue)
distIdx = getCacheIndex(nNodes, sourceIdx, nodeIdx)
if not dists[distIdx]: # Unvisited, otherwise we skip it
dists[distIdx] = dist
for peer in nodestore[nodeIDs[nodeIdx]].links:
pIdx = idxs[peer]
pdIdx = getCacheIndex(nNodes, sourceIdx, pIdx)
if not dists[pdIdx]:
# Peer is also unvisited, so add to queue
heapq.heappush(queue, (dist+LINK_COST, pIdx))
return dists
def linkNodes(node1, node2):
node1.links[node2.info.nodeID] = node2
node2.links[node1.info.nodeID] = node1
############################
# Store topology functions #
############################
def makeStoreSquareGrid(sideLength, randomize=True):
# Simple grid in a sideLength*sideLength square
# Just used to validate that the code runs
store = dict()
nodeIDs = list(range(sideLength*sideLength))
if randomize: random.shuffle(nodeIDs)
for nodeID in nodeIDs:
store[nodeID] = Node(nodeID)
for index in xrange(len(nodeIDs)):
if (index % sideLength != 0): linkNodes(store[nodeIDs[index]], store[nodeIDs[index-1]])
if (index >= sideLength): linkNodes(store[nodeIDs[index]], store[nodeIDs[index-sideLength]])
print "Grid store created, size {}".format(len(store))
return store
def makeStoreASRelGraph(pathToGraph):
#Existing network graphs, in caida.org's asrel format (ASx|ASy|z per line, z denotes relationship type)
with open(pathToGraph, "r") as f:
inData = f.readlines()
store = dict()
for line in inData:
if line.strip()[0] == "#": continue # Skip comment lines
line = line.replace('|'," ")
nodes = map(int, line.split()[0:2])
if nodes[0] not in store: store[nodes[0]] = Node(nodes[0])
if nodes[1] not in store: store[nodes[1]] = Node(nodes[1])
linkNodes(store[nodes[0]], store[nodes[1]])
print "CAIDA AS-relation graph successfully imported, size {}".format(len(store))
return store
def makeStoreASRelGraphMaxDeg(pathToGraph, degIdx=0):
with open(pathToGraph, "r") as f:
inData = f.readlines()
store = dict()
nodeDeg = dict()
for line in inData:
if line.strip()[0] == "#": continue # Skip comment lines
line = line.replace('|'," ")
nodes = map(int, line.split()[0:2])
if nodes[0] not in nodeDeg: nodeDeg[nodes[0]] = 0
if nodes[1] not in nodeDeg: nodeDeg[nodes[1]] = 0
nodeDeg[nodes[0]] += 1
nodeDeg[nodes[1]] += 1
sortedNodes = sorted(nodeDeg.keys(), \
key=lambda x: (nodeDeg[x], x), \
reverse=True)
maxDegNodeID = sortedNodes[degIdx]
return makeStoreASRelGraphFixedRoot(pathToGraph, maxDegNodeID)
def makeStoreASRelGraphFixedRoot(pathToGraph, rootNodeID):
with open(pathToGraph, "r") as f:
inData = f.readlines()
store = dict()
for line in inData:
if line.strip()[0] == "#": continue # Skip comment lines
line = line.replace('|'," ")
nodes = map(int, line.split()[0:2])
if nodes[0] not in store:
store[nodes[0]] = Node(nodes[0])
if nodes[0] == rootNodeID: store[nodes[0]].info.treeID += 1000000000
if nodes[1] not in store:
store[nodes[1]] = Node(nodes[1])
if nodes[1] == rootNodeID: store[nodes[1]].info.treeID += 1000000000
linkNodes(store[nodes[0]], store[nodes[1]])
print "CAIDA AS-relation graph successfully imported, size {}".format(len(store))
return store
def makeStoreDimesEdges(pathToGraph, rootNodeID=None):
# Read from a DIMES csv-formatted graph from a gzip file
store = dict()
with gzip.open(pathToGraph, "r") as f:
inData = f.readlines()
size = len(inData)
index = 0
for edge in inData:
if not index % 1000:
pct = 100.0*index/size
print "Processing edge {}, {:.2f}%".format(index, pct)
index += 1
dat = edge.rstrip().split(',')
node1 = "N" + str(dat[0].strip())
node2 = "N" + str(dat[1].strip())
if '?' in node1 or '?' in node2: continue #Unknown node
if node1 == rootNodeID: node1 = "R" + str(dat[0].strip())
if node2 == rootNodeID: node2 = "R" + str(dat[1].strip())
if node1 not in store: store[node1] = Node(node1)
if node2 not in store: store[node2] = Node(node2)
if node1 != node2: linkNodes(store[node1], store[node2])
print "DIMES graph successfully imported, size {}".format(len(store))
return store
def makeStoreGeneratedGraph(pathToGraph, root=None):
with open(pathToGraph, "r") as f:
inData = f.readlines()
store = dict()
for line in inData:
if line.strip()[0] == "#": continue # Skip comment lines
nodes = map(int, line.strip().split(' ')[0:2])
node1 = nodes[0]
node2 = nodes[1]
if node1 == root: node1 += 1000000
if node2 == root: node2 += 1000000
if node1 not in store: store[node1] = Node(node1)
if node2 not in store: store[node2] = Node(node2)
linkNodes(store[node1], store[node2])
print "Generated graph successfully imported, size {}".format(len(store))
return store
############################################
# Functions used as parts of network tests #
############################################
def idleUntilConverged(store):
nodeIDs = sorted(store.keys())
timeOfLastChange = 0
step = 0
# Idle until the network has converged
while step - timeOfLastChange < 4*TIMEOUT:
step += 1
print "Step: {}, last change: {}".format(step, timeOfLastChange)
changed = False
for nodeID in nodeIDs:
# Update node status, send messages
changed |= store[nodeID].tick()
for nodeID in nodeIDs:
# Process messages
changed |= store[nodeID].handleMessages()
if changed: timeOfLastChange = step
initTables(store)
return store
def getCacheIndex(nodes, sourceIndex, destIndex):
return sourceIndex*nodes + destIndex
def initTables(store):
nodeIDs = sorted(store.keys())
nNodes = len(nodeIDs)
print "Initializing routing tables for {} nodes".format(nNodes)
for idx in xrange(nNodes):
nodeID = nodeIDs[idx]
store[nodeID].initTable()
print "Routing tables initialized"
return None
def getCache(store):
nodeIDs = sorted(store.keys())
nNodes = len(nodeIDs)
nodeIdxs = dict()
for nodeIdx in xrange(nNodes):
nodeIdxs[nodeIDs[nodeIdx]] = nodeIdx
cache = array.array("H", [0]*nNodes*nNodes)
for sourceIdx in xrange(nNodes):
sourceID = nodeIDs[sourceIdx]
print "Building fast lookup table for node {} / {} ({})".format(sourceIdx+1, nNodes, sourceID)
for destIdx in xrange(nNodes):
destID = nodeIDs[destIdx]
if sourceID == destID: nextHop = destID # lookup would fail
else: nextHop = store[sourceID].lookup(store[destID].info)
nextHopIdx = nodeIdxs[nextHop]
cache[getCacheIndex(nNodes, sourceIdx, destIdx)] = nextHopIdx
return cache
def testPaths(store, dists):
cache = getCache(store)
nodeIDs = sorted(store.keys())
nNodes = len(nodeIDs)
idxs = dict()
for nodeIdx in xrange(nNodes):
nodeID = nodeIDs[nodeIdx]
idxs[nodeID] = nodeIdx
results = dict()
for sourceIdx in xrange(nNodes):
sourceID = nodeIDs[sourceIdx]
print "Testing paths from node {} / {} ({})".format(sourceIdx+1, len(nodeIDs), sourceID)
#dists = dijkstra(store, sourceID)
for destIdx in xrange(nNodes):
destID = nodeIDs[destIdx]
if destID == sourceID: continue # Skip self
distIdx = getCacheIndex(nNodes, sourceIdx, destIdx)
eHops = dists[distIdx]
if not eHops: continue # The network is split, no path exists
hops = 0
for pair in ((sourceIdx, destIdx),):
nHops = 0
locIdx = pair[0]
dIdx = pair[1]
while locIdx != dIdx:
locIdx = cache[getCacheIndex(nNodes, locIdx, dIdx)]
nHops += 1
if not hops or nHops < hops: hops = nHops
if eHops not in results: results[eHops] = dict()
if hops not in results[eHops]: results[eHops][hops] = 0
results[eHops][hops] += 1
return results
def getAvgStretch(pathMatrix):
avgStretch = 0.
checked = 0.
for eHops in sorted(pathMatrix.keys()):
for nHops in sorted(pathMatrix[eHops].keys()):
count = pathMatrix[eHops][nHops]
stretch = float(nHops)/float(max(1, eHops))
avgStretch += stretch*count
checked += count
avgStretch /= max(1, checked)
return avgStretch
def getMaxStretch(pathMatrix):
maxStretch = 0.
for eHops in sorted(pathMatrix.keys()):
for nHops in sorted(pathMatrix[eHops].keys()):
stretch = float(nHops)/float(max(1, eHops))
maxStretch = max(maxStretch, stretch)
return maxStretch
def getCertSizes(store):
# Returns nCerts frequency distribution
# De-duplicates common certs (for shared prefixes in the path)
sizes = dict()
for node in store.values():
certs = set()
for peer in node.peers.values():
pCerts = set()
assert len(peer.path) == 2
assert peer.coords[-1] == peer.path[0]
hops = peer.coords + peer.path[1:]
for hopIdx in xrange(len(hops)-1):
send = hops[hopIdx]
if send == node.info.nodeID: continue # We created it, already have it
path = hops[0:hopIdx+2]
# Each cert is signed by the sender
# Includes information about the path from the sender to the next hop
# Next hop is at hopIdx+1, so the path to next hop is hops[0:hopIdx+2]
cert = "{}:{}".format(send, path)
certs.add(cert)
size = len(certs)
if size not in sizes: sizes[size] = 0
sizes[size] += 1
return sizes
def getMinLinkCertSizes(store):
# Returns nCerts frequency distribution
# De-duplicates common certs (for shared prefixes in the path)
# Based on the minimum number of certs that must be traded through a particular link
# Handled per link
sizes = dict()
for node in store.values():
peerCerts = dict()
for peer in node.peers.values():
pCerts = set()
assert len(peer.path) == 2
assert peer.coords[-1] == peer.path[0]
hops = peer.coords + peer.path[1:]
for hopIdx in xrange(len(hops)-1):
send = hops[hopIdx]
if send == node.info.nodeID: continue # We created it, already have it
path = hops[0:hopIdx+2]
# Each cert is signed by the sender
# Includes information about the path from the sender to the next hop
# Next hop is at hopIdx+1, so the path to next hop is hops[0:hopIdx+2]
cert = "{}:{}".format(send, path)
pCerts.add(cert)
peerCerts[peer.nodeID] = pCerts
for peer in peerCerts:
size = 0
pCerts = peerCerts[peer]
for cert in pCerts:
required = True
for p2 in peerCerts:
if p2 == peer: continue
p2Certs = peerCerts[p2]
if cert in p2Certs: required = False
if required: size += 1
if size not in sizes: sizes[size] = 0
sizes[size] += 1
return sizes
def getPathSizes(store):
# Returns frequency distribution of the total number of hops the routing table
# I.e. a node with 3 peers, each with 5 hop coord+path, would count as 3x5=15
sizes = dict()
for node in store.values():
size = 0
for peer in node.peers.values():
assert len(peer.path) == 2
assert peer.coords[-1] == peer.path[0]
peerSize = len(peer.coords) + len(peer.path) - 1 # double-counts peer, -1
size += peerSize
if size not in sizes: sizes[size] = 0
sizes[size] += 1
return sizes
def getPeerSizes(store):
# Returns frequency distribution of the number of peers each node has
sizes = dict()
for node in store.values():
nPeers = len(node.peers)
if nPeers not in sizes: sizes[nPeers] = 0
sizes[nPeers] += 1
return sizes
def getAvgSize(sizes):
sumSizes = 0
nNodes = 0
for size in sizes:
count = sizes[size]
sumSizes += size*count
nNodes += count
avgSize = float(sumSizes)/max(1, nNodes)
return avgSize
def getMaxSize(sizes):
return max(sizes.keys())
def getMinSize(sizes):
return min(sizes.keys())
def getResults(pathMatrix):
results = []
for eHops in sorted(pathMatrix.keys()):
for nHops in sorted(pathMatrix[eHops].keys()):
count = pathMatrix[eHops][nHops]
results.append("{} {} {}".format(eHops, nHops, count))
return '\n'.join(results)
####################################
# Functions to run different tests #
####################################
def runTest(store):
# Runs the usual set of tests on the store
# Does not save results, so only meant for quick tests
# To e.g. check the code works, maybe warm up the pypy jit
for node in store.values():
node.info.time = random.randint(0, TIMEOUT)
node.info.tstamp = TIMEOUT
print "Begin testing network"
dists = None
if not dists: dists = dijkstrall(store)
idleUntilConverged(store)
pathMatrix = testPaths(store, dists)
avgStretch = getAvgStretch(pathMatrix)
maxStretch = getMaxStretch(pathMatrix)
peers = getPeerSizes(store)
certs = getCertSizes(store)
paths = getPathSizes(store)
linkCerts = getMinLinkCertSizes(store)
avgPeerSize = getAvgSize(peers)
maxPeerSize = getMaxSize(peers)
avgCertSize = getAvgSize(certs)
maxCertSize = getMaxSize(certs)
avgPathSize = getAvgSize(paths)
maxPathSize = getMaxSize(paths)
avgLinkCert = getAvgSize(linkCerts)
maxLinkCert = getMaxSize(linkCerts)
totalCerts = sum(map(lambda x: x*certs[x], certs.keys()))
totalLinks = sum(map(lambda x: x*peers[x], peers.keys())) # one-way links
avgCertsPerLink = float(totalCerts)/max(1, totalLinks)
print "Finished testing network"
print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch)
print "Avg / Max nPeers size: {} / {}".format(avgPeerSize, maxPeerSize)
print "Avg / Max nCerts size: {} / {}".format(avgCertSize, maxCertSize)
print "Avg / Max total hops in any node's routing table: {} / {}".format(avgPathSize, maxPathSize)
print "Avg / Max lower bound cert requests per link (one-way): {} / {}".format(avgLinkCert, maxLinkCert)
print "Avg certs per link (one-way): {}".format(avgCertsPerLink)
return # End of function
def rootNodeASTest(path, outDir="output-treesim-AS", dists=None, proc = 1):
# Checks performance for every possible choice of root node
# Saves output for each root node to a separate file on disk
# path = input path to some caida.org formatted AS-relationship graph
if not os.path.exists(outDir): os.makedirs(outDir)
assert os.path.exists(outDir)
store = makeStoreASRelGraph(path)
nodes = sorted(store.keys())
for nodeIdx in xrange(len(nodes)):
if nodeIdx % proc != 0: continue # Work belongs to someone else
rootNodeID = nodes[nodeIdx]
outpath = outDir+"/{}".format(rootNodeID)
if os.path.exists(outpath):
print "Skipping {}, already processed".format(rootNodeID)
continue
store = makeStoreASRelGraphFixedRoot(path, rootNodeID)
for node in store.values():
node.info.time = random.randint(0, TIMEOUT)
node.info.tstamp = TIMEOUT
print "Beginning {}, size {}".format(nodeIdx, len(store))
if not dists: dists = dijkstrall(store)
idleUntilConverged(store)
pathMatrix = testPaths(store, dists)
avgStretch = getAvgStretch(pathMatrix)
maxStretch = getMaxStretch(pathMatrix)
results = getResults(pathMatrix)
with open(outpath, "w") as f:
f.write(results)
print "Finished test for root AS {} ({} / {})".format(rootNodeID, nodeIdx+1, len(store))
print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch)
#break # Stop after 1, because they can take forever
return # End of function
def timelineASTest():
# Meant to study the performance of the network as a function of network size
# Loops over a set of AS-relationship graphs
# Runs a test on each graph, selecting highest-degree node as the root
# Saves results for each graph to a separate file on disk
outDir = "output-treesim-timeline-AS"
if not os.path.exists(outDir): os.makedirs(outDir)
assert os.path.exists(outDir)
paths = sorted(glob.glob("asrel/datasets/*"))
for path in paths:
date = os.path.basename(path).split(".")[0]
outpath = outDir+"/{}".format(date)
if os.path.exists(outpath):
print "Skipping {}, already processed".format(date)
continue
store = makeStoreASRelGraphMaxDeg(path)
dists = None
for node in store.values():
node.info.time = random.randint(0, TIMEOUT)
node.info.tstamp = TIMEOUT
print "Beginning {}, size {}".format(date, len(store))
if not dists: dists = dijkstrall(store)
idleUntilConverged(store)
pathMatrix = testPaths(store, dists)
avgStretch = getAvgStretch(pathMatrix)
maxStretch = getMaxStretch(pathMatrix)
results = getResults(pathMatrix)
with open(outpath, "w") as f:
f.write(results)
print "Finished {} with {} nodes".format(date, len(store))
print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch)
#break # Stop after 1, because they can take forever
return # End of function
def timelineDimesTest():
# Meant to study the performance of the network as a function of network size
# Loops over a set of AS-relationship graphs
# Runs a test on each graph, selecting highest-degree node as the root
# Saves results for each graph to a separate file on disk
outDir = "output-treesim-timeline-dimes"
if not os.path.exists(outDir): os.makedirs(outDir)
assert os.path.exists(outDir)
# Input files are named ASEdgesX_Y where X = month (no leading 0), Y = year
paths = sorted(glob.glob("DIMES/ASEdges/*.gz"))
exists = set(glob.glob(outDir+"/*"))
for path in paths:
date = os.path.basename(path).split(".")[0]
outpath = outDir+"/{}".format(date)
if outpath in exists:
print "Skipping {}, already processed".format(date)
continue
store = makeStoreDimesEdges(path)
# Get the highest degree node and make it root
# Sorted by nodeID just to make it stable in the event of a tie
nodeIDs = sorted(store.keys())
bestRoot = ""
bestDeg = 0
for nodeID in nodeIDs:
node = store[nodeID]
if len(node.links) > bestDeg:
bestRoot = nodeID
bestDeg = len(node.links)
assert bestRoot
store = makeStoreDimesEdges(path, bestRoot)
rootID = "R" + bestRoot[1:]
assert rootID in store
# Don't forget to set random seed before setting times
# To make results reproducible
nodeIDs = sorted(store.keys())
random.seed(12345)
for nodeID in nodeIDs:
node = store[nodeID]
node.info.time = random.randint(0, TIMEOUT)
node.info.tstamp = TIMEOUT
print "Beginning {}, size {}".format(date, len(store))
if not dists: dists = dijkstrall(store)
idleUntilConverged(store)
pathMatrix = testPaths(store, dists)
avgStretch = getAvgStretch(pathMatrix)
maxStretch = getMaxStretch(pathMatrix)
results = getResults(pathMatrix)
with open(outpath, "w") as f:
f.write(results)
print "Finished {} with {} nodes".format(date, len(store))
print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch)
break # Stop after 1, because they can take forever
return # End of function
def scalingTest(maxTests=None, inputDir="graphs"):
# Meant to study the performance of the network as a function of network size
# Loops over a set of nodes in a previously generated graph
# Runs a test on each graph, testing each node as the root
# if maxTests is set, tests only that number of roots (highest degree first)
# Saves results for each graph to a separate file on disk
outDir = "output-treesim-{}".format(inputDir)
if not os.path.exists(outDir): os.makedirs(outDir)
assert os.path.exists(outDir)
paths = sorted(glob.glob("{}/*".format(inputDir)))
exists = set(glob.glob(outDir+"/*"))
for path in paths:
gc.collect() # pypy waits for gc to close files
graph = os.path.basename(path).split(".")[0]
store = makeStoreGeneratedGraph(path)
# Get the highest degree node and make it root
# Sorted by nodeID just to make it stable in the event of a tie
nodeIDs = sorted(store.keys(), key=lambda x: len(store[x].links), reverse=True)
dists = None
if maxTests: nodeIDs = nodeIDs[:maxTests]
for nodeID in nodeIDs:
nodeIDStr = str(nodeID).zfill(len(str(len(store)-1)))
outpath = outDir+"/{}-{}".format(graph, nodeIDStr)
if outpath in exists:
print "Skipping {}-{}, already processed".format(graph, nodeIDStr)
continue
store = makeStoreGeneratedGraph(path, nodeID)
# Don't forget to set random seed before setting times
random.seed(12345) # To make results reproducible
nIDs = sorted(store.keys())
for nID in nIDs:
node = store[nID]
node.info.time = random.randint(0, TIMEOUT)
node.info.tstamp = TIMEOUT
print "Beginning {}, size {}".format(graph, len(store))
if not dists: dists = dijkstrall(store)
idleUntilConverged(store)
pathMatrix = testPaths(store, dists)
avgStretch = getAvgStretch(pathMatrix)
maxStretch = getMaxStretch(pathMatrix)
results = getResults(pathMatrix)
with open(outpath, "w") as f:
f.write(results)
print "Finished {} with {} nodes for root {}".format(graph, len(store), nodeID)
print "Avg / Max stretch: {} / {}".format(avgStretch, maxStretch)
return # End of function
##################
# Main Execution #
##################
if __name__ == "__main__":
if True: # Run a quick test
random.seed(12345) # DEBUG
store = makeStoreSquareGrid(4)
runTest(store) # Quick test
store = None
# Do some real work
#runTest(makeStoreDimesEdges("DIMES/ASEdges/ASEdges1_2007.csv.gz"))
#timelineDimesTest()
#rootNodeASTest("asrel/datasets/19980101.as-rel.txt")
#timelineASTest()
#rootNodeASTest("hype-2016-09-19.list", "output-treesim-hype")
#scalingTest(None, "graphs-20") # First argument 1 to only test 1 root per graph
#store = makeStoreGeneratedGraph("bgp_tables")
#store = makeStoreGeneratedGraph("skitter")
#store = makeStoreASRelGraphMaxDeg("hype-2016-09-19.list") #http://hia.cjdns.ca/watchlist/c/walk.peers.20160919
#store = makeStoreGeneratedGraph("fc00-2017-08-12.txt")
if store: runTest(store)
#rootNodeASTest("skitter", "output-treesim-skitter", None, 0, 1)
#scalingTest(1, "graphs-20") # First argument 1 to only test 1 root per graph
#scalingTest(1, "graphs-21") # First argument 1 to only test 1 root per graph
#scalingTest(1, "graphs-22") # First argument 1 to only test 1 root per graph
#scalingTest(1, "graphs-23") # First argument 1 to only test 1 root per graph
if not store:
import sys
args = sys.argv
if len(args) == 2:
job_number = int(sys.argv[1])
rootNodeASTest("fc00-2017-08-12.txt", "fc00", None, job_number)
else:
print "Usage: {} job_number".format(args[0])
print "job_number = which job set to run on this node (1-indexed)"

459
misc/sim/treesim.go Normal file
View file

@ -0,0 +1,459 @@
// +build !lint
package main
import (
"bufio"
"flag"
"fmt"
"os"
"runtime"
"runtime/pprof"
"strconv"
"strings"
"time"
"github.com/gologme/log"
. "github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
. "github.com/yggdrasil-network/yggdrasil-go/src/crypto"
)
////////////////////////////////////////////////////////////////////////////////
type Node struct {
index int
core Core
send chan<- []byte
recv <-chan []byte
}
func (n *Node) init(index int) {
n.index = index
n.core.Init()
n.send = n.core.DEBUG_getSend()
n.recv = n.core.DEBUG_getRecv()
n.core.DEBUG_simFixMTU()
}
func (n *Node) printTraffic() {
for {
packet := <-n.recv
fmt.Println(n.index, packet)
//panic("Got a packet")
}
}
func (n *Node) startPeers() {
//for _, p := range n.core.Peers.Ports {
// go p.MainLoop()
//}
//go n.printTraffic()
//n.core.Peers.DEBUG_startPeers()
}
func linkNodes(m, n *Node) {
// Don't allow duplicates
if m.core.DEBUG_getPeers().DEBUG_hasPeer(n.core.DEBUG_getSigningPublicKey()) {
return
}
// Create peers
// Buffering reduces packet loss in the sim
// This slightly speeds up testing (fewer delays before retrying a ping)
pLinkPub, pLinkPriv := m.core.DEBUG_newBoxKeys()
qLinkPub, qLinkPriv := m.core.DEBUG_newBoxKeys()
p := m.core.DEBUG_getPeers().DEBUG_newPeer(n.core.DEBUG_getEncryptionPublicKey(),
n.core.DEBUG_getSigningPublicKey(), *m.core.DEBUG_getSharedKey(pLinkPriv, qLinkPub))
q := n.core.DEBUG_getPeers().DEBUG_newPeer(m.core.DEBUG_getEncryptionPublicKey(),
m.core.DEBUG_getSigningPublicKey(), *n.core.DEBUG_getSharedKey(qLinkPriv, pLinkPub))
DEBUG_simLinkPeers(p, q)
return
}
func makeStoreSquareGrid(sideLength int) map[int]*Node {
store := make(map[int]*Node)
nNodes := sideLength * sideLength
idxs := make([]int, 0, nNodes)
// TODO shuffle nodeIDs
for idx := 1; idx <= nNodes; idx++ {
idxs = append(idxs, idx)
}
for _, idx := range idxs {
node := &Node{}
node.init(idx)
store[idx] = node
}
for idx := 0; idx < nNodes; idx++ {
if (idx % sideLength) != 0 {
linkNodes(store[idxs[idx]], store[idxs[idx-1]])
}
if idx >= sideLength {
linkNodes(store[idxs[idx]], store[idxs[idx-sideLength]])
}
}
//for _, node := range store { node.initPorts() }
return store
}
func makeStoreStar(nNodes int) map[int]*Node {
store := make(map[int]*Node)
center := &Node{}
center.init(0)
store[0] = center
for idx := 1; idx < nNodes; idx++ {
node := &Node{}
node.init(idx)
store[idx] = node
linkNodes(center, node)
}
return store
}
func loadGraph(path string) map[int]*Node {
f, err := os.Open(path)
if err != nil {
panic(err)
}
defer f.Close()
store := make(map[int]*Node)
s := bufio.NewScanner(f)
for s.Scan() {
line := s.Text()
nodeIdxstrs := strings.Split(line, " ")
nodeIdx0, _ := strconv.Atoi(nodeIdxstrs[0])
nodeIdx1, _ := strconv.Atoi(nodeIdxstrs[1])
if store[nodeIdx0] == nil {
node := &Node{}
node.init(nodeIdx0)
store[nodeIdx0] = node
}
if store[nodeIdx1] == nil {
node := &Node{}
node.init(nodeIdx1)
store[nodeIdx1] = node
}
linkNodes(store[nodeIdx0], store[nodeIdx1])
}
//for _, node := range store { node.initPorts() }
return store
}
////////////////////////////////////////////////////////////////////////////////
func startNetwork(store map[[32]byte]*Node) {
for _, node := range store {
node.startPeers()
}
}
func getKeyedStore(store map[int]*Node) map[[32]byte]*Node {
newStore := make(map[[32]byte]*Node)
for _, node := range store {
newStore[node.core.DEBUG_getSigningPublicKey()] = node
}
return newStore
}
func testPaths(store map[[32]byte]*Node) bool {
nNodes := len(store)
count := 0
for _, source := range store {
count++
fmt.Printf("Testing paths from node %d / %d (%d)\n", count, nNodes, source.index)
for _, dest := range store {
//if source == dest { continue }
destLoc := dest.core.DEBUG_getLocator()
coords := destLoc.DEBUG_getCoords()
temp := 0
ttl := ^uint64(0)
oldTTL := ttl
for here := source; here != dest; {
temp++
if temp > 4096 {
fmt.Println("Loop?")
time.Sleep(time.Second)
return false
}
nextPort := here.core.DEBUG_switchLookup(coords)
// First check if "here" is accepting packets from the previous node
// TODO explain how this works
ports := here.core.DEBUG_getPeers().DEBUG_getPorts()
nextPeer := ports[nextPort]
if nextPeer == nil {
fmt.Println("Peer associated with next port is nil")
return false
}
next := store[nextPeer.DEBUG_getSigKey()]
/*
if next == here {
//for idx, link := range here.links {
// fmt.Println("DUMP:", idx, link.nodeID)
//}
if nextPort != 0 { panic("This should not be") }
fmt.Println("Failed to route:", source.index, here.index, dest.index, oldTTL, ttl)
//here.table.DEBUG_dumpTable()
//fmt.Println("Ports:", here.nodeID, here.ports)
return false
panic(fmt.Sprintln("Routing Loop:",
source.index,
here.index,
dest.index))
}
*/
if temp > 4090 {
fmt.Println("DEBUG:",
source.index, source.core.DEBUG_getLocator(),
here.index, here.core.DEBUG_getLocator(),
dest.index, dest.core.DEBUG_getLocator())
//here.core.DEBUG_getSwitchTable().DEBUG_dumpTable()
}
if here != source {
// This is sufficient to check for routing loops or blackholes
//break
}
if here == next {
fmt.Println("Drop:", source.index, here.index, dest.index, oldTTL)
return false
}
here = next
}
}
}
return true
}
func stressTest(store map[[32]byte]*Node) {
fmt.Println("Stress testing network...")
nNodes := len(store)
dests := make([][]byte, 0, nNodes)
for _, dest := range store {
loc := dest.core.DEBUG_getLocator()
coords := loc.DEBUG_getCoords()
dests = append(dests, coords)
}
lookups := 0
start := time.Now()
for _, source := range store {
for _, coords := range dests {
source.core.DEBUG_switchLookup(coords)
lookups++
}
}
timed := time.Since(start)
fmt.Printf("%d lookups in %s (%f lookups per second)\n",
lookups,
timed,
float64(lookups)/timed.Seconds())
}
func pingNodes(store map[[32]byte]*Node) {
fmt.Println("Sending pings...")
nNodes := len(store)
count := 0
equiv := func(a []byte, b []byte) bool {
if len(a) != len(b) {
return false
}
for idx := 0; idx < len(a); idx++ {
if a[idx] != b[idx] {
return false
}
}
return true
}
for _, source := range store {
count++
//if count > 16 { break }
fmt.Printf("Sending packets from node %d/%d (%d)\n", count, nNodes, source.index)
sourceKey := source.core.DEBUG_getEncryptionPublicKey()
payload := sourceKey[:]
sourceAddr := source.core.DEBUG_getAddr()[:]
sendTo := func(bs []byte, destAddr []byte) {
packet := make([]byte, 40+len(bs))
copy(packet[8:24], sourceAddr)
copy(packet[24:40], destAddr)
copy(packet[40:], bs)
packet[0] = 6 << 4
source.send <- packet
}
destCount := 0
for _, dest := range store {
destCount += 1
fmt.Printf("%d Nodes, %d Send, %d Recv\n", nNodes, count, destCount)
if dest == source {
fmt.Println("Skipping self")
continue
}
destAddr := dest.core.DEBUG_getAddr()[:]
ticker := time.NewTicker(150 * time.Millisecond)
sendTo(payload, destAddr)
for loop := true; loop; {
select {
case packet := <-dest.recv:
{
if equiv(payload, packet[len(packet)-len(payload):]) {
loop = false
}
}
case <-ticker.C:
sendTo(payload, destAddr)
//dumpDHTSize(store) // note that this uses racey functions to read things...
}
}
ticker.Stop()
}
//break // Only try sending pings from 1 node
// This is because, for some reason, stopTun() doesn't always close it
// And if two tuns are up, bad things happen (sends via wrong interface)
}
fmt.Println("Finished pinging nodes")
}
func pingBench(store map[[32]byte]*Node) {
fmt.Println("Benchmarking pings...")
nPings := 0
payload := make([]byte, 1280+40) // MTU + ipv6 header
var timed time.Duration
//nNodes := len(store)
count := 0
for _, source := range store {
count++
//fmt.Printf("Sending packets from node %d/%d (%d)\n", count, nNodes, source.index)
getPing := func(key [32]byte, decodedCoords []byte) []byte {
// TODO write some function to do this the right way, put... somewhere...
coords := DEBUG_wire_encode_coords(decodedCoords)
packet := make([]byte, 0, len(key)+len(coords)+len(payload))
packet = append(packet, key[:]...)
packet = append(packet, coords...)
packet = append(packet, payload[:]...)
return packet
}
for _, dest := range store {
key := dest.core.DEBUG_getEncryptionPublicKey()
loc := dest.core.DEBUG_getLocator()
coords := loc.DEBUG_getCoords()
ping := getPing(key, coords)
// TODO make sure the session is open first
start := time.Now()
for i := 0; i < 1000000; i++ {
source.send <- ping
nPings++
}
timed += time.Since(start)
break
}
break
}
fmt.Printf("Sent %d pings in %s (%f per second)\n",
nPings,
timed,
float64(nPings)/timed.Seconds())
}
func dumpStore(store map[NodeID]*Node) {
for _, node := range store {
fmt.Println("DUMPSTORE:", node.index, node.core.DEBUG_getLocator())
node.core.DEBUG_getSwitchTable().DEBUG_dumpTable()
}
}
func dumpDHTSize(store map[[32]byte]*Node) {
var min, max, sum int
for _, node := range store {
num := node.core.DEBUG_getDHTSize()
min = num
max = num
break
}
for _, node := range store {
num := node.core.DEBUG_getDHTSize()
if num < min {
min = num
}
if num > max {
max = num
}
sum += num
}
avg := float64(sum) / float64(len(store))
fmt.Printf("DHT min %d / avg %f / max %d\n", min, avg, max)
}
func (n *Node) startTCP(listen string) {
n.core.DEBUG_setupAndStartGlobalTCPInterface(listen)
}
func (n *Node) connectTCP(remoteAddr string) {
n.core.AddPeer(remoteAddr, remoteAddr)
}
////////////////////////////////////////////////////////////////////////////////
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to this file")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
panic(fmt.Sprintf("could not create CPU profile: ", err))
}
if err := pprof.StartCPUProfile(f); err != nil {
panic(fmt.Sprintf("could not start CPU profile: ", err))
}
defer pprof.StopCPUProfile()
}
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
panic(fmt.Sprintf("could not create memory profile: ", err))
}
defer func() { pprof.WriteHeapProfile(f); f.Close() }()
}
fmt.Println("Test")
Util_testAddrIDMask()
idxstore := makeStoreSquareGrid(4)
//idxstore := makeStoreStar(256)
//idxstore := loadGraph("misc/sim/hype-2016-09-19.list")
//idxstore := loadGraph("misc/sim/fc00-2017-08-12.txt")
//idxstore := loadGraph("skitter")
kstore := getKeyedStore(idxstore)
//*
logger := log.New(os.Stderr, "", log.Flags())
for _, n := range kstore {
n.core.DEBUG_setLogger(logger)
}
//*/
startNetwork(kstore)
//time.Sleep(10*time.Second)
// Note that testPaths only works if pressure is turned off
// Otherwise congestion can lead to routing loops?
for finished := false; !finished; {
finished = testPaths(kstore)
}
pingNodes(kstore)
//pingBench(kstore) // Only after disabling debug output
//stressTest(kstore)
//time.Sleep(120 * time.Second)
dumpDHTSize(kstore) // note that this uses racey functions to read things...
if false {
// This connects the sim to the local network
for _, node := range kstore {
node.startTCP("localhost:0")
node.connectTCP("localhost:12345")
break // just 1
}
for _, node := range kstore {
go func() {
// Just dump any packets sent to this node
for range node.recv {
}
}()
}
var block chan struct{}
<-block
}
runtime.GC()
}

View file

@ -3,7 +3,9 @@
package address
import (
"crypto/ed25519"
"fmt"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
)
// Address represents an IPv6 address in the yggdrasil address range.
@ -43,34 +45,25 @@ func (s *Subnet) IsValid() bool {
return (*s)[l-1] == prefix[l-1]|0x01
}
// AddrForKey takes an ed25519.PublicKey as an argument and returns an *Address.
// This function returns nil if the key length is not ed25519.PublicKeySize.
// AddrForNodeID takes a *NodeID as an argument and returns an *Address.
// This address begins with the contents of GetPrefix(), with the last bit set to 0 to indicate an address.
// The following 8 bits are set to the number of leading 1 bits in the bitwise inverse of the public key.
// The bitwise inverse of the key, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the address.
func AddrForKey(publicKey ed25519.PublicKey) *Address {
// The following 8 bits are set to the number of leading 1 bits in the NodeID.
// The NodeID, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the address.
func AddrForNodeID(nid *crypto.NodeID) *Address {
// 128 bit address
// Begins with prefix
// Next bit is a 0
// Next 7 bits, interpreted as a uint, are # of leading 1s in the NodeID
// Leading 1s and first leading 0 of the NodeID are truncated off
// The rest is appended to the IPv6 address (truncated to 128 bits total)
if len(publicKey) != ed25519.PublicKeySize {
return nil
}
var buf [ed25519.PublicKeySize]byte
copy(buf[:], publicKey)
for idx := range buf {
buf[idx] = ^buf[idx]
}
var addr Address
var temp = make([]byte, 0, 32)
var temp []byte
done := false
ones := byte(0)
bits := byte(0)
nBits := 0
for idx := 0; idx < 8*len(buf); idx++ {
bit := (buf[idx/8] & (0x80 >> byte(idx%8))) >> byte(7-(idx%8))
for idx := 0; idx < 8*len(nid); idx++ {
bit := (nid[idx/8] & (0x80 >> byte(idx%8))) >> byte(7-(idx%8))
if !done && bit != 0 {
ones++
continue
@ -93,58 +86,91 @@ func AddrForKey(publicKey ed25519.PublicKey) *Address {
return &addr
}
// SubnetForKey takes an ed25519.PublicKey as an argument and returns a *Subnet.
// This function returns nil if the key length is not ed25519.PublicKeySize.
// The subnet begins with the address prefix, with the last bit set to 1 to indicate a prefix.
// The following 8 bits are set to the number of leading 1 bits in the bitwise inverse of the key.
// The bitwise inverse of the key, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the subnet.
func SubnetForKey(publicKey ed25519.PublicKey) *Subnet {
// SubnetForNodeID takes a *NodeID as an argument and returns an *Address.
// This subnet begins with the address prefix, with the last bit set to 1 to indicate a prefix.
// The following 8 bits are set to the number of leading 1 bits in the NodeID.
// The NodeID, excluding the leading 1 bits and the first leading 0 bit, is truncated to the appropriate length and makes up the remainder of the subnet.
func SubnetForNodeID(nid *crypto.NodeID) *Subnet {
// Exactly as the address version, with two exceptions:
// 1) The first bit after the fixed prefix is a 1 instead of a 0
// 2) It's truncated to a subnet prefix length instead of 128 bits
addr := AddrForKey(publicKey)
if addr == nil {
return nil
}
addr := *AddrForNodeID(nid)
var snet Subnet
copy(snet[:], addr[:])
prefix := GetPrefix() // nolint:staticcheck
prefix := GetPrefix()
snet[len(prefix)-1] |= 0x01
return &snet
}
// GetKey returns the partial ed25519.PublicKey for the Address.
// This is used for key lookup.
func (a *Address) GetKey() ed25519.PublicKey {
var key [ed25519.PublicKeySize]byte
prefix := GetPrefix() // nolint:staticcheck
// GetNodeIDandMask returns two *NodeID.
// The first is a NodeID with all the bits known from the Address set to their correct values.
// The second is a bitmask with 1 bit set for each bit that was known from the Address.
// This is used to look up NodeIDs in the DHT and tell if they match an Address.
func (a *Address) GetNodeIDandMask() (*crypto.NodeID, *crypto.NodeID) {
// Mask is a bitmask to mark the bits visible from the address
// This means truncated leading 1s, first leading 0, and visible part of addr
var nid crypto.NodeID
var mask crypto.NodeID
prefix := GetPrefix()
ones := int(a[len(prefix)])
for idx := 0; idx < ones; idx++ {
key[idx/8] |= 0x80 >> byte(idx%8)
nid[idx/8] |= 0x80 >> byte(idx%8)
}
keyOffset := ones + 1
nidOffset := ones + 1
addrOffset := 8*len(prefix) + 8
for idx := addrOffset; idx < 8*len(a); idx++ {
bits := a[idx/8] & (0x80 >> byte(idx%8))
bits <<= byte(idx % 8)
keyIdx := keyOffset + (idx - addrOffset)
bits >>= byte(keyIdx % 8)
idx := keyIdx / 8
if idx >= len(key) {
break
}
key[idx] |= bits
nidIdx := nidOffset + (idx - addrOffset)
bits >>= byte(nidIdx % 8)
nid[nidIdx/8] |= bits
}
for idx := range key {
key[idx] = ^key[idx]
maxMask := 8*(len(a)-len(prefix)-1) + ones + 1
for idx := 0; idx < maxMask; idx++ {
mask[idx/8] |= 0x80 >> byte(idx%8)
}
return ed25519.PublicKey(key[:])
return &nid, &mask
}
// GetKey returns the partial ed25519.PublicKey for the Subnet.
// This is used for key lookup.
func (s *Subnet) GetKey() ed25519.PublicKey {
var addr Address
copy(addr[:], s[:])
return addr.GetKey()
// GetNodeIDLengthString returns a string representation of the known bits of the NodeID, along with the number of known bits, for use with yggdrasil.Dialer's Dial and DialContext functions.
func (a *Address) GetNodeIDLengthString() string {
nid, mask := a.GetNodeIDandMask()
l := mask.PrefixLength()
return fmt.Sprintf("%s/%d", nid.String(), l)
}
// GetNodeIDandMask returns two *NodeID.
// The first is a NodeID with all the bits known from the Subnet set to their correct values.
// The second is a bitmask with 1 bit set for each bit that was known from the Subnet.
// This is used to look up NodeIDs in the DHT and tell if they match a Subnet.
func (s *Subnet) GetNodeIDandMask() (*crypto.NodeID, *crypto.NodeID) {
// As with the address version, but visible parts of the subnet prefix instead
var nid crypto.NodeID
var mask crypto.NodeID
prefix := GetPrefix()
ones := int(s[len(prefix)])
for idx := 0; idx < ones; idx++ {
nid[idx/8] |= 0x80 >> byte(idx%8)
}
nidOffset := ones + 1
addrOffset := 8*len(prefix) + 8
for idx := addrOffset; idx < 8*len(s); idx++ {
bits := s[idx/8] & (0x80 >> byte(idx%8))
bits <<= byte(idx % 8)
nidIdx := nidOffset + (idx - addrOffset)
bits >>= byte(nidIdx % 8)
nid[nidIdx/8] |= bits
}
maxMask := 8*(len(s)-len(prefix)-1) + ones + 1
for idx := 0; idx < maxMask; idx++ {
mask[idx/8] |= 0x80 >> byte(idx%8)
}
return &nid, &mask
}
// GetNodeIDLengthString returns a string representation of the known bits of the NodeID, along with the number of known bits, for use with yggdrasil.Dialer's Dial and DialContext functions.
func (s *Subnet) GetNodeIDLengthString() string {
nid, mask := s.GetNodeIDandMask()
l := mask.PrefixLength()
return fmt.Sprintf("%s/%d", nid.String(), l)
}

View file

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

View file

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

View file

@ -1,69 +1,52 @@
package admin
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
"github.com/yggdrasil-network/yggdrasil-go/src/util"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
)
// TODO: Add authentication
type AdminSocket struct {
core *core.Core
log core.Logger
listener net.Listener
handlers map[string]handler
done chan struct{}
config struct {
listenaddr ListenAddress
}
core *yggdrasil.Core
log *log.Logger
listenaddr string
listener net.Listener
handlers map[string]handler
started bool
}
type AdminSocketRequest struct {
Name string `json:"request"`
Arguments json.RawMessage `json:"arguments,omitempty"`
KeepAlive bool `json:"keepalive,omitempty"`
}
type AdminSocketResponse struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
Request AdminSocketRequest `json:"request"`
Response json.RawMessage `json:"response"`
}
// Info refers to information that is returned to the admin socket handler.
type Info map[string]interface{}
type handler struct {
desc string // What does the endpoint do?
args []string // List of human-readable argument names
handler core.AddHandlerFunc // First is input map, second is output
}
type ListResponse struct {
List []ListEntry `json:"list"`
}
type ListEntry struct {
Command string `json:"command"`
Description string `json:"description"`
Fields []string `json:"fields,omitempty"`
args []string // List of human-readable argument names
handler func(Info) (Info, error) // First is input map, second is output
}
// AddHandler is called for each admin function to add the handler and help documentation to the API.
func (a *AdminSocket) AddHandler(name, desc string, args []string, handlerfunc core.AddHandlerFunc) error {
func (a *AdminSocket) AddHandler(name string, args []string, handlerfunc func(Info) (Info, error)) error {
if _, ok := a.handlers[strings.ToLower(name)]; ok {
return errors.New("handler already exists")
}
a.handlers[strings.ToLower(name)] = handler{
desc: desc,
args: args,
handler: handlerfunc,
}
@ -71,55 +54,347 @@ func (a *AdminSocket) AddHandler(name, desc string, args []string, handlerfunc c
}
// Init runs the initial admin setup.
func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, error) {
a := &AdminSocket{
core: c,
log: log,
handlers: make(map[string]handler),
}
for _, opt := range opts {
a._applyOption(opt)
}
if a.config.listenaddr == "none" || a.config.listenaddr == "" {
return nil, nil
}
func (a *AdminSocket) Init(c *yggdrasil.Core, state *config.NodeState, log *log.Logger, options interface{}) error {
a.core = c
a.log = log
a.handlers = make(map[string]handler)
current := state.GetCurrent()
a.listenaddr = current.AdminListen
a.AddHandler("list", []string{}, func(in Info) (Info, error) {
handlers := make(map[string]interface{})
for handlername, handler := range a.handlers {
handlers[handlername] = Info{"fields": handler.args}
}
return Info{"list": handlers}, nil
})
return nil
}
listenaddr := string(a.config.listenaddr)
u, err := url.Parse(listenaddr)
func (a *AdminSocket) UpdateConfig(config *config.NodeConfig) {
a.log.Debugln("Reloading admin configuration...")
if a.listenaddr != config.AdminListen {
a.listenaddr = config.AdminListen
if a.IsStarted() {
a.Stop()
}
a.Start()
}
}
func (a *AdminSocket) SetupAdminHandlers(na *AdminSocket) {
a.AddHandler("getSelf", []string{}, func(in Info) (Info, error) {
ip := a.core.Address().String()
subnet := a.core.Subnet()
return Info{
"self": Info{
ip: Info{
"box_pub_key": a.core.EncryptionPublicKey(),
"build_name": version.BuildName(),
"build_version": version.BuildVersion(),
"coords": fmt.Sprintf("%v", a.core.Coords()),
"subnet": subnet.String(),
},
},
}, nil
})
a.AddHandler("getPeers", []string{}, func(in Info) (Info, error) {
peers := make(Info)
for _, p := range a.core.GetPeers() {
addr := *address.AddrForNodeID(crypto.GetNodeID(&p.PublicKey))
so := net.IP(addr[:]).String()
peers[so] = Info{
"port": p.Port,
"uptime": p.Uptime.Seconds(),
"bytes_sent": p.BytesSent,
"bytes_recvd": p.BytesRecvd,
"proto": p.Protocol,
"endpoint": p.Endpoint,
"box_pub_key": hex.EncodeToString(p.PublicKey[:]),
}
}
return Info{"peers": peers}, nil
})
a.AddHandler("getSwitchPeers", []string{}, func(in Info) (Info, error) {
switchpeers := make(Info)
for _, s := range a.core.GetSwitchPeers() {
addr := *address.AddrForNodeID(crypto.GetNodeID(&s.PublicKey))
so := fmt.Sprint(s.Port)
switchpeers[so] = Info{
"ip": net.IP(addr[:]).String(),
"coords": fmt.Sprintf("%v", s.Coords),
"port": s.Port,
"bytes_sent": s.BytesSent,
"bytes_recvd": s.BytesRecvd,
"proto": s.Protocol,
"endpoint": s.Endpoint,
"box_pub_key": hex.EncodeToString(s.PublicKey[:]),
}
}
return Info{"switchpeers": switchpeers}, nil
})
/*
a.AddHandler("getSwitchQueues", []string{}, func(in Info) (Info, error) {
queues := a.core.GetSwitchQueues()
return Info{"switchqueues": queues.asMap()}, nil
})
*/
a.AddHandler("getDHT", []string{}, func(in Info) (Info, error) {
dht := make(Info)
for _, d := range a.core.GetDHT() {
addr := *address.AddrForNodeID(crypto.GetNodeID(&d.PublicKey))
so := net.IP(addr[:]).String()
dht[so] = Info{
"coords": fmt.Sprintf("%v", d.Coords),
"last_seen": d.LastSeen.Seconds(),
"box_pub_key": hex.EncodeToString(d.PublicKey[:]),
}
}
return Info{"dht": dht}, nil
})
a.AddHandler("getSessions", []string{}, func(in Info) (Info, error) {
sessions := make(Info)
for _, s := range a.core.GetSessions() {
addr := *address.AddrForNodeID(crypto.GetNodeID(&s.PublicKey))
so := net.IP(addr[:]).String()
sessions[so] = Info{
"coords": fmt.Sprintf("%v", s.Coords),
"bytes_sent": s.BytesSent,
"bytes_recvd": s.BytesRecvd,
"mtu": s.MTU,
"uptime": s.Uptime.Seconds(),
"was_mtu_fixed": s.WasMTUFixed,
"box_pub_key": hex.EncodeToString(s.PublicKey[:]),
}
}
return Info{"sessions": sessions}, nil
})
a.AddHandler("addPeer", []string{"uri", "[interface]"}, func(in Info) (Info, error) {
// Set sane defaults
intf := ""
// Has interface been specified?
if itf, ok := in["interface"]; ok {
intf = itf.(string)
}
if a.core.AddPeer(in["uri"].(string), intf) == nil {
return Info{
"added": []string{
in["uri"].(string),
},
}, nil
}
return Info{
"not_added": []string{
in["uri"].(string),
},
}, errors.New("Failed to add peer")
})
a.AddHandler("disconnectPeer", []string{"port"}, func(in Info) (Info, error) {
port, err := strconv.ParseInt(fmt.Sprint(in["port"]), 10, 64)
if err != nil {
return Info{}, err
}
if a.core.DisconnectPeer(uint64(port)) == nil {
return Info{
"disconnected": []string{
fmt.Sprint(port),
},
}, nil
} else {
return Info{
"not_disconnected": []string{
fmt.Sprint(port),
},
}, errors.New("Failed to disconnect peer")
}
})
a.AddHandler("removePeer", []string{"uri", "[interface]"}, func(in Info) (Info, error) {
// Set sane defaults
intf := ""
// Has interface been specified?
if itf, ok := in["interface"]; ok {
intf = itf.(string)
}
if a.core.RemovePeer(in["uri"].(string), intf) == nil {
return Info{
"removed": []string{
in["uri"].(string),
},
}, nil
} else {
return Info{
"not_removed": []string{
in["uri"].(string),
},
}, errors.New("Failed to remove peer")
}
return Info{
"not_removed": []string{
in["uri"].(string),
},
}, errors.New("Failed to remove peer")
})
a.AddHandler("getAllowedEncryptionPublicKeys", []string{}, func(in Info) (Info, error) {
return Info{"allowed_box_pubs": a.core.GetAllowedEncryptionPublicKeys()}, nil
})
a.AddHandler("addAllowedEncryptionPublicKey", []string{"box_pub_key"}, func(in Info) (Info, error) {
if a.core.AddAllowedEncryptionPublicKey(in["box_pub_key"].(string)) == nil {
return Info{
"added": []string{
in["box_pub_key"].(string),
},
}, nil
}
return Info{
"not_added": []string{
in["box_pub_key"].(string),
},
}, errors.New("Failed to add allowed key")
})
a.AddHandler("removeAllowedEncryptionPublicKey", []string{"box_pub_key"}, func(in Info) (Info, error) {
if a.core.RemoveAllowedEncryptionPublicKey(in["box_pub_key"].(string)) == nil {
return Info{
"removed": []string{
in["box_pub_key"].(string),
},
}, nil
}
return Info{
"not_removed": []string{
in["box_pub_key"].(string),
},
}, errors.New("Failed to remove allowed key")
})
a.AddHandler("dhtPing", []string{"box_pub_key", "coords", "[target]"}, func(in Info) (Info, error) {
var reserr error
var result yggdrasil.DHTRes
if in["target"] == nil {
in["target"] = "none"
}
coords := util.DecodeCoordString(in["coords"].(string))
var boxPubKey crypto.BoxPubKey
if b, err := hex.DecodeString(in["box_pub_key"].(string)); err == nil {
copy(boxPubKey[:], b)
if n, err := hex.DecodeString(in["target"].(string)); err == nil {
var targetNodeID crypto.NodeID
copy(targetNodeID[:], n)
result, reserr = a.core.DHTPing(boxPubKey, coords, &targetNodeID)
} else {
result, reserr = a.core.DHTPing(boxPubKey, coords, nil)
}
} else {
return Info{}, err
}
if reserr != nil {
return Info{}, reserr
}
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.PublicKey[:]),
"coords": fmt.Sprintf("%v", dinfo.Coords),
}
addr := net.IP(address.AddrForNodeID(crypto.GetNodeID(&dinfo.PublicKey))[:]).String()
infos[addr] = info
}
return Info{"nodes": infos}, nil
})
a.AddHandler("getNodeInfo", []string{"[box_pub_key]", "[coords]", "[nocache]"}, func(in Info) (Info, error) {
var nocache bool
if in["nocache"] != nil {
nocache = in["nocache"].(string) == "true"
}
var boxPubKey crypto.BoxPubKey
var coords []uint64
if in["box_pub_key"] == nil && in["coords"] == nil {
nodeinfo := a.core.MyNodeInfo()
var jsoninfo interface{}
if err := json.Unmarshal(nodeinfo, &jsoninfo); err != nil {
return Info{}, err
}
return Info{"nodeinfo": jsoninfo}, nil
} else if in["box_pub_key"] == nil || in["coords"] == nil {
return Info{}, errors.New("Expecting both box_pub_key and coords")
} else {
if b, err := hex.DecodeString(in["box_pub_key"].(string)); err == nil {
copy(boxPubKey[:], b)
} else {
return Info{}, err
}
coords = util.DecodeCoordString(in["coords"].(string))
}
result, err := a.core.GetNodeInfo(boxPubKey, coords, nocache)
if err == nil {
var m map[string]interface{}
if err = json.Unmarshal(result, &m); err == nil {
return Info{"nodeinfo": m}, nil
}
return Info{}, err
}
return Info{}, err
})
}
// Start runs the admin API socket to listen for / respond to admin API calls.
func (a *AdminSocket) Start() error {
if a.listenaddr != "none" && a.listenaddr != "" {
go a.listen()
a.started = true
}
return nil
}
// IsStarted returns true if the module has been started.
func (a *AdminSocket) IsStarted() bool {
return a.started
}
// Stop will stop the admin API and close the socket.
func (a *AdminSocket) Stop() error {
if a.listener != nil {
a.started = false
return a.listener.Close()
}
return nil
}
// listen is run by start and manages API connections.
func (a *AdminSocket) listen() {
u, err := url.Parse(a.listenaddr)
if err == nil {
switch strings.ToLower(u.Scheme) {
case "unix":
if _, err := os.Stat(u.Path); err == nil {
a.log.Debugln("Admin socket", u.Path, "already exists, trying to clean up")
if _, err := net.DialTimeout("unix", u.Path, time.Second*2); err == nil || err.(net.Error).Timeout() {
a.log.Errorln("Admin socket", u.Path, "already exists and is in use by another process")
if _, err := os.Stat(a.listenaddr[7:]); err == nil {
a.log.Debugln("Admin socket", a.listenaddr[7:], "already exists, trying to clean up")
if _, err := net.DialTimeout("unix", a.listenaddr[7:], time.Second*2); err == nil || err.(net.Error).Timeout() {
a.log.Errorln("Admin socket", a.listenaddr[7:], "already exists and is in use by another process")
os.Exit(1)
} else {
if err := os.Remove(u.Path); err == nil {
a.log.Debugln(u.Path, "was cleaned up")
if err := os.Remove(a.listenaddr[7:]); err == nil {
a.log.Debugln(a.listenaddr[7:], "was cleaned up")
} else {
a.log.Errorln(u.Path, "already exists and was not cleaned up:", err)
a.log.Errorln(a.listenaddr[7:], "already exists and was not cleaned up:", err)
os.Exit(1)
}
}
}
a.listener, err = net.Listen("unix", u.Path)
a.listener, err = net.Listen("unix", a.listenaddr[7:])
if err == nil {
switch u.Path[:1] {
switch a.listenaddr[7:8] {
case "@": // maybe abstract namespace
default:
if err := os.Chmod(u.Path, 0660); err != nil {
a.log.Warnln("WARNING:", u.Path, "may have unsafe permissions!")
if err := os.Chmod(a.listenaddr[7:], 0660); err != nil {
a.log.Warnln("WARNING:", a.listenaddr[:7], "may have unsafe permissions!")
}
}
}
case "tcp":
a.listener, err = net.Listen("tcp", u.Host)
default:
a.listener, err = net.Listen("tcp", listenaddr)
// err = errors.New(fmt.Sprint("protocol not supported: ", u.Scheme))
a.listener, err = net.Listen("tcp", a.listenaddr)
}
} else {
a.listener, err = net.Listen("tcp", listenaddr)
a.listener, err = net.Listen("tcp", a.listenaddr)
}
if err != nil {
a.log.Errorf("Admin socket failed to listen: %v", err)
@ -128,170 +403,11 @@ func New(c *core.Core, log core.Logger, opts ...SetupOption) (*AdminSocket, erro
a.log.Infof("%s admin socket listening on %s",
strings.ToUpper(a.listener.Addr().Network()),
a.listener.Addr().String())
_ = a.AddHandler("list", "List available commands", []string{}, func(_ json.RawMessage) (interface{}, error) {
res := &ListResponse{}
for name, handler := range a.handlers {
res.List = append(res.List, ListEntry{
Command: name,
Description: handler.desc,
Fields: handler.args,
})
}
sort.SliceStable(res.List, func(i, j int) bool {
return strings.Compare(res.List[i].Command, res.List[j].Command) < 0
})
return res, nil
})
a.done = make(chan struct{})
go a.listen()
return a, a.core.SetAdmin(a)
}
func (a *AdminSocket) SetupAdminHandlers() {
_ = a.AddHandler(
"getSelf", "Show details about this node", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetSelfRequest{}
res := &GetSelfResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getSelfHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getPeers", "Show directly connected peers", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetPeersRequest{}
res := &GetPeersResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getPeersHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getTree", "Show known Tree entries", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetTreeRequest{}
res := &GetTreeResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getTreeHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getPaths", "Show established paths through this node", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetPathsRequest{}
res := &GetPathsResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getPathsHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"getSessions", "Show established traffic sessions with remote nodes", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetSessionsRequest{}
res := &GetSessionsResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.getSessionsHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"addPeer", "Add a peer to the peer list", []string{"uri", "interface"},
func(in json.RawMessage) (interface{}, error) {
req := &AddPeerRequest{}
res := &AddPeerResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.addPeerHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
_ = a.AddHandler(
"removePeer", "Remove a peer from the peer list", []string{"uri", "interface"},
func(in json.RawMessage) (interface{}, error) {
req := &RemovePeerRequest{}
res := &RemovePeerResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := a.removePeerHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
}
// IsStarted returns true if the module has been started.
func (a *AdminSocket) IsStarted() bool {
select {
case <-a.done:
// Not blocking, so we're not currently running
return false
default:
// Blocked, so we must have started
return true
}
}
// Stop will stop the admin API and close the socket.
func (a *AdminSocket) Stop() error {
if a == nil {
return nil
}
if a.listener != nil {
select {
case <-a.done:
default:
close(a.done)
}
return a.listener.Close()
}
return nil
}
// listen is run by start and manages API connections.
func (a *AdminSocket) listen() {
defer a.listener.Close()
for {
conn, err := a.listener.Accept()
if err == nil {
go a.handleRequest(conn)
} else {
select {
case <-a.done:
// Not blocked, so we havent started or already stopped
return
default:
// Blocked, so we're supposed to keep running
}
}
}
}
@ -299,72 +415,104 @@ func (a *AdminSocket) listen() {
// handleRequest calls the request handler for each request sent to the admin API.
func (a *AdminSocket) handleRequest(conn net.Conn) {
decoder := json.NewDecoder(conn)
decoder.DisallowUnknownFields()
encoder := json.NewEncoder(conn)
encoder.SetIndent("", " ")
recv := make(Info)
send := make(Info)
defer conn.Close()
defer func() {
r := recover()
if r != nil {
send = Info{
"status": "error",
"error": "Check your syntax and input types",
}
a.log.Debugln("Admin socket error:", r)
if err := encoder.Encode(&send); err != nil {
a.log.Debugln("Admin socket JSON encode error:", err)
}
conn.Close()
}
}()
for {
var err error
var buf json.RawMessage
var req AdminSocketRequest
var resp AdminSocketResponse
req.Arguments = []byte("{}")
if err := func() error {
if err = decoder.Decode(&buf); err != nil {
return fmt.Errorf("Failed to find request")
// Start with a clean slate on each request
recv = Info{}
send = Info{}
// Decode the input
if err := decoder.Decode(&recv); err != nil {
a.log.Debugln("Admin socket JSON decode error:", err)
return
}
// Send the request back with the response, and default to "error"
// unless the status is changed below by one of the handlers
send["request"] = recv
send["status"] = "error"
n := strings.ToLower(recv["request"].(string))
if _, ok := recv["request"]; !ok {
send["error"] = "No request sent"
goto respond
}
if h, ok := a.handlers[n]; ok {
// Check that we have all the required arguments
for _, arg := range h.args {
// An argument in [square brackets] is optional and not required,
// so we can safely ignore those
if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
continue
}
// Check if the field is missing
if _, ok := recv[arg]; !ok {
send = Info{
"status": "error",
"error": "Expected field missing: " + arg,
"expecting": arg,
}
goto respond
}
}
if err = json.Unmarshal(buf, &req); err != nil {
return fmt.Errorf("Failed to unmarshal request")
}
resp.Request = req
if req.Name == "" {
return fmt.Errorf("No request specified")
}
reqname := strings.ToLower(req.Name)
handler, ok := a.handlers[reqname]
if !ok {
return fmt.Errorf("Unknown action '%s', try 'list' for help", reqname)
}
res, err := handler.handler(req.Arguments)
// By this point we should have all the fields we need, so call
// the handler
response, err := h.handler(recv)
if err != nil {
return err
send["error"] = err.Error()
if response != nil {
send["response"] = response
goto respond
}
} else {
send["status"] = "success"
if response != nil {
send["response"] = response
goto respond
}
}
if resp.Response, err = json.Marshal(res); err != nil {
return fmt.Errorf("Failed to marshal response: %w", err)
}
resp.Status = "success"
return nil
}(); err != nil {
resp.Status = "error"
resp.Error = err.Error()
}
if err = encoder.Encode(resp); err != nil {
a.log.Debugln("Encode error:", err)
}
if !req.KeepAlive {
break
} else {
continue
// Start with a clean response on each request, which defaults to an error
// state. If a handler is found below then this will be overwritten
send = Info{
"request": recv,
"status": "error",
"error": fmt.Sprintf("Unknown action '%s', try 'list' for help", recv["request"].(string)),
}
goto respond
}
// Send the response back
respond:
if err := encoder.Encode(&send); err != nil {
return
}
// If "keepalive" isn't true then close the connection
if keepalive, ok := recv["keepalive"]; !ok || !keepalive.(bool) {
conn.Close()
}
}
}
type DataUnit uint64
func (d DataUnit) String() string {
switch {
case d >= 1024*1024*1024*1024:
return fmt.Sprintf("%2.1fTB", float64(d)/1024/1024/1024/1024)
case d >= 1024*1024*1024:
return fmt.Sprintf("%2.1fGB", float64(d)/1024/1024/1024)
case d >= 1024*1024:
return fmt.Sprintf("%2.1fMB", float64(d)/1024/1024)
case d >= 100:
return fmt.Sprintf("%2.1fKB", float64(d)/1024)
default:
return fmt.Sprintf("%dB", d)
}
}

View file

@ -1,5 +0,0 @@
package admin
type ErrorResponse struct {
Error string `json:"error"`
}

View file

@ -1,42 +0,0 @@
package admin
import (
"encoding/hex"
"net"
"slices"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type GetPathsRequest struct {
}
type GetPathsResponse struct {
Paths []PathEntry `json:"paths"`
}
type PathEntry struct {
IPAddress string `json:"address"`
PublicKey string `json:"key"`
Path []uint64 `json:"path"`
Sequence uint64 `json:"sequence"`
}
func (a *AdminSocket) getPathsHandler(_ *GetPathsRequest, res *GetPathsResponse) error {
paths := a.core.GetPaths()
res.Paths = make([]PathEntry, 0, len(paths))
for _, p := range paths {
addr := address.AddrForKey(p.Key)
res.Paths = append(res.Paths, PathEntry{
IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(p.Key),
Path: p.Path,
Sequence: p.Sequence,
})
}
slices.SortStableFunc(res.Paths, func(a, b PathEntry) int {
return strings.Compare(a.PublicKey, b.PublicKey)
})
return nil
}

View file

@ -1,91 +0,0 @@
package admin
import (
"encoding/hex"
"net"
"slices"
"strings"
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type GetPeersRequest struct {
}
type GetPeersResponse struct {
Peers []PeerEntry `json:"peers"`
}
type PeerEntry struct {
URI string `json:"remote,omitempty"`
Up bool `json:"up"`
Inbound bool `json:"inbound"`
IPAddress string `json:"address,omitempty"`
PublicKey string `json:"key"`
Port uint64 `json:"port"`
Priority uint64 `json:"priority"`
Cost uint64 `json:"cost"`
RXBytes DataUnit `json:"bytes_recvd,omitempty"`
TXBytes DataUnit `json:"bytes_sent,omitempty"`
RXRate DataUnit `json:"rate_recvd,omitempty"`
TXRate DataUnit `json:"rate_sent,omitempty"`
Uptime float64 `json:"uptime,omitempty"`
Latency time.Duration `json:"latency,omitempty"`
LastErrorTime time.Duration `json:"last_error_time,omitempty"`
LastError string `json:"last_error,omitempty"`
}
func (a *AdminSocket) getPeersHandler(_ *GetPeersRequest, res *GetPeersResponse) error {
peers := a.core.GetPeers()
res.Peers = make([]PeerEntry, 0, len(peers))
for _, p := range peers {
peer := PeerEntry{
Port: p.Port,
Up: p.Up,
Inbound: p.Inbound,
Priority: uint64(p.Priority), // can't be uint8 thanks to gobind
Cost: p.Cost,
URI: p.URI,
RXBytes: DataUnit(p.RXBytes),
TXBytes: DataUnit(p.TXBytes),
RXRate: DataUnit(p.RXRate),
TXRate: DataUnit(p.TXRate),
Uptime: p.Uptime.Seconds(),
}
if p.Latency > 0 {
peer.Latency = p.Latency
}
if addr := address.AddrForKey(p.Key); addr != nil {
peer.PublicKey = hex.EncodeToString(p.Key)
peer.IPAddress = net.IP(addr[:]).String()
}
if p.LastError != nil {
peer.LastError = p.LastError.Error()
peer.LastErrorTime = time.Since(p.LastErrorTime)
}
res.Peers = append(res.Peers, peer)
}
slices.SortStableFunc(res.Peers, func(a, b PeerEntry) int {
if !a.Inbound && b.Inbound {
return -1
}
if a.Inbound && !b.Inbound {
return 1
}
if d := strings.Compare(a.PublicKey, b.PublicKey); d != 0 {
return d
}
if d := a.Priority - b.Priority; d != 0 {
return int(d)
}
if d := a.Cost - b.Cost; d != 0 {
return int(d)
}
if d := a.Uptime - b.Uptime; d != 0 {
return int(d)
}
return 0
})
return nil
}

View file

@ -1,30 +0,0 @@
package admin
import (
"encoding/hex"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
type GetSelfRequest struct{}
type GetSelfResponse struct {
BuildName string `json:"build_name"`
BuildVersion string `json:"build_version"`
PublicKey string `json:"key"`
IPAddress string `json:"address"`
RoutingEntries uint64 `json:"routing_entries"`
Subnet string `json:"subnet"`
}
func (a *AdminSocket) getSelfHandler(_ *GetSelfRequest, res *GetSelfResponse) error {
self := a.core.GetSelf()
snet := a.core.Subnet()
res.BuildName = version.BuildName()
res.BuildVersion = version.BuildVersion()
res.PublicKey = hex.EncodeToString(self.Key[:])
res.IPAddress = a.core.Address().String()
res.Subnet = snet.String()
res.RoutingEntries = self.RoutingEntries
return nil
}

View file

@ -1,43 +0,0 @@
package admin
import (
"encoding/hex"
"net"
"slices"
"strings"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type GetSessionsRequest struct{}
type GetSessionsResponse struct {
Sessions []SessionEntry `json:"sessions"`
}
type SessionEntry struct {
IPAddress string `json:"address"`
PublicKey string `json:"key"`
RXBytes DataUnit `json:"bytes_recvd"`
TXBytes DataUnit `json:"bytes_sent"`
Uptime float64 `json:"uptime"`
}
func (a *AdminSocket) getSessionsHandler(_ *GetSessionsRequest, res *GetSessionsResponse) error {
sessions := a.core.GetSessions()
res.Sessions = make([]SessionEntry, 0, len(sessions))
for _, s := range sessions {
addr := address.AddrForKey(s.Key)
res.Sessions = append(res.Sessions, SessionEntry{
IPAddress: net.IP(addr[:]).String(),
PublicKey: hex.EncodeToString(s.Key[:]),
RXBytes: DataUnit(s.RXBytes),
TXBytes: DataUnit(s.TXBytes),
Uptime: s.Uptime.Seconds(),
})
}
slices.SortStableFunc(res.Sessions, func(a, b SessionEntry) int {
return strings.Compare(a.PublicKey, b.PublicKey)
})
return nil
}

View file

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

View file

@ -1,79 +0,0 @@
package admin
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"net"
"sync"
"time"
"github.com/Arceliar/ironwood/network"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
func (c *AdminSocket) _applyOption(opt SetupOption) {
switch v := opt.(type) {
case ListenAddress:
c.config.listenaddr = v
case LogLookups:
c.logLookups()
}
}
type SetupOption interface {
isSetupOption()
}
type ListenAddress string
func (a ListenAddress) isSetupOption() {}
type LogLookups struct{}
func (l LogLookups) isSetupOption() {}
func (a *AdminSocket) logLookups() {
type resi struct {
Address string `json:"addr"`
Key string `json:"key"`
Path []uint64 `json:"path"`
Time int64 `json:"time"`
}
type res struct {
Infos []resi `json:"infos"`
}
type info struct {
path []uint64
time time.Time
}
type edk [ed25519.PublicKeySize]byte
infos := make(map[edk]info)
var m sync.Mutex
a.core.PacketConn.PacketConn.Debug.SetDebugLookupLogger(func(l network.DebugLookupInfo) {
var k edk
copy(k[:], l.Key[:])
m.Lock()
infos[k] = info{path: l.Path, time: time.Now()}
m.Unlock()
})
_ = a.AddHandler(
"lookups", "Dump a record of lookups received in the past hour", []string{},
func(in json.RawMessage) (interface{}, error) {
m.Lock()
rs := make([]resi, 0, len(infos))
for k, v := range infos {
if time.Since(v.time) > 24*time.Hour {
// TODO? automatic cleanup, so we don't need to call lookups periodically to prevent leaks
delete(infos, k)
}
a := address.AddrForKey(ed25519.PublicKey(k[:]))
addr := net.IP(a[:]).String()
rs = append(rs, resi{Address: addr, Key: hex.EncodeToString(k[:]), Path: v.path, Time: v.time.Unix()})
}
m.Unlock()
return &res{Infos: rs}, nil
},
)
}

View file

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

View file

@ -17,244 +17,143 @@ configuration option that is not provided.
package config
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"os"
"time"
"sync"
"github.com/hjson/hjson-go/v4"
"golang.org/x/text/encoding/unicode"
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
"github.com/yggdrasil-network/yggdrasil-go/src/defaults"
"github.com/yggdrasil-network/yggdrasil-go/src/types"
)
type MTU = types.MTU
// NodeState represents the active and previous configuration of an Yggdrasil
// node. A NodeState object is returned when starting an Yggdrasil node. Note
// that this structure and related functions are likely to disappear soon.
type NodeState struct {
Current NodeConfig
Previous NodeConfig
Mutex sync.RWMutex
}
// Current returns the active node configuration.
func (s *NodeState) GetCurrent() NodeConfig {
s.Mutex.RLock()
defer s.Mutex.RUnlock()
return s.Current
}
// Previous returns the previous node configuration.
func (s *NodeState) GetPrevious() NodeConfig {
s.Mutex.RLock()
defer s.Mutex.RUnlock()
return s.Previous
}
// Replace the node configuration with new configuration.
func (s *NodeState) Replace(n NodeConfig) {
s.Mutex.Lock()
defer s.Mutex.Unlock()
s.Previous = s.Current
s.Current = n
}
// NodeConfig is the main configuration structure, containing configuration
// options that are necessary for an Yggdrasil node to run. You will need to
// supply one of these structs to the Yggdrasil core when starting a node.
type NodeConfig struct {
PrivateKey KeyBytes `json:",omitempty" comment:"Your private key. DO NOT share this with anyone!"`
PrivateKeyPath string `json:",omitempty" comment:"The path to your private key file in PEM format."`
Certificate *tls.Certificate `json:"-"`
Peers []string `comment:"List of outbound peer connection strings (e.g. tls://a.b.c.d:e or\nsocks://a.b.c.d:e/f.g.h.i:j). Connection strings can contain options,\nsee https://yggdrasil-network.github.io/configurationref.html#peers.\nYggdrasil has no concept of bootstrap nodes - all network traffic\nwill transit peer connections. Therefore make sure to only peer with\nnearby nodes that have good connectivity and low latency. Avoid adding\npeers to this list from distant countries as this will worsen your\nnode's connectivity and performance considerably."`
InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ \"tls://a.b.c.d:e\" ] }.\nYou should only use this option if your machine is multi-homed and you\nwant to establish outbound peer connections on different interfaces.\nOtherwise you should use \"Peers\"."`
Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nThis is not required if you wish to establish outbound peerings only.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntls://0.0.0.0:0 or tls://[::]:0 to listen on all interfaces."`
AdminListen string `json:",omitempty" comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."`
MulticastInterfaces []MulticastInterfaceConfig `comment:"Configuration for which interfaces multicast peer discovery should be\nenabled on. Regex is a regular expression which is matched against an\ninterface name, and interfaces use the first configuration that they\nmatch against. Beacon controls whether or not your node advertises its\npresence to others, whereas Listen controls whether or not your node\nlistens out for and tries to connect to other advertising nodes. See\nhttps://yggdrasil-network.github.io/configurationref.html#multicastinterfaces\nfor more supported options."`
AllowedPublicKeys []string `comment:"List of peer public keys to allow incoming peering connections\nfrom. If left empty/undefined then all connections will be allowed\nby default. This does not affect outgoing peerings, nor does it\naffect link-local peers discovered via multicast.\nWARNING: THIS IS NOT A FIREWALL and DOES NOT limit who can reach\nopen ports or services running on your machine!"`
IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."`
IfMTU uint64 `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
LogLookups bool `json:",omitempty"`
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
NodeInfo map[string]interface{} `comment:"Optional nodeinfo. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
Peers []string `comment:"List of connection strings for outbound peer connections in URI format,\ne.g. tcp://a.b.c.d:e or socks://a.b.c.d:e/f.g.h.i:j. These connections\nwill obey the operating system routing table, therefore you should\nuse this section when you may connect via different interfaces."`
InterfacePeers map[string][]string `comment:"List of connection strings for outbound peer connections in URI format,\narranged by source interface, e.g. { \"eth0\": [ tcp://a.b.c.d:e ] }.\nNote that SOCKS peerings will NOT be affected by this option and should\ngo in the \"Peers\" section instead."`
Listen []string `comment:"Listen addresses for incoming connections. You will need to add\nlisteners in order to accept incoming peerings from non-local nodes.\nMulticast peer discovery will work regardless of any listeners set\nhere. Each listener should be specified in URI format as above, e.g.\ntcp://0.0.0.0:0 or tcp://[::]:0 to listen on all interfaces."`
AdminListen string `comment:"Listen address for admin connections. Default is to listen for local\nconnections either on TCP/9001 or a UNIX socket depending on your\nplatform. Use this value for yggdrasilctl -endpoint=X. To disable\nthe admin socket, use the value \"none\" instead."`
MulticastInterfaces []string `comment:"Regular expressions for which interfaces multicast peer discovery\nshould be enabled on. If none specified, multicast peer discovery is\ndisabled. The default value is .* which uses all interfaces."`
AllowedEncryptionPublicKeys []string `comment:"List of peer encryption public keys to allow incoming TCP peering\nconnections from. If left empty/undefined then all connections will\nbe allowed by default. This does not affect outgoing peerings, nor\ndoes it affect link-local peers discovered via multicast."`
EncryptionPublicKey string `comment:"Your public encryption key. Your peers may ask you for this to put\ninto their AllowedEncryptionPublicKeys configuration."`
EncryptionPrivateKey string `comment:"Your private encryption key. DO NOT share this with anyone!"`
SigningPublicKey string `comment:"Your public signing key. You should not ordinarily need to share\nthis with anyone."`
SigningPrivateKey string `comment:"Your private signing key. DO NOT share this with anyone!"`
LinkLocalTCPPort uint16 `comment:"The port number to be used for the link-local TCP listeners for the\nconfigured MulticastInterfaces. This option does not affect listeners\nspecified in the Listen option. Unless you plan to firewall link-local\ntraffic, it is best to leave this as the default value of 0. This\noption cannot currently be changed by reloading config during runtime."`
IfName string `comment:"Local network interface name for TUN adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN."`
IfMTU MTU `comment:"Maximum Transmission Unit (MTU) size for your local TUN interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."`
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."`
NodeInfoPrivacy bool `comment:"By default, nodeinfo contains some defaults including the platform,\narchitecture and Yggdrasil version. These can help when surveying\nthe network and diagnosing network routing problems. Enabling\nnodeinfo privacy prevents this, so that only items specified in\n\"NodeInfo\" are sent back if specified."`
NodeInfo map[string]interface{} `comment:"Optional node info. This must be a { \"key\": \"value\", ... } map\nor set as null. This is entirely optional but, if set, is visible\nto the whole network on request."`
}
type MulticastInterfaceConfig struct {
Regex string
Beacon bool
Listen bool
Port uint16 `json:",omitempty"`
Priority uint64 `json:",omitempty"` // really uint8, but gobind won't export it
Password string
// SessionFirewall controls the session firewall configuration.
type SessionFirewall struct {
Enable bool `comment:"Enable or disable the session firewall. If disabled, network traffic\nfrom any node will be allowed. If enabled, the below rules apply."`
AllowFromDirect bool `comment:"Allow network traffic from directly connected peers."`
AllowFromRemote bool `comment:"Allow network traffic from remote nodes on the network that you are\nnot directly peered with."`
AlwaysAllowOutbound bool `comment:"Allow outbound network traffic regardless of AllowFromDirect or\nAllowFromRemote. This does allow a remote node to send unsolicited\ntraffic back to you for the length of the session."`
WhitelistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always accepted,\nregardless of AllowFromDirect or AllowFromRemote."`
BlacklistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always rejected,\nregardless of the whitelist, AllowFromDirect or AllowFromRemote."`
}
// TunnelRouting contains the crypto-key routing tables for tunneling regular
// IPv4 or IPv6 subnets across the Yggdrasil network.
type TunnelRouting struct {
Enable bool `comment:"Enable or disable tunnel routing."`
IPv6RemoteSubnets map[string]string `comment:"IPv6 subnets belonging to remote nodes, mapped to the node's public\nkey, e.g. { \"aaaa:bbbb:cccc::/e\": \"boxpubkey\", ... }"`
IPv6LocalSubnets []string `comment:"IPv6 subnets belonging to this node's end of the tunnels. Only traffic\nfrom these ranges (or the Yggdrasil node's IPv6 address/subnet)\nwill be tunnelled."`
IPv4RemoteSubnets map[string]string `comment:"IPv4 subnets belonging to remote nodes, mapped to the node's public\nkey, e.g. { \"a.b.c.d/e\": \"boxpubkey\", ... }"`
IPv4LocalSubnets []string `comment:"IPv4 subnets belonging to this node's end of the tunnels. Only traffic\nfrom these ranges will be tunnelled."`
}
// SwitchOptions contains tuning options for the switch. These are advanced
// options and shouldn't be changed unless necessary.
type SwitchOptions struct {
MaxTotalQueueSize uint64 `comment:"Maximum size of all switch queues combined (in bytes)."`
}
// Generates default configuration and returns a pointer to the resulting
// NodeConfig. This is used when outputting the -genconf parameter and also when
// using -autoconf.
func GenerateConfig() *NodeConfig {
// Get the defaults for the platform.
defaults := GetDefaults()
// Generate encryption keys.
bpub, bpriv := crypto.NewBoxKeys()
spub, spriv := crypto.NewSigKeys()
// Create a node configuration and populate it.
cfg := new(NodeConfig)
cfg.NewPrivateKey()
cfg := NodeConfig{}
cfg.Listen = []string{}
cfg.AdminListen = defaults.DefaultAdminListen
cfg.AdminListen = defaults.GetDefaults().DefaultAdminListen
cfg.EncryptionPublicKey = hex.EncodeToString(bpub[:])
cfg.EncryptionPrivateKey = hex.EncodeToString(bpriv[:])
cfg.SigningPublicKey = hex.EncodeToString(spub[:])
cfg.SigningPrivateKey = hex.EncodeToString(spriv[:])
cfg.Peers = []string{}
cfg.InterfacePeers = map[string][]string{}
cfg.AllowedPublicKeys = []string{}
cfg.MulticastInterfaces = defaults.DefaultMulticastInterfaces
cfg.IfName = defaults.DefaultIfName
cfg.IfMTU = defaults.DefaultIfMTU
cfg.AllowedEncryptionPublicKeys = []string{}
cfg.MulticastInterfaces = defaults.GetDefaults().DefaultMulticastInterfaces
cfg.IfName = defaults.GetDefaults().DefaultIfName
cfg.IfMTU = defaults.GetDefaults().DefaultIfMTU
cfg.SessionFirewall.Enable = false
cfg.SessionFirewall.AllowFromDirect = true
cfg.SessionFirewall.AllowFromRemote = true
cfg.SessionFirewall.AlwaysAllowOutbound = true
cfg.SwitchOptions.MaxTotalQueueSize = 4 * 1024 * 1024
cfg.NodeInfoPrivacy = false
if err := cfg.postprocessConfig(); err != nil {
panic(err)
}
return cfg
return &cfg
}
func (cfg *NodeConfig) ReadFrom(r io.Reader) (int64, error) {
conf, err := io.ReadAll(r)
if err != nil {
return 0, err
}
n := int64(len(conf))
// If there's a byte order mark - which Windows 10 is now incredibly fond of
// throwing everywhere when it's converting things into UTF-16 for the hell
// of it - remove it and decode back down into UTF-8. This is necessary
// because hjson doesn't know what to do with UTF-16 and will panic
if bytes.Equal(conf[0:2], []byte{0xFF, 0xFE}) ||
bytes.Equal(conf[0:2], []byte{0xFE, 0xFF}) {
utf := unicode.UTF16(unicode.BigEndian, unicode.UseBOM)
decoder := utf.NewDecoder()
conf, err = decoder.Bytes(conf)
if err != nil {
return n, err
}
}
// Generate a new configuration - this gives us a set of sane defaults -
// then parse the configuration we loaded above on top of it. The effect
// of this is that any configuration item that is missing from the provided
// configuration will use a sane default.
*cfg = *GenerateConfig()
if err := cfg.UnmarshalHJSON(conf); err != nil {
return n, err
}
return n, nil
// NewEncryptionKeys replaces the encryption keypair in the NodeConfig with a
// new encryption keypair. The encryption keys are used by the router to encrypt
// traffic and to derive the node ID and IPv6 address/subnet of the node, so
// this is equivalent to discarding the node's identity on the network.
func (cfg *NodeConfig) NewEncryptionKeys() {
bpub, bpriv := crypto.NewBoxKeys()
cfg.EncryptionPublicKey = hex.EncodeToString(bpub[:])
cfg.EncryptionPrivateKey = hex.EncodeToString(bpriv[:])
}
func (cfg *NodeConfig) UnmarshalHJSON(b []byte) error {
if err := hjson.Unmarshal(b, cfg); err != nil {
return err
}
return cfg.postprocessConfig()
}
func (cfg *NodeConfig) postprocessConfig() error {
if cfg.PrivateKeyPath != "" {
cfg.PrivateKey = nil
f, err := os.ReadFile(cfg.PrivateKeyPath)
if err != nil {
return err
}
if err := cfg.UnmarshalPEMPrivateKey(f); err != nil {
return err
}
}
switch {
case cfg.Certificate == nil:
// No self-signed certificate has been generated yet.
fallthrough
case !bytes.Equal(cfg.Certificate.PrivateKey.(ed25519.PrivateKey), cfg.PrivateKey):
// A self-signed certificate was generated but the private
// key has changed since then, possibly because a new config
// was parsed.
if err := cfg.GenerateSelfSignedCertificate(); err != nil {
return err
}
}
return nil
}
// RFC5280 section 4.1.2.5
var notAfterNeverExpires = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)
func (cfg *NodeConfig) GenerateSelfSignedCertificate() error {
key, err := cfg.MarshalPEMPrivateKey()
if err != nil {
return err
}
cert, err := cfg.MarshalPEMCertificate()
if err != nil {
return err
}
tlsCert, err := tls.X509KeyPair(cert, key)
if err != nil {
return err
}
cfg.Certificate = &tlsCert
return nil
}
func (cfg *NodeConfig) MarshalPEMCertificate() ([]byte, error) {
privateKey := ed25519.PrivateKey(cfg.PrivateKey)
publicKey := privateKey.Public().(ed25519.PublicKey)
cert := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: hex.EncodeToString(publicKey),
},
NotBefore: time.Now(),
NotAfter: notAfterNeverExpires,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
certbytes, err := x509.CreateCertificate(rand.Reader, cert, cert, publicKey, privateKey)
if err != nil {
return nil, err
}
block := &pem.Block{
Type: "CERTIFICATE",
Bytes: certbytes,
}
return pem.EncodeToMemory(block), nil
}
func (cfg *NodeConfig) NewPrivateKey() {
_, spriv, err := ed25519.GenerateKey(nil)
if err != nil {
panic(err)
}
cfg.PrivateKey = KeyBytes(spriv)
}
func (cfg *NodeConfig) MarshalPEMPrivateKey() ([]byte, error) {
b, err := x509.MarshalPKCS8PrivateKey(ed25519.PrivateKey(cfg.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to marshal PKCS8 key: %w", err)
}
block := &pem.Block{
Type: "PRIVATE KEY",
Bytes: b,
}
return pem.EncodeToMemory(block), nil
}
func (cfg *NodeConfig) UnmarshalPEMPrivateKey(b []byte) error {
p, _ := pem.Decode(b)
if p == nil {
return fmt.Errorf("failed to parse PEM file")
}
if p.Type != "PRIVATE KEY" {
return fmt.Errorf("unexpected PEM type %q", p.Type)
}
k, err := x509.ParsePKCS8PrivateKey(p.Bytes)
if err != nil {
return fmt.Errorf("failed to unmarshal PKCS8 key: %w", err)
}
key, ok := k.(ed25519.PrivateKey)
if !ok {
return fmt.Errorf("private key must be ed25519 key")
}
if len(key) != ed25519.PrivateKeySize {
return fmt.Errorf("unexpected ed25519 private key length")
}
cfg.PrivateKey = KeyBytes(key)
return nil
}
type KeyBytes []byte
func (k KeyBytes) MarshalJSON() ([]byte, error) {
return json.Marshal(hex.EncodeToString(k))
}
func (k *KeyBytes) UnmarshalJSON(b []byte) error {
var s string
var err error
if err = json.Unmarshal(b, &s); err != nil {
return err
}
*k, err = hex.DecodeString(s)
return err
// NewSigningKeys replaces the signing keypair in the NodeConfig with a new
// signing keypair. The signing keys are used by the switch to derive the
// structure of the spanning tree.
func (cfg *NodeConfig) NewSigningKeys() {
spub, spriv := crypto.NewSigKeys()
cfg.SigningPublicKey = hex.EncodeToString(spub[:])
cfg.SigningPrivateKey = hex.EncodeToString(spriv[:])
}

View file

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

View file

@ -1,34 +0,0 @@
package config
var defaultConfig = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultConfig=/path/to/config
var defaultAdminListen = "" // LDFLAGS='-X github.com/yggdrasil-network/yggdrasil-go/src/config.defaultAdminListen=unix://path/to/sock'
// Defines which parameters are expected by default for configuration on a
// specific platform. These values are populated in the relevant defaults_*.go
// for the platform being targeted. They must be set.
type platformDefaultParameters struct {
// Admin socket
DefaultAdminListen string
// Configuration (used for yggdrasilctl)
DefaultConfigFile string
// Multicast interfaces
DefaultMulticastInterfaces []MulticastInterfaceConfig
// TUN
MaximumIfMTU uint64
DefaultIfMTU uint64
DefaultIfName string
}
func GetDefaults() platformDefaultParameters {
defaults := getDefaults()
if defaultConfig != "" {
defaults.DefaultConfigFile = defaultConfig
}
if defaultAdminListen != "" {
defaults.DefaultAdminListen = defaultAdminListen
}
return defaults
}

View file

@ -1,265 +0,0 @@
package core
import (
"crypto/ed25519"
"encoding/json"
"net"
"net/url"
"sync/atomic"
"time"
"github.com/Arceliar/phony"
"github.com/Arceliar/ironwood/network"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
type SelfInfo struct {
Key ed25519.PublicKey
RoutingEntries uint64
}
type PeerInfo struct {
URI string
Up bool
Inbound bool
LastError error
LastErrorTime time.Time
Key ed25519.PublicKey
Root ed25519.PublicKey
Coords []uint64
Port uint64
Priority uint8
Cost uint64
RXBytes uint64
TXBytes uint64
RXRate uint64
TXRate uint64
Uptime time.Duration
Latency time.Duration
}
type TreeEntryInfo struct {
Key ed25519.PublicKey
Parent ed25519.PublicKey
Sequence uint64
//Port uint64
//Rest uint64
}
type PathEntryInfo struct {
Key ed25519.PublicKey
Path []uint64
Sequence uint64
}
type SessionInfo struct {
Key ed25519.PublicKey
RXBytes uint64
TXBytes uint64
Uptime time.Duration
}
func (c *Core) GetSelf() SelfInfo {
var self SelfInfo
s := c.PacketConn.PacketConn.Debug.GetSelf()
self.Key = s.Key
self.RoutingEntries = s.RoutingEntries
return self
}
func (c *Core) GetPeers() []PeerInfo {
peers := []PeerInfo{}
conns := map[net.Conn]network.DebugPeerInfo{}
iwpeers := c.PacketConn.PacketConn.Debug.GetPeers()
for _, p := range iwpeers {
conns[p.Conn] = p
}
phony.Block(&c.links, func() {
for info, state := range c.links._links {
var peerinfo PeerInfo
var conn net.Conn
peerinfo.URI = info.uri
peerinfo.LastError = state._err
peerinfo.LastErrorTime = state._errtime
if c := state._conn; c != nil {
conn = c
peerinfo.Up = true
peerinfo.Inbound = state.linkType == linkTypeIncoming
peerinfo.RXBytes = atomic.LoadUint64(&c.rx)
peerinfo.TXBytes = atomic.LoadUint64(&c.tx)
peerinfo.RXRate = atomic.LoadUint64(&c.rxrate)
peerinfo.TXRate = atomic.LoadUint64(&c.txrate)
peerinfo.Uptime = time.Since(c.up)
}
if p, ok := conns[conn]; ok {
peerinfo.Key = p.Key
peerinfo.Root = p.Root
peerinfo.Port = p.Port
peerinfo.Priority = p.Priority
peerinfo.Latency = p.Latency
peerinfo.Cost = p.Cost
}
peers = append(peers, peerinfo)
}
})
return peers
}
func (c *Core) GetTree() []TreeEntryInfo {
var trees []TreeEntryInfo
ts := c.PacketConn.PacketConn.Debug.GetTree()
for _, t := range ts {
var info TreeEntryInfo
info.Key = t.Key
info.Parent = t.Parent
info.Sequence = t.Sequence
//info.Port = d.Port
//info.Rest = d.Rest
trees = append(trees, info)
}
return trees
}
func (c *Core) GetPaths() []PathEntryInfo {
var paths []PathEntryInfo
ps := c.PacketConn.PacketConn.Debug.GetPaths()
for _, p := range ps {
var info PathEntryInfo
info.Key = p.Key
info.Sequence = p.Sequence
info.Path = p.Path
paths = append(paths, info)
}
return paths
}
func (c *Core) GetSessions() []SessionInfo {
var sessions []SessionInfo
ss := c.PacketConn.Debug.GetSessions()
for _, s := range ss {
var info SessionInfo
info.Key = s.Key
info.RXBytes = s.RX
info.TXBytes = s.TX
info.Uptime = s.Uptime
sessions = append(sessions, info)
}
return sessions
}
// Listen starts a new listener (either TCP or TLS). The input should be a url.URL
// parsed from a string of the form e.g. "tcp://a.b.c.d:e". In the case of a
// link-local address, the interface should be provided as the second argument.
func (c *Core) Listen(u *url.URL, sintf string) (*Listener, error) {
return c.links.listen(u, sintf, false)
}
// ListenLocal starts a listener, like the Listen function, but is used for
// more trustworthy situations where you want to ignore AllowedPublicKeys, i.e.
// with multicast listeners.
func (c *Core) ListenLocal(u *url.URL, sintf string) (*Listener, error) {
return c.links.listen(u, sintf, true)
}
// Address gets the IPv6 address of the Yggdrasil node. This is always a /128
// address. The IPv6 address is only relevant when the node is operating as an
// IP router and often is meaningless when embedded into an application, unless
// that application also implements either VPN functionality or deals with IP
// packets specifically.
func (c *Core) Address() net.IP {
addr := net.IP(address.AddrForKey(c.public)[:])
return addr
}
// Subnet gets the routed IPv6 subnet of the Yggdrasil node. This is always a
// /64 subnet. The IPv6 subnet is only relevant when the node is operating as an
// IP router and often is meaningless when embedded into an application, unless
// that application also implements either VPN functionality or deals with IP
// packets specifically.
func (c *Core) Subnet() net.IPNet {
subnet := address.SubnetForKey(c.public)[:]
subnet = append(subnet, 0, 0, 0, 0, 0, 0, 0, 0)
return net.IPNet{IP: subnet, Mask: net.CIDRMask(64, 128)}
}
// SetLogger sets the output logger of the Yggdrasil node after startup. This
// may be useful if you want to redirect the output later. Note that this
// expects a Logger from the github.com/gologme/log package and not from Go's
// built-in log package.
func (c *Core) SetLogger(log Logger) {
c.log = log
}
// AddPeer adds a peer. This should be specified in the peer URI format, e.g.:
//
// tcp://a.b.c.d:e
// socks://a.b.c.d:e/f.g.h.i:j
//
// This adds the peer to the peer list, so that they will be called again if the
// connection drops.
func (c *Core) AddPeer(u *url.URL, sintf string) error {
return c.links.add(u, sintf, linkTypePersistent)
}
// RemovePeer removes a peer. The peer should be specified in URI format, see AddPeer.
// The peer is not disconnected immediately.
func (c *Core) RemovePeer(u *url.URL, sintf string) error {
return c.links.remove(u, sintf, linkTypePersistent)
}
// CallPeer calls a peer once. This should be specified in the peer URI format,
// e.g.:
//
// tcp://a.b.c.d:e
// socks://a.b.c.d:e/f.g.h.i:j
//
// This does not add the peer to the peer list, so if the connection drops, the
// peer will not be called again automatically.
func (c *Core) CallPeer(u *url.URL, sintf string) error {
return c.links.add(u, sintf, linkTypeEphemeral)
}
func (c *Core) PublicKey() ed25519.PublicKey {
return c.public
}
// Hack to get the admin stuff working, TODO something cleaner
type AddHandler interface {
AddHandler(name, desc string, args []string, handlerfunc AddHandlerFunc) error
}
type AddHandlerFunc func(json.RawMessage) (interface{}, error)
// SetAdmin must be called after Init and before Start.
// It sets the admin handler for NodeInfo and the Debug admin functions.
func (c *Core) SetAdmin(a AddHandler) error {
if err := a.AddHandler(
"getNodeInfo", "Request nodeinfo from a remote node by its public key", []string{"key"},
c.proto.nodeinfo.nodeInfoAdminHandler,
); err != nil {
return err
}
if err := a.AddHandler(
"debug_remoteGetSelf", "Debug use only", []string{"key"},
c.proto.getSelfHandler,
); err != nil {
return err
}
if err := a.AddHandler(
"debug_remoteGetPeers", "Debug use only", []string{"key"},
c.proto.getPeersHandler,
); err != nil {
return err
}
if err := a.AddHandler(
"debug_remoteGetTree", "Debug use only", []string{"key"},
c.proto.getTreeHandler,
); err != nil {
return err
}
return nil
}

View file

@ -1,252 +0,0 @@
package core
import (
"context"
"crypto/ed25519"
"crypto/tls"
"fmt"
"io"
"net"
"net/url"
"time"
iwe "github.com/Arceliar/ironwood/encrypted"
iwn "github.com/Arceliar/ironwood/network"
iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
// The Core object represents the Yggdrasil node. You should create a Core
// object for each Yggdrasil node you plan to run.
type Core struct {
// This is the main data structure that holds everything else for a node
// We're going to keep our own copy of the provided config - that way we can
// guarantee that it will be covered by the mutex
phony.Inbox
*iwe.PacketConn
ctx context.Context
cancel context.CancelFunc
secret ed25519.PrivateKey
public ed25519.PublicKey
links links
proto protoHandler
log Logger
addPeerTimer *time.Timer
config struct {
tls *tls.Config // immutable after startup
//_peers map[Peer]*linkInfo // configurable after startup
_listeners map[ListenAddress]struct{} // configurable after startup
peerFilter func(ip net.IP) bool // immutable after startup
nodeinfo NodeInfo // immutable after startup
nodeinfoPrivacy NodeInfoPrivacy // immutable after startup
_allowedPublicKeys map[[32]byte]struct{} // configurable after startup
}
pathNotify func(ed25519.PublicKey)
}
func New(cert *tls.Certificate, logger Logger, opts ...SetupOption) (*Core, error) {
c := &Core{
log: logger,
}
c.ctx, c.cancel = context.WithCancel(context.Background())
if c.log == nil {
c.log = log.New(io.Discard, "", 0)
}
if name := version.BuildName(); name != "unknown" {
c.log.Infoln("Build name:", name)
}
if version := version.BuildVersion(); version != "unknown" {
c.log.Infoln("Build version:", version)
}
var err error
c.config._listeners = map[ListenAddress]struct{}{}
c.config._allowedPublicKeys = map[[32]byte]struct{}{}
for _, opt := range opts {
switch opt.(type) {
case Peer, ListenAddress:
// We can't do peers yet as the links aren't set up.
continue
default:
if err = c._applyOption(opt); err != nil {
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
}
}
}
if cert == nil || cert.PrivateKey == nil {
return nil, fmt.Errorf("no private key supplied")
}
var ok bool
if c.secret, ok = cert.PrivateKey.(ed25519.PrivateKey); !ok {
return nil, fmt.Errorf("private key must be ed25519")
}
if len(c.secret) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("private key is incorrect length")
}
c.public = c.secret.Public().(ed25519.PublicKey)
if c.config.tls, err = c.generateTLSConfig(cert); err != nil {
return nil, fmt.Errorf("error generating TLS config: %w", err)
}
keyXform := func(key ed25519.PublicKey) ed25519.PublicKey {
return address.SubnetForKey(key).GetKey()
}
if c.PacketConn, err = iwe.NewPacketConn(
c.secret,
iwn.WithBloomTransform(keyXform),
iwn.WithPeerMaxMessageSize(65535*2),
iwn.WithPathNotify(c.doPathNotify),
); err != nil {
return nil, fmt.Errorf("error creating encryption: %w", err)
}
c.proto.init(c)
if err := c.links.init(c); err != nil {
return nil, fmt.Errorf("error initialising links: %w", err)
}
for _, opt := range opts {
switch opt.(type) {
case Peer, ListenAddress:
// Now do the peers and listeners.
if err = c._applyOption(opt); err != nil {
return nil, fmt.Errorf("failed to apply configuration option %T: %w", opt, err)
}
default:
continue
}
}
if err := c.proto.nodeinfo.setNodeInfo(c.config.nodeinfo, bool(c.config.nodeinfoPrivacy)); err != nil {
return nil, fmt.Errorf("error setting node info: %w", err)
}
for listenaddr := range c.config._listeners {
u, err := url.Parse(string(listenaddr))
if err != nil {
c.log.Errorf("Invalid listener URI %q specified, ignoring\n", listenaddr)
continue
}
if _, err = c.links.listen(u, "", false); err != nil {
c.log.Errorf("Failed to start listener %q: %s\n", listenaddr, err)
}
}
return c, nil
}
func (c *Core) RetryPeersNow() {
phony.Block(&c.links, func() {
for _, l := range c.links._links {
select {
case l.kick <- struct{}{}:
default:
}
}
})
}
// Stop shuts down the Yggdrasil node.
func (c *Core) Stop() {
phony.Block(c, func() {
c.log.Infoln("Stopping...")
_ = c._close()
c.log.Infoln("Stopped")
})
}
// This function is unsafe and should only be ran by the core actor.
func (c *Core) _close() error {
c.cancel()
c.links.shutdown()
err := c.PacketConn.Close()
if c.addPeerTimer != nil {
c.addPeerTimer.Stop()
c.addPeerTimer = nil
}
return err
}
func (c *Core) MTU() uint64 {
const sessionTypeOverhead = 1
MTU := c.PacketConn.MTU() - sessionTypeOverhead
if MTU > 65535 {
MTU = 65535
}
return MTU
}
func (c *Core) ReadFrom(p []byte) (n int, from net.Addr, err error) {
buf := allocBytes(int(c.PacketConn.MTU()))
defer freeBytes(buf)
for {
bs := buf
n, from, err = c.PacketConn.ReadFrom(bs)
if err != nil {
return 0, from, err
}
if n == 0 {
continue
}
switch bs[0] {
case typeSessionTraffic:
// This is what we want to handle here
case typeSessionProto:
var key keyArray
copy(key[:], from.(iwt.Addr))
data := append([]byte(nil), bs[1:n]...)
c.proto.handleProto(nil, key, data)
continue
default:
continue
}
bs = bs[1:n]
copy(p, bs)
if len(p) < len(bs) {
n = len(p)
} else {
n = len(bs)
}
return
}
}
func (c *Core) WriteTo(p []byte, addr net.Addr) (n int, err error) {
buf := allocBytes(0)
defer func() { freeBytes(buf) }()
buf = append(buf, typeSessionTraffic)
buf = append(buf, p...)
n, err = c.PacketConn.WriteTo(buf, addr)
if n > 0 {
n -= 1
}
return
}
func (c *Core) doPathNotify(key ed25519.PublicKey) {
c.Act(nil, func() {
if c.pathNotify != nil {
c.pathNotify(key)
}
})
}
func (c *Core) SetPathNotify(notify func(ed25519.PublicKey)) {
c.Act(nil, func() {
c.pathNotify = notify
})
}
type Logger interface {
Printf(string, ...interface{})
Println(...interface{})
Infof(string, ...interface{})
Infoln(...interface{})
Warnf(string, ...interface{})
Warnln(...interface{})
Errorf(string, ...interface{})
Errorln(...interface{})
Debugf(string, ...interface{})
Debugln(...interface{})
Traceln(...interface{})
}

View file

@ -1,290 +0,0 @@
package core
import (
"bytes"
"crypto/rand"
"net/url"
"os"
"testing"
"time"
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
)
// GetLoggerWithPrefix creates a new logger instance with prefix.
// If verbose is set to true, three log levels are enabled: "info", "warn", "error".
func GetLoggerWithPrefix(prefix string, verbose bool) *log.Logger {
l := log.New(os.Stderr, prefix, log.Flags())
if !verbose {
return l
}
l.EnableLevel("info")
l.EnableLevel("warn")
l.EnableLevel("error")
return l
}
func require_NoError(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}
func require_Equal[T comparable](t *testing.T, a, b T) {
t.Helper()
if a != b {
t.Fatalf("%v != %v", a, b)
}
}
func require_True(t *testing.T, a bool) {
t.Helper()
if !a {
t.Fatal("expected true")
}
}
// CreateAndConnectTwo creates two nodes. nodeB connects to nodeA.
// Verbosity flag is passed to logger.
func CreateAndConnectTwo(t testing.TB, verbose bool) (nodeA *Core, nodeB *Core) {
var err error
cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig()
if err = cfgA.GenerateSelfSignedCertificate(); err != nil {
t.Fatal(err)
}
if err = cfgB.GenerateSelfSignedCertificate(); err != nil {
t.Fatal(err)
}
logger := GetLoggerWithPrefix("", false)
logger.EnableLevel("debug")
if nodeA, err = New(cfgA.Certificate, logger); err != nil {
t.Fatal(err)
}
if nodeB, err = New(cfgB.Certificate, logger); err != nil {
t.Fatal(err)
}
nodeAListenURL, err := url.Parse("tcp://localhost:0")
if err != nil {
t.Fatal(err)
}
nodeAListener, err := nodeA.Listen(nodeAListenURL, "")
if err != nil {
t.Fatal(err)
}
nodeAURL, err := url.Parse("tcp://" + nodeAListener.Addr().String())
if err != nil {
t.Fatal(err)
}
if err = nodeB.CallPeer(nodeAURL, ""); err != nil {
t.Fatal(err)
}
time.Sleep(100 * time.Millisecond)
if l := len(nodeA.GetPeers()); l != 1 {
t.Fatal("unexpected number of peers", l)
}
if l := len(nodeB.GetPeers()); l != 1 {
t.Fatal("unexpected number of peers", l)
}
return nodeA, nodeB
}
// WaitConnected blocks until either nodes negotiated DHT or 5 seconds passed.
func WaitConnected(nodeA, nodeB *Core) bool {
// It may take up to 3 seconds, but let's wait 5.
for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond)
/*
if len(nodeA.GetPeers()) > 0 && len(nodeB.GetPeers()) > 0 {
return true
}
*/
if len(nodeA.GetTree()) > 1 && len(nodeB.GetTree()) > 1 {
time.Sleep(3 * time.Second) // FIXME hack, there's still stuff happening internally
return true
}
}
return false
}
// CreateEchoListener creates a routine listening on nodeA. It expects repeats messages of length bufLen.
// It returns a channel used to synchronize the routine with caller.
func CreateEchoListener(t testing.TB, nodeA *Core, bufLen int, repeats int) chan struct{} {
// Start routine
done := make(chan struct{})
go func() {
buf := make([]byte, bufLen)
res := make([]byte, bufLen)
for i := 0; i < repeats; i++ {
n, from, err := nodeA.ReadFrom(buf)
if err != nil {
t.Error(err)
return
}
if n != bufLen {
t.Error("missing data")
return
}
copy(res, buf)
copy(res[8:24], buf[24:40])
copy(res[24:40], buf[8:24])
_, err = nodeA.WriteTo(res, from)
if err != nil {
t.Error(err)
}
}
done <- struct{}{}
}()
return done
}
// TestCore_Start_Connect checks if two nodes can connect together.
func TestCore_Start_Connect(t *testing.T) {
CreateAndConnectTwo(t, true)
}
// TestCore_Start_Transfer checks that messages can be passed between nodes (in both directions).
func TestCore_Start_Transfer(t *testing.T) {
nodeA, nodeB := CreateAndConnectTwo(t, true)
defer nodeA.Stop()
defer nodeB.Stop()
msgLen := 1500
done := CreateEchoListener(t, nodeA, msgLen, 1)
if !WaitConnected(nodeA, nodeB) {
t.Fatal("nodes did not connect")
}
// Send
msg := make([]byte, msgLen)
_, _ = rand.Read(msg[40:])
msg[0] = 0x60
copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address())
_, err := nodeB.WriteTo(msg, nodeA.LocalAddr())
if err != nil {
t.Fatal(err)
}
buf := make([]byte, msgLen)
_, _, err = nodeB.ReadFrom(buf)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(msg[40:], buf[40:]) {
t.Fatal("expected echo")
}
<-done
}
// BenchmarkCore_Start_Transfer estimates the possible transfer between nodes (in MB/s).
func BenchmarkCore_Start_Transfer(b *testing.B) {
nodeA, nodeB := CreateAndConnectTwo(b, false)
msgLen := 1500 // typical MTU
done := CreateEchoListener(b, nodeA, msgLen, b.N)
if !WaitConnected(nodeA, nodeB) {
b.Fatal("nodes did not connect")
}
// Send
msg := make([]byte, msgLen)
_, _ = rand.Read(msg[40:])
msg[0] = 0x60
copy(msg[8:24], nodeB.Address())
copy(msg[24:40], nodeA.Address())
buf := make([]byte, msgLen)
b.SetBytes(int64(msgLen))
b.ResetTimer()
addr := nodeA.LocalAddr()
for i := 0; i < b.N; i++ {
_, err := nodeB.WriteTo(msg, addr)
if err != nil {
b.Fatal(err)
}
_, _, err = nodeB.ReadFrom(buf)
if err != nil {
b.Fatal(err)
}
}
<-done
}
func TestAllowedPublicKeys(t *testing.T) {
logger := GetLoggerWithPrefix("", false)
cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig()
require_NoError(t, cfgA.GenerateSelfSignedCertificate())
require_NoError(t, cfgB.GenerateSelfSignedCertificate())
nodeA, err := New(cfgA.Certificate, logger, AllowedPublicKey("abcdef"))
require_NoError(t, err)
defer nodeA.Stop()
nodeB, err := New(cfgB.Certificate, logger)
require_NoError(t, err)
defer nodeB.Stop()
u, err := url.Parse("tcp://localhost:0")
require_NoError(t, err)
l, err := nodeA.Listen(u, "")
require_NoError(t, err)
u, err = url.Parse("tcp://" + l.Addr().String())
require_NoError(t, err)
require_NoError(t, nodeB.AddPeer(u, ""))
time.Sleep(time.Second)
peers := nodeB.GetPeers()
require_Equal(t, len(peers), 1)
require_True(t, !peers[0].Up)
require_True(t, peers[0].LastError != nil)
}
func TestAllowedPublicKeysLocal(t *testing.T) {
logger := GetLoggerWithPrefix("", false)
cfgA, cfgB := config.GenerateConfig(), config.GenerateConfig()
require_NoError(t, cfgA.GenerateSelfSignedCertificate())
require_NoError(t, cfgB.GenerateSelfSignedCertificate())
nodeA, err := New(cfgA.Certificate, logger, AllowedPublicKey("abcdef"))
require_NoError(t, err)
defer nodeA.Stop()
nodeB, err := New(cfgB.Certificate, logger)
require_NoError(t, err)
defer nodeB.Stop()
u, err := url.Parse("tcp://localhost:0")
require_NoError(t, err)
l, err := nodeA.ListenLocal(u, "")
require_NoError(t, err)
u, err = url.Parse("tcp://" + l.Addr().String())
require_NoError(t, err)
require_NoError(t, nodeB.AddPeer(u, ""))
time.Sleep(time.Second)
peers := nodeB.GetPeers()
require_Equal(t, len(peers), 1)
require_True(t, peers[0].Up)
require_True(t, peers[0].LastError == nil)
}

View file

@ -1,19 +0,0 @@
package core
import (
"fmt"
"net/http"
_ "net/http/pprof"
"os"
)
// Start the profiler if the required environment variable is set.
func init() {
envVarName := "PPROFLISTEN"
if hostPort := os.Getenv(envVarName); hostPort != "" {
fmt.Fprintf(os.Stderr, "DEBUG: Starting pprof on %s\n", hostPort)
go func() {
fmt.Fprintf(os.Stderr, "DEBUG: %s", http.ListenAndServe(hostPort, nil))
}()
}
}

View file

@ -1,767 +0,0 @@
package core
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"io"
"net"
"net/url"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"golang.org/x/crypto/blake2b"
)
type linkType int
const (
linkTypePersistent linkType = iota // Statically configured
linkTypeEphemeral // Multicast discovered
linkTypeIncoming // Incoming connection
)
const defaultBackoffLimit = time.Second << 12 // 1h8m16s
const minimumBackoffLimit = time.Second * 30
type links struct {
phony.Inbox
core *Core
tcp *linkTCP // TCP interface support
tls *linkTLS // TLS interface support
unix *linkUNIX // UNIX interface support
socks *linkSOCKS // SOCKS interface support
quic *linkQUIC // QUIC interface support
ws *linkWS // WS interface support
wss *linkWSS // WSS interface support
// _links can only be modified safely from within the links actor
_links map[linkInfo]*link // *link is nil if connection in progress
_listeners map[*Listener]context.CancelFunc
}
type linkProtocol interface {
dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error)
listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error)
}
// linkInfo is used as a map key
type linkInfo struct {
uri string // Peering URI in complete form
sintf string // Peering source interface (i.e. from InterfacePeers)
}
// link tracks the state of a connection, either persistent or non-persistent
type link struct {
ctx context.Context // Connection context
cancel context.CancelFunc // Stop future redial attempts (when peer removed)
kick chan struct{} // Attempt to reconnect now, if backing off
linkType linkType // Type of link, i.e. outbound/inbound, persistent/ephemeral
linkProto string // Protocol carrier of link, e.g. TCP, AWDL
// The remaining fields can only be modified safely from within the links actor
_conn *linkConn // Connected link, if any, nil if not connected
_err error // Last error on the connection, if any
_errtime time.Time // Last time an error occurred
}
type linkOptions struct {
pinnedEd25519Keys map[keyArray]struct{}
priority uint8
tlsSNI string
password []byte
maxBackoff time.Duration
}
type Listener struct {
listener net.Listener
ctx context.Context
Cancel context.CancelFunc
}
func (l *Listener) Addr() net.Addr {
return l.listener.Addr()
}
func (l *links) init(c *Core) error {
l.core = c
l.tcp = l.newLinkTCP()
l.tls = l.newLinkTLS(l.tcp)
l.unix = l.newLinkUNIX()
l.socks = l.newLinkSOCKS()
l.quic = l.newLinkQUIC()
l.ws = l.newLinkWS()
l.wss = l.newLinkWSS()
l._links = make(map[linkInfo]*link)
l._listeners = make(map[*Listener]context.CancelFunc)
l.Act(nil, l._updateAverages)
return nil
}
func (l *links) _updateAverages() {
select {
case <-l.core.ctx.Done():
return
default:
}
for _, l := range l._links {
if l._conn == nil {
continue
}
rx := atomic.LoadUint64(&l._conn.rx)
tx := atomic.LoadUint64(&l._conn.tx)
lastrx := atomic.LoadUint64(&l._conn.lastrx)
lasttx := atomic.LoadUint64(&l._conn.lasttx)
atomic.StoreUint64(&l._conn.rxrate, rx-lastrx)
atomic.StoreUint64(&l._conn.txrate, tx-lasttx)
atomic.StoreUint64(&l._conn.lastrx, rx)
atomic.StoreUint64(&l._conn.lasttx, tx)
}
time.AfterFunc(time.Second, func() {
l.Act(nil, l._updateAverages)
})
}
func (l *links) shutdown() {
phony.Block(l, func() {
for _, cancel := range l._listeners {
cancel()
}
for _, link := range l._links {
if link._conn != nil {
_ = link._conn.Close()
}
}
})
}
type linkError string
func (e linkError) Error() string { return string(e) }
const ErrLinkAlreadyConfigured = linkError("peer is already configured")
const ErrLinkNotConfigured = linkError("peer is not configured")
const ErrLinkPriorityInvalid = linkError("priority value is invalid")
const ErrLinkPinnedKeyInvalid = linkError("pinned public key is invalid")
const ErrLinkPasswordInvalid = linkError("invalid password supplied")
const ErrLinkUnrecognisedSchema = linkError("link schema unknown")
const ErrLinkMaxBackoffInvalid = linkError("max backoff duration invalid")
const ErrLinkSNINotSupported = linkError("SNI not supported on this link type")
const ErrLinkNoSuitableIPs = linkError("peer has no suitable addresses")
func (l *links) add(u *url.URL, sintf string, linkType linkType) error {
var retErr error
phony.Block(l, func() {
// Generate the link info and see whether we think we already
// have an open peering to this peer.
lu := urlForLinkInfo(*u)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
}
// Collect together the link options, these are global options
// that are not specific to any given protocol.
options := linkOptions{
maxBackoff: defaultBackoffLimit,
}
for _, pubkey := range u.Query()["key"] {
sigPub, err := hex.DecodeString(pubkey)
if err != nil {
retErr = ErrLinkPinnedKeyInvalid
return
}
var sigPubKey keyArray
copy(sigPubKey[:], sigPub)
if options.pinnedEd25519Keys == nil {
options.pinnedEd25519Keys = map[keyArray]struct{}{}
}
options.pinnedEd25519Keys[sigPubKey] = struct{}{}
}
if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8)
if err != nil {
retErr = ErrLinkPriorityInvalid
return
}
options.priority = uint8(pi)
}
if p := u.Query().Get("password"); p != "" {
if len(p) > blake2b.Size {
retErr = ErrLinkPasswordInvalid
return
}
options.password = []byte(p)
}
if p := u.Query().Get("maxbackoff"); p != "" {
d, err := time.ParseDuration(p)
if err != nil || d < minimumBackoffLimit {
retErr = ErrLinkMaxBackoffInvalid
return
}
options.maxBackoff = d
}
// SNI headers must contain hostnames and not IP addresses, so we must make sure
// that we do not populate the SNI with an IP literal. We do this by splitting
// the host-port combo from the query option and then seeing if it parses to an
// IP address successfully or not.
if sni := u.Query().Get("sni"); sni != "" {
if net.ParseIP(sni) == nil {
options.tlsSNI = sni
}
}
// If the SNI is not configured still because the above failed then we'll try
// again but this time we'll use the host part of the peering URI instead.
if options.tlsSNI == "" {
if host, _, err := net.SplitHostPort(u.Host); err == nil && net.ParseIP(host) == nil {
options.tlsSNI = host
}
}
// If we think we're already connected to this peer, load up
// the existing peer state. Try to kick the peer if possible,
// which will cause an immediate connection attempt if it is
// backing off for some reason.
state, ok := l._links[info]
if ok && state != nil {
select {
case state.kick <- struct{}{}:
default:
}
retErr = ErrLinkAlreadyConfigured
return
}
// Create the link entry. This will contain the connection
// in progress (if any), any error details and a context that
// lets the link be cancelled later.
state = &link{
linkType: linkType,
linkProto: strings.ToUpper(u.Scheme),
kick: make(chan struct{}),
}
state.ctx, state.cancel = context.WithCancel(l.core.ctx)
// Store the state of the link so that it can be queried later.
l._links[info] = state
// Track how many consecutive connection failures we have had,
// as we will back off exponentially rather than hammering the
// remote node endlessly.
var backoff int
// backoffNow is called when there's a connection error. It
// will wait for the specified amount of time and then return
// true, unless the peering context was cancelled (due to a
// peer removal most likely), in which case it returns false.
// The caller should check the return value to decide whether
// or not to give up trying.
backoffNow := func() bool {
if backoff < 32 {
backoff++
}
duration := time.Second << backoff
if duration > options.maxBackoff {
duration = options.maxBackoff
}
select {
case <-state.kick:
return true
case <-state.ctx.Done():
return false
case <-l.core.ctx.Done():
return false
case <-time.After(duration):
return true
}
}
// resetBackoff is called by the connection handler when the
// handshake has successfully completed.
resetBackoff := func() {
backoff = 0
}
// The goroutine is responsible for attempting the connection
// and then running the handler. If the connection is persistent
// then the loop will run endlessly, using backoffs as needed.
// Otherwise the loop will end, cleaning up the link entry.
go func() {
defer phony.Block(l, func() {
if l._links[info] == state {
delete(l._links, info)
}
})
// This loop will run each and every time we want to attempt
// a connection to this peer.
// TODO get rid of this loop, this is *exactly* what time.AfterFunc is for, we should just send a signal to the links actor to kick off a goroutine as needed
for {
select {
case <-state.ctx.Done():
// The peering context has been cancelled, so don't try
// to dial again.
return
default:
}
conn, err := l.connect(state.ctx, u, info, options)
if err != nil || conn == nil {
if err == nil && conn == nil {
l.core.log.Warnf("Link %q reached inconsistent error state", u.String())
}
if linkType == linkTypePersistent {
// If the link is a persistent configured peering,
// store information about the connection error so
// that we can report it through the admin socket.
phony.Block(l, func() {
state._conn = nil
state._err = err
state._errtime = time.Now()
})
// Back off for a bit. If true is returned here, we
// can continue onto the next loop iteration to try
// the next connection.
if backoffNow() {
continue
}
return
}
// Ephemeral and incoming connections don't remain
// after a connection failure, so exit out of the
// loop and clean up the link entry.
break
}
// The linkConn wrapper allows us to track the number of
// bytes written to and read from this connection without
// the help of ironwood.
lc := &linkConn{
Conn: conn,
up: time.Now(),
}
// Update the link state with our newly wrapped connection.
// Clear the error state.
var doRet bool
phony.Block(l, func() {
if state._conn != nil {
// If a peering has come up in this time, abort this one.
doRet = true
}
state._conn = lc
})
if doRet {
return
}
// Give the connection to the handler. The handler will block
// for the lifetime of the connection.
switch err = l.handler(linkType, options, lc, resetBackoff, false); {
case err == nil:
case errors.Is(err, io.EOF):
case errors.Is(err, net.ErrClosed):
default:
l.core.log.Debugf("Link %s error: %s\n", u.Host, err)
}
// The handler has stopped running so the connection is dead,
// try to close the underlying socket just in case and then
// update the link state.
_ = lc.Close()
phony.Block(l, func() {
state._conn = nil
if err == nil {
err = fmt.Errorf("remote side closed the connection")
}
state._err = err
state._errtime = time.Now()
})
// If the link is persistently configured, back off if needed
// and then try reconnecting. Otherwise, exit out.
if linkType == linkTypePersistent {
if backoffNow() {
continue
}
}
// Ephemeral or incoming connections don't reconnect.
return
}
}()
})
return retErr
}
func (l *links) remove(u *url.URL, sintf string, _ linkType) error {
var retErr error
phony.Block(l, func() {
// Generate the link info and see whether we think we already
// have an open peering to this peer.
lu := urlForLinkInfo(*u)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
}
// If this peer is already configured then we will close the
// connection and stop it from retrying.
state, ok := l._links[info]
if ok && state != nil {
state.cancel()
if conn := state._conn; conn != nil {
retErr = conn.Close()
}
return
}
retErr = ErrLinkNotConfigured
})
return retErr
}
func (l *links) listen(u *url.URL, sintf string, local bool) (*Listener, error) {
ctx, ctxcancel := context.WithCancel(l.core.ctx)
var protocol linkProtocol
switch strings.ToLower(u.Scheme) {
case "tcp":
protocol = l.tcp
case "tls":
protocol = l.tls
case "unix":
protocol = l.unix
case "quic":
protocol = l.quic
case "ws":
protocol = l.ws
case "wss":
protocol = l.wss
default:
ctxcancel()
return nil, ErrLinkUnrecognisedSchema
}
listener, err := protocol.listen(ctx, u, sintf)
if err != nil {
ctxcancel()
return nil, err
}
addr := listener.Addr()
cancel := func() {
ctxcancel()
if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
l.core.log.Warnf("Error closing %s listener %s: %s", strings.ToUpper(u.Scheme), addr, err)
}
}
li := &Listener{
listener: listener,
ctx: ctx,
Cancel: cancel,
}
var options linkOptions
if p := u.Query().Get("priority"); p != "" {
pi, err := strconv.ParseUint(p, 10, 8)
if err != nil {
return nil, ErrLinkPriorityInvalid
}
options.priority = uint8(pi)
}
if p := u.Query().Get("password"); p != "" {
if len(p) > blake2b.Size {
return nil, ErrLinkPasswordInvalid
}
options.password = []byte(p)
}
phony.Block(l, func() {
l._listeners[li] = cancel
})
go func() {
l.core.log.Infof("%s listener started on %s", strings.ToUpper(u.Scheme), addr)
defer phony.Block(l, func() {
cancel()
delete(l._listeners, li)
l.core.log.Infof("%s listener stopped on %s", strings.ToUpper(u.Scheme), addr)
})
for {
conn, err := li.listener.Accept()
if err != nil {
return
}
go func(conn net.Conn) {
defer conn.Close()
// In order to populate a somewhat sane looking connection
// URI in the admin socket, we need to replace the host in
// the listener URL with the remote address.
pu := *u
pu.Host = conn.RemoteAddr().String()
lu := urlForLinkInfo(pu)
info := linkInfo{
uri: lu.String(),
sintf: sintf,
}
// If there's an existing link state for this link, get it.
// If this node is already connected to us, just drop the
// connection. This prevents duplicate peerings.
var lc *linkConn
var state *link
phony.Block(l, func() {
var ok bool
state, ok = l._links[info]
if !ok || state == nil {
state = &link{
linkType: linkTypeIncoming,
linkProto: strings.ToUpper(u.Scheme),
kick: make(chan struct{}),
}
}
if state._conn != nil {
// If a connection has come up in this time, abort
// this one.
return
}
// The linkConn wrapper allows us to track the number of
// bytes written to and read from this connection without
// the help of ironwood.
lc = &linkConn{
Conn: conn,
up: time.Now(),
}
// Update the link state with our newly wrapped connection.
// Clear the error state.
state._conn = lc
state._err = nil
state._errtime = time.Time{}
// Store the state of the link so that it can be queried later.
l._links[info] = state
})
defer phony.Block(l, func() {
if l._links[info] == state {
delete(l._links, info)
}
})
if lc == nil {
return
}
// Give the connection to the handler. The handler will block
// for the lifetime of the connection.
switch err = l.handler(linkTypeIncoming, options, lc, nil, local); {
case err == nil:
case errors.Is(err, io.EOF):
case errors.Is(err, net.ErrClosed):
default:
l.core.log.Debugf("Link %s error: %s\n", u.Host, err)
}
// The handler has stopped running so the connection is dead,
// try to close the underlying socket just in case and then
// drop the link state.
_ = lc.Close()
}(conn)
}
}()
return li, nil
}
func (l *links) connect(ctx context.Context, u *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
var dialer linkProtocol
switch strings.ToLower(u.Scheme) {
case "tcp":
dialer = l.tcp
case "tls":
dialer = l.tls
case "socks", "sockstls":
dialer = l.socks
case "unix":
dialer = l.unix
case "quic":
dialer = l.quic
case "ws":
dialer = l.ws
case "wss":
dialer = l.wss
default:
return nil, ErrLinkUnrecognisedSchema
}
return dialer.dial(ctx, u, info, options)
}
func (l *links) handler(linkType linkType, options linkOptions, conn net.Conn, success func(), local bool) error {
meta := version_getBaseMetadata()
meta.publicKey = l.core.public
meta.priority = options.priority
metaBytes, err := meta.encode(l.core.secret, options.password)
if err != nil {
return fmt.Errorf("failed to generate handshake: %w", err)
}
if err := conn.SetDeadline(time.Now().Add(time.Second * 6)); err != nil {
return fmt.Errorf("failed to set handshake deadline: %w", err)
}
n, err := conn.Write(metaBytes)
switch {
case err != nil:
return fmt.Errorf("write handshake: %w", err)
case n != len(metaBytes):
return fmt.Errorf("incomplete handshake send")
}
meta = version_metadata{}
base := version_getBaseMetadata()
if err := meta.decode(conn, options.password); err != nil {
_ = conn.Close()
return err
}
if !meta.check() {
return fmt.Errorf("remote node incompatible version (local %s, remote %s)",
fmt.Sprintf("%d.%d", base.majorVer, base.minorVer),
fmt.Sprintf("%d.%d", meta.majorVer, meta.minorVer),
)
}
if err = conn.SetDeadline(time.Time{}); err != nil {
return fmt.Errorf("failed to clear handshake deadline: %w", err)
}
// Check if the remote side matches the keys we expected. This is a bit of a weak
// check - in future versions we really should check a signature or something like that.
if pinned := options.pinnedEd25519Keys; len(pinned) > 0 {
var key keyArray
copy(key[:], meta.publicKey)
if _, allowed := pinned[key]; !allowed {
return fmt.Errorf("node public key that does not match pinned keys")
}
}
// Check if we're authorized to connect to this key / IP
if !local {
var allowed map[[32]byte]struct{}
phony.Block(l.core, func() {
allowed = l.core.config._allowedPublicKeys
})
isallowed := len(allowed) == 0
for k := range allowed {
if bytes.Equal(k[:], meta.publicKey) {
isallowed = true
break
}
}
if linkType == linkTypeIncoming && !isallowed {
return fmt.Errorf("node public key %q is not in AllowedPublicKeys", hex.EncodeToString(meta.publicKey))
}
}
dir := "outbound"
if linkType == linkTypeIncoming {
dir = "inbound"
}
remoteAddr := net.IP(address.AddrForKey(meta.publicKey)[:]).String()
remoteStr := fmt.Sprintf("%s@%s", remoteAddr, conn.RemoteAddr())
localStr := conn.LocalAddr()
priority := options.priority
if meta.priority > priority {
priority = meta.priority
}
l.core.log.Infof("Connected %s: %s, source %s",
dir, remoteStr, localStr)
if success != nil {
success()
}
err = l.core.HandleConn(meta.publicKey, conn, priority)
switch err {
case io.EOF, net.ErrClosed, nil:
l.core.log.Infof("Disconnected %s: %s, source %s",
dir, remoteStr, localStr)
default:
l.core.log.Infof("Disconnected %s: %s, source %s; error: %s",
dir, remoteStr, localStr, err)
}
return err
}
func (l *links) findSuitableIP(url *url.URL, fn func(hostname string, ip net.IP, port int) (net.Conn, error)) (net.Conn, error) {
host, p, err := net.SplitHostPort(url.Host)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(p)
if err != nil {
return nil, err
}
resp, err := net.LookupIP(host)
if err != nil {
return nil, err
}
var _ips [64]net.IP
ips := _ips[:0]
for _, ip := range resp {
switch {
case ip.IsUnspecified():
continue
case ip.IsMulticast():
continue
case ip.IsLinkLocalMulticast():
continue
case ip.IsInterfaceLocalMulticast():
continue
case l.core.config.peerFilter != nil && !l.core.config.peerFilter(ip):
continue
}
ips = append(ips, ip)
}
if len(ips) == 0 {
return nil, ErrLinkNoSuitableIPs
}
for _, ip := range ips {
var conn net.Conn
if conn, err = fn(host, ip, port); err != nil {
url := *url
url.RawQuery = ""
l.core.log.Debugln("Dialling", url.Redacted(), "reported error:", err)
continue
}
return conn, nil
}
return nil, err
}
func urlForLinkInfo(u url.URL) url.URL {
u.RawQuery = ""
return u
}
type linkConn struct {
// tx and rx are at the beginning of the struct to ensure 64-bit alignment
// on 32-bit platforms, see https://pkg.go.dev/sync/atomic#pkg-note-BUG
rx uint64
tx uint64
rxrate uint64
txrate uint64
lastrx uint64
lasttx uint64
up time.Time
net.Conn
}
func (c *linkConn) Read(p []byte) (n int, err error) {
n, err = c.Conn.Read(p)
atomic.AddUint64(&c.rx, uint64(n))
return
}
func (c *linkConn) Write(p []byte) (n int, err error) {
n, err = c.Conn.Write(p)
atomic.AddUint64(&c.tx, uint64(n))
return
}

View file

@ -1,108 +0,0 @@
package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/url"
"time"
"github.com/Arceliar/phony"
"github.com/quic-go/quic-go"
)
type linkQUIC struct {
phony.Inbox
*links
tlsconfig *tls.Config
quicconfig *quic.Config
}
type linkQUICStream struct {
quic.Connection
quic.Stream
}
type linkQUICListener struct {
*quic.Listener
ch <-chan *linkQUICStream
}
func (l *linkQUICListener) Accept() (net.Conn, error) {
qs := <-l.ch
if qs == nil {
return nil, context.Canceled
}
return qs, nil
}
func (l *links) newLinkQUIC() *linkQUIC {
lt := &linkQUIC{
links: l,
tlsconfig: l.core.config.tls.Clone(),
quicconfig: &quic.Config{
MaxIdleTimeout: time.Minute,
KeepAlivePeriod: time.Second * 20,
TokenStore: quic.NewLRUTokenStore(255, 255),
},
}
return lt
}
func (l *linkQUIC) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
tlsconfig := l.tlsconfig.Clone()
return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) {
tlsconfig.ServerName = hostname
tlsconfig.MinVersion = tls.VersionTLS12
tlsconfig.MaxVersion = tls.VersionTLS13
hostport := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port))
qc, err := quic.DialAddr(ctx, hostport, l.tlsconfig, l.quicconfig)
if err != nil {
return nil, err
}
qs, err := qc.OpenStreamSync(ctx)
if err != nil {
return nil, err
}
return &linkQUICStream{
Connection: qc,
Stream: qs,
}, nil
})
}
func (l *linkQUIC) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
ql, err := quic.ListenAddr(url.Host, l.tlsconfig, l.quicconfig)
if err != nil {
return nil, err
}
ch := make(chan *linkQUICStream)
lql := &linkQUICListener{
Listener: ql,
ch: ch,
}
go func() {
for {
qc, err := ql.Accept(ctx)
switch err {
case context.Canceled, context.DeadlineExceeded:
ql.Close()
fallthrough
case quic.ErrServerClosed:
return
case nil:
qs, err := qc.AcceptStream(ctx)
if err != nil {
_ = qc.CloseWithError(1, fmt.Sprintf("stream error: %s", err))
continue
}
ch <- &linkQUICStream{
Connection: qc,
Stream: qs,
}
}
}
}()
return lql, nil
}

View file

@ -1,67 +0,0 @@
package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/url"
"strings"
"golang.org/x/net/proxy"
)
type linkSOCKS struct {
*links
}
func (l *links) newLinkSOCKS() *linkSOCKS {
lt := &linkSOCKS{
links: l,
}
return lt
}
func (l *linkSOCKS) dial(_ context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
var proxyAuth *proxy.Auth
if url.User != nil && url.User.Username() != "" {
proxyAuth = &proxy.Auth{
User: url.User.Username(),
}
proxyAuth.Password, _ = url.User.Password()
}
tlsconfig := l.tls.config.Clone()
return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) {
hostport := net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port))
dialer, err := l.tcp.dialerFor(&net.TCPAddr{
IP: ip,
Port: port,
}, info.sintf)
if err != nil {
return nil, err
}
proxy, err := proxy.SOCKS5("tcp", hostport, proxyAuth, dialer)
if err != nil {
return nil, err
}
pathtokens := strings.Split(strings.Trim(url.Path, "/"), "/")
conn, err := proxy.Dial("tcp", pathtokens[0])
if err != nil {
return nil, err
}
if url.Scheme == "sockstls" {
tlsconfig.ServerName = hostname
tlsconfig.MinVersion = tls.VersionTLS12
tlsconfig.MaxVersion = tls.VersionTLS13
if sni := options.tlsSNI; sni != "" {
tlsconfig.ServerName = sni
}
conn = tls.Client(conn, tlsconfig)
}
return conn, nil
})
}
func (l *linkSOCKS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return nil, fmt.Errorf("SOCKS listener not supported")
}

View file

@ -1,111 +0,0 @@
package core
import (
"context"
"fmt"
"net"
"net/url"
"time"
"github.com/Arceliar/phony"
)
type linkTCP struct {
phony.Inbox
*links
listenconfig *net.ListenConfig
}
func (l *links) newLinkTCP() *linkTCP {
lt := &linkTCP{
links: l,
listenconfig: &net.ListenConfig{
KeepAlive: -1,
},
}
lt.listenconfig.Control = lt.tcpContext
return lt
}
func (l *linkTCP) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) {
addr := &net.TCPAddr{
IP: ip,
Port: port,
}
dialer, err := l.tcp.dialerFor(addr, info.sintf)
if err != nil {
return nil, err
}
return dialer.DialContext(ctx, "tcp", addr.String())
})
}
func (l *linkTCP) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
hostport := url.Host
if sintf != "" {
if host, port, err := net.SplitHostPort(hostport); err == nil {
hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port)
}
}
return l.listenconfig.Listen(ctx, "tcp", hostport)
}
func (l *linkTCP) dialerFor(dst *net.TCPAddr, sintf string) (*net.Dialer, error) {
if dst.IP.IsLinkLocalUnicast() {
if sintf != "" {
dst.Zone = sintf
}
if dst.Zone == "" {
return nil, fmt.Errorf("link-local address requires a zone")
}
}
dialer := &net.Dialer{
Timeout: time.Second * 5,
KeepAlive: -1,
Control: l.tcpContext,
}
if sintf != "" {
dialer.Control = l.getControl(sintf)
ief, err := net.InterfaceByName(sintf)
if err != nil {
return nil, fmt.Errorf("interface %q not found", sintf)
}
if ief.Flags&net.FlagUp == 0 {
return nil, fmt.Errorf("interface %q is not up", sintf)
}
addrs, err := ief.Addrs()
if err != nil {
return nil, fmt.Errorf("interface %q addresses not available: %w", sintf, err)
}
for addrindex, addr := range addrs {
src, _, err := net.ParseCIDR(addr.String())
if err != nil {
continue
}
if !src.IsGlobalUnicast() && !src.IsLinkLocalUnicast() {
continue
}
bothglobal := src.IsGlobalUnicast() == dst.IP.IsGlobalUnicast()
bothlinklocal := src.IsLinkLocalUnicast() == dst.IP.IsLinkLocalUnicast()
if !bothglobal && !bothlinklocal {
continue
}
if (src.To4() != nil) != (dst.IP.To4() != nil) {
continue
}
if bothglobal || bothlinklocal || addrindex == len(addrs)-1 {
dialer.LocalAddr = &net.TCPAddr{
IP: src,
Port: 0,
Zone: sintf,
}
break
}
}
if dialer.LocalAddr == nil {
return nil, fmt.Errorf("no suitable source address found on interface %q", sintf)
}
}
return dialer, nil
}

View file

@ -1,30 +0,0 @@
//go:build linux
// +build linux
package core
import (
"syscall"
"golang.org/x/sys/unix"
)
// WARNING: This context is used both by net.Dialer and net.Listen in tcp.go
func (t *linkTCP) tcpContext(network, address string, c syscall.RawConn) error {
return nil
}
func (t *linkTCP) getControl(sintf string) func(string, string, syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) error {
var err error
btd := func(fd uintptr) {
err = unix.BindToDevice(int(fd), sintf)
}
_ = c.Control(btd)
if err != nil {
t.links.core.log.Debugln("Failed to set SO_BINDTODEVICE:", sintf)
}
return t.tcpContext(network, address, c)
}
}

View file

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

View file

@ -1,72 +0,0 @@
package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/url"
"github.com/Arceliar/phony"
)
type linkTLS struct {
phony.Inbox
*links
tcp *linkTCP
listener *net.ListenConfig
config *tls.Config
}
func (l *links) newLinkTLS(tcp *linkTCP) *linkTLS {
lt := &linkTLS{
links: l,
tcp: tcp,
listener: &net.ListenConfig{
Control: tcp.tcpContext,
KeepAlive: -1,
},
config: l.core.config.tls.Clone(),
}
return lt
}
func (l *linkTLS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
tlsconfig := l.config.Clone()
return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) {
tlsconfig.ServerName = hostname
tlsconfig.MinVersion = tls.VersionTLS12
tlsconfig.MaxVersion = tls.VersionTLS13
if sni := options.tlsSNI; sni != "" {
tlsconfig.ServerName = sni
}
addr := &net.TCPAddr{
IP: ip,
Port: port,
}
dialer, err := l.tcp.dialerFor(addr, info.sintf)
if err != nil {
return nil, err
}
tlsdialer := &tls.Dialer{
NetDialer: dialer,
Config: tlsconfig,
}
return tlsdialer.DialContext(ctx, "tcp", addr.String())
})
}
func (l *linkTLS) listen(ctx context.Context, url *url.URL, sintf string) (net.Listener, error) {
hostport := url.Host
if sintf != "" {
if host, port, err := net.SplitHostPort(hostport); err == nil {
hostport = fmt.Sprintf("[%s%%%s]:%s", host, sintf, port)
}
}
listener, err := l.listener.Listen(ctx, "tcp", hostport)
if err != nil {
return nil, err
}
tlslistener := tls.NewListener(listener, l.config)
return tlslistener, nil
}

View file

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

View file

@ -1,148 +0,0 @@
package core
import (
"context"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/Arceliar/phony"
"github.com/coder/websocket"
)
type linkWS struct {
phony.Inbox
*links
listenconfig *net.ListenConfig
}
type linkWSConn struct {
net.Conn
}
type linkWSListener struct {
ch chan *linkWSConn
ctx context.Context
httpServer *http.Server
listener net.Listener
}
type wsServer struct {
ch chan *linkWSConn
ctx context.Context
}
func (l *linkWSListener) Accept() (net.Conn, error) {
qs := <-l.ch
if qs == nil {
return nil, context.Canceled
}
return qs, nil
}
func (l *linkWSListener) Addr() net.Addr {
return l.listener.Addr()
}
func (l *linkWSListener) Close() error {
if err := l.httpServer.Shutdown(l.ctx); err != nil {
return err
}
return l.listener.Close()
}
func (s *wsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/health" || r.URL.Path == "/healthz" {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
return
}
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{"ygg-ws"},
})
if err != nil {
return
}
if c.Subprotocol() != "ygg-ws" {
c.Close(websocket.StatusPolicyViolation, "client must speak the ygg-ws subprotocol")
return
}
s.ch <- &linkWSConn{
Conn: websocket.NetConn(s.ctx, c, websocket.MessageBinary),
}
}
func (l *links) newLinkWS() *linkWS {
lt := &linkWS{
links: l,
listenconfig: &net.ListenConfig{
KeepAlive: -1,
},
}
return lt
}
func (l *linkWS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) {
u := *url
u.Host = net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port))
addr := &net.TCPAddr{
IP: ip,
Port: port,
}
dialer, err := l.tcp.dialerFor(addr, info.sintf)
if err != nil {
return nil, err
}
wsconn, _, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
HTTPClient: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: dialer.Dial,
DialContext: dialer.DialContext,
},
},
Subprotocols: []string{"ygg-ws"},
Host: hostname,
})
if err != nil {
return nil, err
}
return &linkWSConn{
Conn: websocket.NetConn(ctx, wsconn, websocket.MessageBinary),
}, nil
})
}
func (l *linkWS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
nl, err := l.listenconfig.Listen(ctx, "tcp", url.Host)
if err != nil {
return nil, err
}
ch := make(chan *linkWSConn)
httpServer := &http.Server{
Handler: &wsServer{
ch: ch,
ctx: ctx,
},
BaseContext: func(_ net.Listener) context.Context { return ctx },
ReadTimeout: time.Second * 10,
WriteTimeout: time.Second * 10,
}
lwl := &linkWSListener{
ch: ch,
ctx: ctx,
httpServer: httpServer,
listener: nl,
}
go lwl.httpServer.Serve(nl) // nolint:errcheck
return lwl, nil
}

View file

@ -1,72 +0,0 @@
package core
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"github.com/Arceliar/phony"
"github.com/coder/websocket"
)
type linkWSS struct {
phony.Inbox
*links
tlsconfig *tls.Config
}
type linkWSSConn struct {
net.Conn
}
func (l *links) newLinkWSS() *linkWSS {
lwss := &linkWSS{
links: l,
tlsconfig: l.core.config.tls.Clone(),
}
return lwss
}
func (l *linkWSS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) {
tlsconfig := l.tlsconfig.Clone()
return l.links.findSuitableIP(url, func(hostname string, ip net.IP, port int) (net.Conn, error) {
tlsconfig.ServerName = hostname
tlsconfig.MinVersion = tls.VersionTLS12
tlsconfig.MaxVersion = tls.VersionTLS13
u := *url
u.Host = net.JoinHostPort(ip.String(), fmt.Sprintf("%d", port))
addr := &net.TCPAddr{
IP: ip,
Port: port,
}
dialer, err := l.tcp.dialerFor(addr, info.sintf)
if err != nil {
return nil, err
}
wsconn, _, err := websocket.Dial(ctx, u.String(), &websocket.DialOptions{
HTTPClient: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: dialer.Dial,
DialContext: dialer.DialContext,
TLSClientConfig: tlsconfig,
},
},
Subprotocols: []string{"ygg-ws"},
Host: hostname,
})
if err != nil {
return nil, err
}
return &linkWSSConn{
Conn: websocket.NetConn(ctx, wsconn, websocket.MessageBinary),
}, nil
})
}
func (l *linkWSS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) {
return nil, fmt.Errorf("WSS listener not supported, use WS listener behind reverse proxy instead")
}

View file

@ -1,173 +0,0 @@
package core
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"runtime"
"time"
iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/version"
)
type nodeinfo struct {
phony.Inbox
proto *protoHandler
myNodeInfo json.RawMessage
callbacks map[keyArray]nodeinfoCallback
}
type nodeinfoCallback struct {
call func(nodeinfo json.RawMessage)
created time.Time
}
// Initialises the nodeinfo cache/callback maps, and starts a goroutine to keep
// the cache/callback maps clean of stale entries
func (m *nodeinfo) init(proto *protoHandler) {
m.Act(nil, func() {
m._init(proto)
})
}
func (m *nodeinfo) _init(proto *protoHandler) {
m.proto = proto
m.callbacks = make(map[keyArray]nodeinfoCallback)
m._cleanup()
}
func (m *nodeinfo) _cleanup() {
for boxPubKey, callback := range m.callbacks {
if time.Since(callback.created) > time.Minute {
delete(m.callbacks, boxPubKey)
}
}
time.AfterFunc(time.Second*30, func() {
m.Act(nil, m._cleanup)
})
}
func (m *nodeinfo) _addCallback(sender keyArray, call func(nodeinfo json.RawMessage)) {
m.callbacks[sender] = nodeinfoCallback{
created: time.Now(),
call: call,
}
}
// Handles the callback, if there is one
func (m *nodeinfo) _callback(sender keyArray, nodeinfo json.RawMessage) {
if callback, ok := m.callbacks[sender]; ok {
callback.call(nodeinfo)
delete(m.callbacks, sender)
}
}
func (m *nodeinfo) _getNodeInfo() json.RawMessage {
return m.myNodeInfo
}
// Set the current node's nodeinfo
func (m *nodeinfo) setNodeInfo(given map[string]interface{}, privacy bool) (err error) {
phony.Block(m, func() {
err = m._setNodeInfo(given, privacy)
})
return
}
func (m *nodeinfo) _setNodeInfo(given map[string]interface{}, privacy bool) error {
newnodeinfo := make(map[string]interface{}, len(given))
for k, v := range given {
newnodeinfo[k] = v
}
if !privacy {
newnodeinfo["buildname"] = version.BuildName()
newnodeinfo["buildversion"] = version.BuildVersion()
newnodeinfo["buildplatform"] = runtime.GOOS
newnodeinfo["buildarch"] = runtime.GOARCH
}
newjson, err := json.Marshal(newnodeinfo)
switch {
case err != nil:
return fmt.Errorf("NodeInfo marshalling failed: %w", err)
case len(newjson) > 16384:
return fmt.Errorf("NodeInfo exceeds max length of 16384 bytes")
default:
m.myNodeInfo = newjson
return nil
}
}
func (m *nodeinfo) sendReq(from phony.Actor, key keyArray, callback func(nodeinfo json.RawMessage)) {
m.Act(from, func() {
m._sendReq(key, callback)
})
}
func (m *nodeinfo) _sendReq(key keyArray, callback func(nodeinfo json.RawMessage)) {
if callback != nil {
m._addCallback(key, callback)
}
_, _ = m.proto.core.PacketConn.WriteTo([]byte{typeSessionProto, typeProtoNodeInfoRequest}, iwt.Addr(key[:]))
}
func (m *nodeinfo) handleReq(from phony.Actor, key keyArray) {
m.Act(from, func() {
m._sendRes(key)
})
}
func (m *nodeinfo) handleRes(from phony.Actor, key keyArray, info json.RawMessage) {
m.Act(from, func() {
m._callback(key, info)
})
}
func (m *nodeinfo) _sendRes(key keyArray) {
bs := append([]byte{typeSessionProto, typeProtoNodeInfoResponse}, m._getNodeInfo()...)
_, _ = m.proto.core.PacketConn.WriteTo(bs, iwt.Addr(key[:]))
}
// Admin socket stuff
type GetNodeInfoRequest struct {
Key string `json:"key"`
}
type GetNodeInfoResponse map[string]json.RawMessage
func (m *nodeinfo) nodeInfoAdminHandler(in json.RawMessage) (interface{}, error) {
var req GetNodeInfoRequest
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if req.Key == "" {
return nil, fmt.Errorf("No remote public key supplied")
}
var key keyArray
var kbs []byte
var err error
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, fmt.Errorf("Failed to decode public key: %w", err)
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
m.sendReq(nil, key, func(info json.RawMessage) {
ch <- info
})
timer := time.NewTimer(6 * time.Second)
defer timer.Stop()
select {
case <-timer.C:
return nil, errors.New("Timed out waiting for response")
case info := <-ch:
var msg json.RawMessage
if err := msg.UnmarshalJSON(info); err != nil {
return nil, err
}
key := hex.EncodeToString(kbs[:])
res := GetNodeInfoResponse{key: msg}
return res, nil
}
}

View file

@ -1,61 +0,0 @@
package core
import (
"crypto/ed25519"
"fmt"
"net"
"net/url"
)
func (c *Core) _applyOption(opt SetupOption) (err error) {
switch v := opt.(type) {
case Peer:
u, err := url.Parse(v.URI)
if err != nil {
return fmt.Errorf("unable to parse peering URI: %w", err)
}
err = c.links.add(u, v.SourceInterface, linkTypePersistent)
switch err {
case ErrLinkAlreadyConfigured:
// Don't return this error, otherwise we'll panic at startup
// if there are multiple of the same peer configured
return nil
default:
return err
}
case ListenAddress:
c.config._listeners[v] = struct{}{}
case PeerFilter:
c.config.peerFilter = v
case NodeInfo:
c.config.nodeinfo = v
case NodeInfoPrivacy:
c.config.nodeinfoPrivacy = v
case AllowedPublicKey:
pk := [32]byte{}
copy(pk[:], v)
c.config._allowedPublicKeys[pk] = struct{}{}
}
return
}
type SetupOption interface {
isSetupOption()
}
type ListenAddress string
type Peer struct {
URI string
SourceInterface string
}
type NodeInfo map[string]interface{}
type NodeInfoPrivacy bool
type AllowedPublicKey ed25519.PublicKey
type PeerFilter func(net.IP) bool
func (a ListenAddress) isSetupOption() {}
func (a Peer) isSetupOption() {}
func (a NodeInfo) isSetupOption() {}
func (a NodeInfoPrivacy) isSetupOption() {}
func (a AllowedPublicKey) isSetupOption() {}
func (a PeerFilter) isSetupOption() {}

View file

@ -1,41 +0,0 @@
package core
import (
"net/url"
"testing"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
)
// Tests that duplicate peers in the configuration file
// won't cause an error when the node starts. Otherwise
// we can panic unnecessarily.
func TestDuplicatePeerAtStartup(t *testing.T) {
cfg := config.GenerateConfig()
for i := 0; i < 5; i++ {
cfg.Peers = append(cfg.Peers, "tcp://1.2.3.4:4321")
}
if _, err := New(cfg.Certificate, nil); err != nil {
t.Fatal(err)
}
}
// Tests that duplicate peers given to us through the
// API will still error as expected, even if they didn't
// at startup. We expect to notify the user through the
// admin socket if they try to add a peer that is already
// configured.
func TestDuplicatePeerFromAPI(t *testing.T) {
cfg := config.GenerateConfig()
c, err := New(cfg.Certificate, nil)
if err != nil {
t.Fatal(err)
}
u, _ := url.Parse("tcp://1.2.3.4:4321")
if err := c.AddPeer(u, ""); err != nil {
t.Fatalf("Adding peer failed on first attempt: %s", err)
}
if err := c.AddPeer(u, ""); err == nil {
t.Fatalf("Adding peer should have failed on second attempt")
}
}

View file

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

View file

@ -1,376 +0,0 @@
package core
import (
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net"
"time"
iwt "github.com/Arceliar/ironwood/types"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
)
const (
typeDebugDummy = iota
typeDebugGetSelfRequest
typeDebugGetSelfResponse
typeDebugGetPeersRequest
typeDebugGetPeersResponse
typeDebugGetTreeRequest
typeDebugGetTreeResponse
)
type reqInfo struct {
callback func([]byte)
timer *time.Timer // time.AfterFunc cleanup
}
type keyArray [ed25519.PublicKeySize]byte
type protoHandler struct {
phony.Inbox
core *Core
nodeinfo nodeinfo
selfRequests map[keyArray]*reqInfo
peersRequests map[keyArray]*reqInfo
treeRequests map[keyArray]*reqInfo
}
func (p *protoHandler) init(core *Core) {
p.core = core
p.nodeinfo.init(p)
p.selfRequests = make(map[keyArray]*reqInfo)
p.peersRequests = make(map[keyArray]*reqInfo)
p.treeRequests = make(map[keyArray]*reqInfo)
}
// Common functions
func (p *protoHandler) handleProto(from phony.Actor, key keyArray, bs []byte) {
if len(bs) == 0 {
return
}
switch bs[0] {
case typeProtoDummy:
case typeProtoNodeInfoRequest:
p.nodeinfo.handleReq(p, key)
case typeProtoNodeInfoResponse:
p.nodeinfo.handleRes(p, key, bs[1:])
case typeProtoDebug:
p.handleDebug(from, key, bs[1:])
}
}
func (p *protoHandler) handleDebug(from phony.Actor, key keyArray, bs []byte) {
p.Act(from, func() {
p._handleDebug(key, bs)
})
}
func (p *protoHandler) _handleDebug(key keyArray, bs []byte) {
if len(bs) == 0 {
return
}
switch bs[0] {
case typeDebugDummy:
case typeDebugGetSelfRequest:
p._handleGetSelfRequest(key)
case typeDebugGetSelfResponse:
p._handleGetSelfResponse(key, bs[1:])
case typeDebugGetPeersRequest:
p._handleGetPeersRequest(key)
case typeDebugGetPeersResponse:
p._handleGetPeersResponse(key, bs[1:])
case typeDebugGetTreeRequest:
p._handleGetTreeRequest(key)
case typeDebugGetTreeResponse:
p._handleGetTreeResponse(key, bs[1:])
}
}
func (p *protoHandler) _sendDebug(key keyArray, dType uint8, data []byte) {
bs := append([]byte{typeSessionProto, typeProtoDebug, dType}, data...)
_, _ = p.core.PacketConn.WriteTo(bs, iwt.Addr(key[:]))
}
// Get self
func (p *protoHandler) sendGetSelfRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() {
if info := p.selfRequests[key]; info != nil {
info.timer.Stop()
delete(p.selfRequests, key)
}
info := new(reqInfo)
info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() {
if p.selfRequests[key] == info {
delete(p.selfRequests, key)
}
})
})
p.selfRequests[key] = info
p._sendDebug(key, typeDebugGetSelfRequest, nil)
})
}
func (p *protoHandler) _handleGetSelfRequest(key keyArray) {
self := p.core.GetSelf()
res := map[string]string{
"key": hex.EncodeToString(self.Key[:]),
"routing_entries": fmt.Sprintf("%v", self.RoutingEntries),
}
bs, err := json.Marshal(res) // FIXME this puts keys in base64, not hex
if err != nil {
return
}
p._sendDebug(key, typeDebugGetSelfResponse, bs)
}
func (p *protoHandler) _handleGetSelfResponse(key keyArray, bs []byte) {
if info := p.selfRequests[key]; info != nil {
info.timer.Stop()
info.callback(bs)
delete(p.selfRequests, key)
}
}
// Get peers
func (p *protoHandler) sendGetPeersRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() {
if info := p.peersRequests[key]; info != nil {
info.timer.Stop()
delete(p.peersRequests, key)
}
info := new(reqInfo)
info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() {
if p.peersRequests[key] == info {
delete(p.peersRequests, key)
}
})
})
p.peersRequests[key] = info
p._sendDebug(key, typeDebugGetPeersRequest, nil)
})
}
func (p *protoHandler) _handleGetPeersRequest(key keyArray) {
peers := p.core.GetPeers()
var bs []byte
for _, pinfo := range peers {
tmp := append(bs, pinfo.Key[:]...)
const responseOverhead = 2 // 1 debug type, 1 getpeers type
if uint64(len(tmp))+responseOverhead > p.core.MTU() {
break
}
bs = tmp
}
p._sendDebug(key, typeDebugGetPeersResponse, bs)
}
func (p *protoHandler) _handleGetPeersResponse(key keyArray, bs []byte) {
if info := p.peersRequests[key]; info != nil {
info.timer.Stop()
info.callback(bs)
delete(p.peersRequests, key)
}
}
// Get Tree
func (p *protoHandler) sendGetTreeRequest(key keyArray, callback func([]byte)) {
p.Act(nil, func() {
if info := p.treeRequests[key]; info != nil {
info.timer.Stop()
delete(p.treeRequests, key)
}
info := new(reqInfo)
info.callback = callback
info.timer = time.AfterFunc(time.Minute, func() {
p.Act(nil, func() {
if p.treeRequests[key] == info {
delete(p.treeRequests, key)
}
})
})
p.treeRequests[key] = info
p._sendDebug(key, typeDebugGetTreeRequest, nil)
})
}
func (p *protoHandler) _handleGetTreeRequest(key keyArray) {
dinfos := p.core.GetTree()
var bs []byte
for _, dinfo := range dinfos {
tmp := append(bs, dinfo.Key[:]...)
const responseOverhead = 2 // 1 debug type, 1 gettree type
if uint64(len(tmp))+responseOverhead > p.core.MTU() {
break
}
bs = tmp
}
p._sendDebug(key, typeDebugGetTreeResponse, bs)
}
func (p *protoHandler) _handleGetTreeResponse(key keyArray, bs []byte) {
if info := p.treeRequests[key]; info != nil {
info.timer.Stop()
info.callback(bs)
delete(p.treeRequests, key)
}
}
// Admin socket stuff for "Get self"
type DebugGetSelfRequest struct {
Key string `json:"key"`
}
type DebugGetSelfResponse map[string]interface{}
func (p *protoHandler) getSelfHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetSelfRequest
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
var key keyArray
var kbs []byte
var err error
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err
}
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
p.sendGetSelfRequest(key, func(info []byte) {
ch <- info
})
select {
case <-time.After(6 * time.Second):
return nil, errors.New("timeout")
case info := <-ch:
var msg json.RawMessage
if err := msg.UnmarshalJSON(info); err != nil {
return nil, err
}
ip := net.IP(address.AddrForKey(kbs)[:])
res := DebugGetSelfResponse{ip.String(): msg}
return res, nil
}
}
// Admin socket stuff for "Get peers"
type DebugGetPeersRequest struct {
Key string `json:"key"`
}
type DebugGetPeersResponse map[string]interface{}
func (p *protoHandler) getPeersHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetPeersRequest
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
var key keyArray
var kbs []byte
var err error
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err
}
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
p.sendGetPeersRequest(key, func(info []byte) {
ch <- info
})
select {
case <-time.After(6 * time.Second):
return nil, errors.New("timeout")
case info := <-ch:
ks := make(map[string][]string)
bs := info
for len(bs) >= len(key) {
ks["keys"] = append(ks["keys"], hex.EncodeToString(bs[:len(key)]))
bs = bs[len(key):]
}
js, err := json.Marshal(ks)
if err != nil {
return nil, err
}
var msg json.RawMessage
if err := msg.UnmarshalJSON(js); err != nil {
return nil, err
}
ip := net.IP(address.AddrForKey(kbs)[:])
res := DebugGetPeersResponse{ip.String(): msg}
return res, nil
}
}
// Admin socket stuff for "Get Tree"
type DebugGetTreeRequest struct {
Key string `json:"key"`
}
type DebugGetTreeResponse map[string]interface{}
func (p *protoHandler) getTreeHandler(in json.RawMessage) (interface{}, error) {
var req DebugGetTreeRequest
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
var key keyArray
var kbs []byte
var err error
if kbs, err = hex.DecodeString(req.Key); err != nil {
return nil, err
}
if len(kbs) != ed25519.PublicKeySize {
return nil, fmt.Errorf("invalid public key length")
}
copy(key[:], kbs)
ch := make(chan []byte, 1)
p.sendGetTreeRequest(key, func(info []byte) {
ch <- info
})
select {
case <-time.After(6 * time.Second):
return nil, errors.New("timeout")
case info := <-ch:
ks := make(map[string][]string)
bs := info
for len(bs) >= len(key) {
ks["keys"] = append(ks["keys"], hex.EncodeToString(bs[:len(key)]))
bs = bs[len(key):]
}
js, err := json.Marshal(ks)
if err != nil {
return nil, err
}
var msg json.RawMessage
if err := msg.UnmarshalJSON(js); err != nil {
return nil, err
}
ip := net.IP(address.AddrForKey(kbs)[:])
res := DebugGetTreeResponse{ip.String(): msg}
return res, nil
}
}

View file

@ -1,29 +0,0 @@
package core
import (
"crypto/tls"
"crypto/x509"
)
func (c *Core) generateTLSConfig(cert *tls.Certificate) (*tls.Config, error) {
config := &tls.Config{
Certificates: []tls.Certificate{*cert},
ClientAuth: tls.NoClientCert,
GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
return cert, nil
},
VerifyPeerCertificate: c.verifyTLSCertificate,
VerifyConnection: c.verifyTLSConnection,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
}
return config, nil
}
func (c *Core) verifyTLSCertificate(_ [][]byte, _ [][]*x509.Certificate) error {
return nil
}
func (c *Core) verifyTLSConnection(_ tls.ConnectionState) error {
return nil
}

View file

@ -1,16 +0,0 @@
package core
// In-band packet types
const (
typeSessionDummy = iota // nolint:deadcode,varcheck
typeSessionTraffic
typeSessionProto
)
// Protocol packet types
const (
typeProtoDummy = iota
typeProtoNodeInfoRequest
typeProtoNodeInfoResponse
typeProtoDebug = 255
)

View file

@ -1,169 +0,0 @@
package core
// This file contains the version metadata struct
// Used in the initial connection setup and key exchange
// Some of this could arguably go in wire.go instead
import (
"bytes"
"crypto/ed25519"
"encoding/binary"
"io"
"golang.org/x/crypto/blake2b"
)
// This is the version-specific metadata exchanged at the start of a connection.
// It must always begin with the 4 bytes "meta" and a wire formatted uint64 major version number.
// The current version also includes a minor version number, and the box/sig/link keys that need to be exchanged to open a connection.
type version_metadata struct {
majorVer uint16
minorVer uint16
publicKey ed25519.PublicKey
priority uint8
}
const (
ProtocolVersionMajor uint16 = 0
ProtocolVersionMinor uint16 = 5
)
// Once a major/minor version is released, it is not safe to change any of these
// (including their ordering), it is only safe to add new ones.
const (
metaVersionMajor uint16 = iota // uint16
metaVersionMinor // uint16
metaPublicKey // [32]byte
metaPriority // uint8
)
type handshakeError string
func (e handshakeError) Error() string { return string(e) }
const ErrHandshakeInvalidPreamble = handshakeError("invalid handshake, remote side is not Yggdrasil")
const ErrHandshakeInvalidLength = handshakeError("invalid handshake length, possible version mismatch")
const ErrHandshakeInvalidPassword = handshakeError("invalid password supplied, check your config")
const ErrHandshakeHashFailure = handshakeError("invalid hash length")
const ErrHandshakeIncorrectPassword = handshakeError("password does not match remote side")
// Gets a base metadata with no keys set, but with the correct version numbers.
func version_getBaseMetadata() version_metadata {
return version_metadata{
majorVer: ProtocolVersionMajor,
minorVer: ProtocolVersionMinor,
}
}
// Encodes version metadata into its wire format.
func (m *version_metadata) encode(privateKey ed25519.PrivateKey, password []byte) ([]byte, error) {
bs := make([]byte, 0, 64)
bs = append(bs, 'm', 'e', 't', 'a')
bs = append(bs, 0, 0) // Remaining message length
bs = binary.BigEndian.AppendUint16(bs, metaVersionMajor)
bs = binary.BigEndian.AppendUint16(bs, 2)
bs = binary.BigEndian.AppendUint16(bs, m.majorVer)
bs = binary.BigEndian.AppendUint16(bs, metaVersionMinor)
bs = binary.BigEndian.AppendUint16(bs, 2)
bs = binary.BigEndian.AppendUint16(bs, m.minorVer)
bs = binary.BigEndian.AppendUint16(bs, metaPublicKey)
bs = binary.BigEndian.AppendUint16(bs, ed25519.PublicKeySize)
bs = append(bs, m.publicKey[:]...)
bs = binary.BigEndian.AppendUint16(bs, metaPriority)
bs = binary.BigEndian.AppendUint16(bs, 1)
bs = append(bs, m.priority)
hasher, err := blake2b.New512(password)
if err != nil {
return nil, err
}
n, err := hasher.Write(m.publicKey)
if err != nil {
return nil, err
}
if n != ed25519.PublicKeySize {
return nil, ErrHandshakeHashFailure
}
hash := hasher.Sum(nil)
bs = append(bs, ed25519.Sign(privateKey, hash)...)
binary.BigEndian.PutUint16(bs[4:6], uint16(len(bs)-6))
return bs, nil
}
// Decodes version metadata from its wire format into the struct.
func (m *version_metadata) decode(r io.Reader, password []byte) error {
bh := [6]byte{}
if _, err := io.ReadFull(r, bh[:]); err != nil {
return err
}
meta := [4]byte{'m', 'e', 't', 'a'}
if !bytes.Equal(bh[:4], meta[:]) {
return ErrHandshakeInvalidPreamble
}
hl := binary.BigEndian.Uint16(bh[4:6])
if hl < ed25519.SignatureSize {
return ErrHandshakeInvalidLength
}
bs := make([]byte, hl)
if _, err := io.ReadFull(r, bs); err != nil {
return err
}
sig := bs[len(bs)-ed25519.SignatureSize:]
bs = bs[:len(bs)-ed25519.SignatureSize]
for len(bs) >= 4 {
op := binary.BigEndian.Uint16(bs[:2])
oplen := binary.BigEndian.Uint16(bs[2:4])
if bs = bs[4:]; len(bs) < int(oplen) {
break
}
switch op {
case metaVersionMajor:
m.majorVer = binary.BigEndian.Uint16(bs[:2])
case metaVersionMinor:
m.minorVer = binary.BigEndian.Uint16(bs[:2])
case metaPublicKey:
m.publicKey = make(ed25519.PublicKey, ed25519.PublicKeySize)
copy(m.publicKey, bs[:ed25519.PublicKeySize])
case metaPriority:
m.priority = bs[0]
}
bs = bs[oplen:]
}
hasher, err := blake2b.New512(password)
if err != nil {
return ErrHandshakeInvalidPassword
}
n, err := hasher.Write(m.publicKey)
if err != nil || n != ed25519.PublicKeySize {
return ErrHandshakeHashFailure
}
hash := hasher.Sum(nil)
if !ed25519.Verify(m.publicKey, hash, sig) {
return ErrHandshakeIncorrectPassword
}
return nil
}
// Checks that the "meta" bytes and the version numbers are the expected values.
func (m *version_metadata) check() bool {
switch {
case m.majorVer != ProtocolVersionMajor:
return false
case m.minorVer != ProtocolVersionMinor:
return false
case len(m.publicKey) != ed25519.PublicKeySize:
return false
default:
return true
}
}

View file

@ -1,78 +0,0 @@
package core
import (
"bytes"
"crypto/ed25519"
"reflect"
"testing"
)
func TestVersionPasswordAuth(t *testing.T) {
for _, tt := range []struct {
password1 []byte // The password on node 1
password2 []byte // The password on node 2
allowed bool // Should the connection have been allowed?
}{
{nil, nil, true}, // Allow: No passwords (both nil)
{nil, []byte(""), true}, // Allow: No passwords (mixed nil and empty string)
{nil, []byte("foo"), false}, // Reject: One node has a password, the other doesn't
{[]byte("foo"), []byte(""), false}, // Reject: One node has a password, the other doesn't
{[]byte("foo"), []byte("foo"), true}, // Allow: Same password
{[]byte("foo"), []byte("bar"), false}, // Reject: Different passwords
} {
pk1, sk1, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("Node 1 failed to generate key: %s", err)
}
metadata1 := &version_metadata{
publicKey: pk1,
}
encoded, err := metadata1.encode(sk1, tt.password1)
if err != nil {
t.Fatalf("Node 1 failed to encode metadata: %s", err)
}
var decoded version_metadata
if allowed := decoded.decode(bytes.NewBuffer(encoded), tt.password2) == nil; allowed != tt.allowed {
t.Fatalf("Permutation %q -> %q should have been %v but was %v", tt.password1, tt.password2, tt.allowed, allowed)
}
}
}
func TestVersionRoundtrip(t *testing.T) {
for _, password := range [][]byte{
nil, []byte(""), []byte("foo"),
} {
for _, test := range []*version_metadata{
{majorVer: 1},
{majorVer: 256},
{majorVer: 2, minorVer: 4},
{majorVer: 2, minorVer: 257},
{majorVer: 258, minorVer: 259},
{majorVer: 3, minorVer: 5, priority: 6},
{majorVer: 260, minorVer: 261, priority: 7},
} {
// Generate a random public key for each time, since it is
// a required field.
pk, sk, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
test.publicKey = pk
meta, err := test.encode(sk, password)
if err != nil {
t.Fatal(err)
}
encoded := bytes.NewBuffer(meta)
decoded := &version_metadata{}
if err := decoded.decode(encoded, password); err != nil {
t.Fatalf("failed to decode: %s", err)
}
if !reflect.DeepEqual(test, decoded) {
t.Fatalf("round-trip failed\nwant: %+v\n got: %+v", test, decoded)
}
}
}
}

311
src/crypto/crypto.go Normal file
View file

@ -0,0 +1,311 @@
// Package crypto is a wrapper around packages under golang.org/x/crypto/, particulaly curve25519, ed25519, and nacl/box.
// This is used to avoid explicitly importing and using these packages throughout yggdrasil.
// It also includes the all-important NodeID and TreeID types, which are used to identify nodes in the DHT and in the spanning tree's root selection algorithm, respectively.
package crypto
/*
This part of the package wraps crypto operations needed elsewhere
In particular, it exposes key generation for ed25519 and nacl box
It also defines NodeID and TreeID as hashes of keys, and wraps hash functions
*/
import (
"crypto/rand"
"crypto/sha512"
"encoding/hex"
"sync"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/nacl/box"
)
////////////////////////////////////////////////////////////////////////////////
// NodeID and TreeID
// NodeIDLen is the length (in bytes) of a NodeID.
const NodeIDLen = sha512.Size
// TreeIDLen is the length (in bytes) of a TreeID.
const TreeIDLen = sha512.Size
// handleLen is the length (in bytes) of a Handle.
const handleLen = 8
// NodeID is how a yggdrasil node is identified in the DHT, and is used to derive IPv6 addresses and subnets in the main executable. It is a sha512sum hash of the node's BoxPubKey
type NodeID [NodeIDLen]byte
// TreeID is how a yggdrasil node is identified in the root selection algorithm used to construct the spanning tree.
type TreeID [TreeIDLen]byte
type Handle [handleLen]byte
func (n *NodeID) String() string {
return hex.EncodeToString(n[:])
}
// Network returns "nodeid" nearly always right now.
func (n *NodeID) Network() string {
return "nodeid"
}
// PrefixLength returns the number of bits set in a masked NodeID.
func (n *NodeID) PrefixLength() int {
var len int
for i, v := range *n {
_, _ = i, v
if v == 0xff {
len += 8
continue
}
for v&0x80 != 0 {
len++
v <<= 1
}
if v != 0 {
return -1
}
for i++; i < NodeIDLen; i++ {
if n[i] != 0 {
return -1
}
}
break
}
return len
}
// GetNodeID returns the NodeID associated with a BoxPubKey.
func GetNodeID(pub *BoxPubKey) *NodeID {
h := sha512.Sum512(pub[:])
return (*NodeID)(&h)
}
// GetTreeID returns the TreeID associated with a BoxPubKey
func GetTreeID(pub *SigPubKey) *TreeID {
h := sha512.Sum512(pub[:])
return (*TreeID)(&h)
}
// NewHandle returns a new (cryptographically random) Handle, used by the session code to identify which session an incoming packet is associated with.
func NewHandle() *Handle {
var h Handle
_, err := rand.Read(h[:])
if err != nil {
panic(err)
}
return &h
}
////////////////////////////////////////////////////////////////////////////////
// Signatures
// SigPubKeyLen is the length of a SigPubKey in bytes.
const SigPubKeyLen = ed25519.PublicKeySize
// SigPrivKeyLen is the length of a SigPrivKey in bytes.
const SigPrivKeyLen = ed25519.PrivateKeySize
// SigLen is the length of SigBytes.
const SigLen = ed25519.SignatureSize
// SigPubKey is a public ed25519 signing key.
type SigPubKey [SigPubKeyLen]byte
// SigPrivKey is a private ed25519 signing key.
type SigPrivKey [SigPrivKeyLen]byte
// SigBytes is an ed25519 signature.
type SigBytes [SigLen]byte
// NewSigKeys generates a public/private ed25519 key pair.
func NewSigKeys() (*SigPubKey, *SigPrivKey) {
var pub SigPubKey
var priv SigPrivKey
pubSlice, privSlice, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
copy(pub[:], pubSlice)
copy(priv[:], privSlice)
return &pub, &priv
}
// Sign returns the SigBytes signing a message.
func Sign(priv *SigPrivKey, msg []byte) *SigBytes {
var sig SigBytes
sigSlice := ed25519.Sign(priv[:], msg)
copy(sig[:], sigSlice)
return &sig
}
// Verify returns true if the provided signature matches the key and message.
func Verify(pub *SigPubKey, msg []byte, sig *SigBytes) bool {
// Should sig be an array instead of a slice?...
// It's fixed size, but
return ed25519.Verify(pub[:], msg, sig[:])
}
// Public returns the SigPubKey associated with this SigPrivKey.
func (p SigPrivKey) Public() SigPubKey {
priv := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
copy(priv[:], p[:])
pub := priv.Public().(ed25519.PublicKey)
var sigPub SigPubKey
copy(sigPub[:], pub[:])
return sigPub
}
////////////////////////////////////////////////////////////////////////////////
// NaCl-like crypto "box" (curve25519+xsalsa20+poly1305)
// BoxPubKeyLen is the length of a BoxPubKey in bytes.
const BoxPubKeyLen = 32
// BoxPrivKeyLen is the length of a BoxPrivKey in bytes.
const BoxPrivKeyLen = 32
// BoxSharedKeyLen is the length of a BoxSharedKey in bytes.
const BoxSharedKeyLen = 32
// BoxNonceLen is the length of a BoxNonce in bytes.
const BoxNonceLen = 24
// BoxOverhead is the length of the overhead from boxing something.
const BoxOverhead = box.Overhead
// BoxPubKey is a NaCl-like "box" public key (curve25519+xsalsa20+poly1305).
type BoxPubKey [BoxPubKeyLen]byte
// BoxPrivKey is a NaCl-like "box" private key (curve25519+xsalsa20+poly1305).
type BoxPrivKey [BoxPrivKeyLen]byte
// BoxSharedKey is a NaCl-like "box" shared key (curve25519+xsalsa20+poly1305).
type BoxSharedKey [BoxSharedKeyLen]byte
// BoxNonce is the nonce used in NaCl-like crypto "box" operations (curve25519+xsalsa20+poly1305), and must not be reused for different messages encrypted using the same BoxSharedKey.
type BoxNonce [BoxNonceLen]byte
// String returns a string representation of the "box" key.
func (k BoxPubKey) String() string {
return hex.EncodeToString(k[:])
}
// Network returns "curve25519" for "box" keys.
func (n BoxPubKey) Network() string {
return "curve25519"
}
// NewBoxKeys generates a new pair of public/private crypto box keys.
func NewBoxKeys() (*BoxPubKey, *BoxPrivKey) {
pubBytes, privBytes, err := box.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
pub := (*BoxPubKey)(pubBytes)
priv := (*BoxPrivKey)(privBytes)
return pub, priv
}
// GetSharedKey returns the shared key derived from your private key and the destination's public key.
func GetSharedKey(myPrivKey *BoxPrivKey,
othersPubKey *BoxPubKey) *BoxSharedKey {
var shared [BoxSharedKeyLen]byte
priv := (*[BoxPrivKeyLen]byte)(myPrivKey)
pub := (*[BoxPubKeyLen]byte)(othersPubKey)
box.Precompute(&shared, pub, priv)
return (*BoxSharedKey)(&shared)
}
// pool is used internally by BoxOpen and BoxSeal to avoid allocating temporary space
var pool = sync.Pool{New: func() interface{} { return []byte(nil) }}
// BoxOpen returns a message and true if it successfully opens a crypto box using the provided shared key and nonce.
// The boxed input slice's backing array is reused for the unboxed output when possible.
func BoxOpen(shared *BoxSharedKey,
boxed []byte,
nonce *BoxNonce) ([]byte, bool) {
s := (*[BoxSharedKeyLen]byte)(shared)
n := (*[BoxNonceLen]byte)(nonce)
temp := append(pool.Get().([]byte), boxed...)
unboxed, success := box.OpenAfterPrecomputation(boxed[:0], temp, n, s)
pool.Put(temp[:0])
return unboxed, success
}
// BoxSeal seals a crypto box using the provided shared key, returning the box and the nonce needed to decrypt it.
// If nonce is nil, a random BoxNonce will be used and returned.
// If nonce is non-nil, then nonce.Increment() will be called before using it, and the incremented BoxNonce is what is returned.
// The unboxed input slice's backing array is reused for the boxed output when possible.
func BoxSeal(shared *BoxSharedKey, unboxed []byte, nonce *BoxNonce) ([]byte, *BoxNonce) {
if nonce == nil {
nonce = NewBoxNonce()
}
nonce.Increment()
s := (*[BoxSharedKeyLen]byte)(shared)
n := (*[BoxNonceLen]byte)(nonce)
temp := append(pool.Get().([]byte), unboxed...)
boxed := box.SealAfterPrecomputation(unboxed[:0], temp, n, s)
pool.Put(temp[:0])
return boxed, nonce
}
// NewBoxNonce generates a (cryptographically) random BoxNonce.
func NewBoxNonce() *BoxNonce {
var nonce BoxNonce
_, err := rand.Read(nonce[:])
for ; err == nil && nonce[0] == 0xff; _, err = rand.Read(nonce[:]) {
// Make sure nonce isn't too high
// This is just to make rollover unlikely to happen
// Rollover is fine, but it may kill the session and force it to reopen
}
if err != nil {
panic(err)
}
return &nonce
}
// Increment adds 2 to a BoxNonce, which is useful if one node intends to send only with odd BoxNonce values, and the other only with even BoxNonce values.
func (n *BoxNonce) Increment() {
oldNonce := *n
n[len(n)-1] += 2
for i := len(n) - 2; i >= 0; i-- {
if n[i+1] < oldNonce[i+1] {
n[i]++
}
}
}
// Public returns the BoxPubKey associated with this BoxPrivKey.
func (p BoxPrivKey) Public() BoxPubKey {
var boxPub [BoxPubKeyLen]byte
var boxPriv [BoxPrivKeyLen]byte
copy(boxPriv[:BoxPrivKeyLen], p[:BoxPrivKeyLen])
curve25519.ScalarBaseMult(&boxPub, &boxPriv)
return boxPub
}
// Minus is the result of subtracting the provided BoNonce from this BoxNonce, bounded at +- 64.
// It's primarily used to determine if a new BoxNonce is higher than the last known BoxNonce from a crypto session, and by how much.
// This is used in the machinery that makes sure replayed packets can't keep a session open indefinitely or stuck using old/bad information about a node.
func (n *BoxNonce) Minus(m *BoxNonce) int64 {
diff := int64(0)
for idx := range n {
diff *= 256
diff += int64(n[idx]) - int64(m[idx])
if diff > 64 {
diff = 64
}
if diff < -64 {
diff = -64
}
}
return diff
}

22
src/defaults/defaults.go Normal file
View file

@ -0,0 +1,22 @@
package defaults
import "github.com/yggdrasil-network/yggdrasil-go/src/types"
// Defines which parameters are expected by default for configuration on a
// specific platform. These values are populated in the relevant defaults_*.go
// for the platform being targeted. They must be set.
type platformDefaultParameters struct {
// Admin socket
DefaultAdminListen string
// Configuration (used for yggdrasilctl)
DefaultConfigFile string
// Multicast interfaces
DefaultMulticastInterfaces []string
// TUN/TAP
MaximumIfMTU types.MTU
DefaultIfMTU types.MTU
DefaultIfName string
}

View file

@ -1,11 +1,10 @@
//go:build darwin
// +build darwin
package config
package defaults
// Sane defaults for the macOS/Darwin platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() platformDefaultParameters {
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@ -14,13 +13,12 @@ func getDefaults() platformDefaultParameters {
DefaultConfigFile: "/etc/yggdrasil.conf",
// Multicast interfaces
DefaultMulticastInterfaces: []MulticastInterfaceConfig{
{Regex: "en.*", Beacon: true, Listen: true},
{Regex: "bridge.*", Beacon: true, Listen: true},
{Regex: "awdl0", Beacon: false, Listen: false},
DefaultMulticastInterfaces: []string{
"en.*",
"bridge.*",
},
// TUN
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "auto",

View file

@ -1,11 +1,10 @@
//go:build freebsd
// +build freebsd
package config
package defaults
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() platformDefaultParameters {
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@ -14,11 +13,11 @@ func getDefaults() platformDefaultParameters {
DefaultConfigFile: "/usr/local/etc/yggdrasil.conf",
// Multicast interfaces
DefaultMulticastInterfaces: []MulticastInterfaceConfig{
{Regex: ".*", Beacon: true, Listen: true},
DefaultMulticastInterfaces: []string{
".*",
},
// TUN
// TUN/TAP
MaximumIfMTU: 32767,
DefaultIfMTU: 32767,
DefaultIfName: "/dev/tun0",

View file

@ -1,11 +1,10 @@
//go:build linux
// +build linux
package config
package defaults
// Sane defaults for the Linux platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() platformDefaultParameters {
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@ -14,11 +13,11 @@ func getDefaults() platformDefaultParameters {
DefaultConfigFile: "/etc/yggdrasil.conf",
// Multicast interfaces
DefaultMulticastInterfaces: []MulticastInterfaceConfig{
{Regex: ".*", Beacon: true, Listen: true},
DefaultMulticastInterfaces: []string{
".*",
},
// TUN
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "auto",

View file

@ -1,11 +1,10 @@
//go:build openbsd
// +build openbsd
package config
package defaults
// Sane defaults for the BSD platforms. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() platformDefaultParameters {
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "unix:///var/run/yggdrasil.sock",
@ -14,11 +13,11 @@ func getDefaults() platformDefaultParameters {
DefaultConfigFile: "/etc/yggdrasil.conf",
// Multicast interfaces
DefaultMulticastInterfaces: []MulticastInterfaceConfig{
{Regex: ".*", Beacon: true, Listen: true},
DefaultMulticastInterfaces: []string{
".*",
},
// TUN
// TUN/TAP
MaximumIfMTU: 16384,
DefaultIfMTU: 16384,
DefaultIfName: "tun0",

View file

@ -1,11 +1,10 @@
//go:build !linux && !darwin && !windows && !openbsd && !freebsd
// +build !linux,!darwin,!windows,!openbsd,!freebsd
package config
package defaults
// Sane defaults for the other platforms. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() platformDefaultParameters {
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "tcp://localhost:9001",
@ -14,11 +13,11 @@ func getDefaults() platformDefaultParameters {
DefaultConfigFile: "/etc/yggdrasil.conf",
// Multicast interfaces
DefaultMulticastInterfaces: []MulticastInterfaceConfig{
{Regex: ".*", Beacon: true, Listen: true},
DefaultMulticastInterfaces: []string{
".*",
},
// TUN
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "none",

View file

@ -1,11 +1,10 @@
//go:build windows
// +build windows
package config
package defaults
// Sane defaults for the Windows platform. The "default" options may be
// may be replaced by the running configuration.
func getDefaults() platformDefaultParameters {
func GetDefaults() platformDefaultParameters {
return platformDefaultParameters{
// Admin
DefaultAdminListen: "tcp://localhost:9001",
@ -14,11 +13,11 @@ func getDefaults() platformDefaultParameters {
DefaultConfigFile: "C:\\Program Files\\Yggdrasil\\yggdrasil.conf",
// Multicast interfaces
DefaultMulticastInterfaces: []MulticastInterfaceConfig{
{Regex: ".*", Beacon: true, Listen: true},
DefaultMulticastInterfaces: []string{
".*",
},
// TUN
// TUN/TAP
MaximumIfMTU: 65535,
DefaultIfMTU: 65535,
DefaultIfName: "Yggdrasil",

View file

@ -1,368 +0,0 @@
package ipv6rwc
import (
"crypto/ed25519"
"errors"
"fmt"
"net"
"sync"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv6"
iwt "github.com/Arceliar/ironwood/types"
"github.com/yggdrasil-network/yggdrasil-go/src/address"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
)
const keyStoreTimeout = 2 * time.Minute
/*
// Out-of-band packet types
const (
typeKeyDummy = iota // nolint:deadcode,varcheck
typeKeyLookup
typeKeyResponse
)
*/
type keyArray [ed25519.PublicKeySize]byte
type keyStore struct {
core *core.Core
address address.Address
subnet address.Subnet
mutex sync.Mutex
keyToInfo map[keyArray]*keyInfo
addrToInfo map[address.Address]*keyInfo
addrBuffer map[address.Address]*buffer
subnetToInfo map[address.Subnet]*keyInfo
subnetBuffer map[address.Subnet]*buffer
mtu uint64
}
type keyInfo struct {
key keyArray
address address.Address
subnet address.Subnet
timeout *time.Timer // From calling a time.AfterFunc to do cleanup
}
type buffer struct {
packet []byte
timeout *time.Timer
}
func (k *keyStore) init(c *core.Core) {
k.core = c
k.address = *address.AddrForKey(k.core.PublicKey())
k.subnet = *address.SubnetForKey(k.core.PublicKey())
/*if err := k.core.SetOutOfBandHandler(k.oobHandler); err != nil {
err = fmt.Errorf("tun.core.SetOutOfBandHander: %w", err)
panic(err)
}*/
k.core.SetPathNotify(func(key ed25519.PublicKey) {
k.update(key)
})
k.keyToInfo = make(map[keyArray]*keyInfo)
k.addrToInfo = make(map[address.Address]*keyInfo)
k.addrBuffer = make(map[address.Address]*buffer)
k.subnetToInfo = make(map[address.Subnet]*keyInfo)
k.subnetBuffer = make(map[address.Subnet]*buffer)
k.mtu = 1280 // Default to something safe, expect user to set this
}
func (k *keyStore) sendToAddress(addr address.Address, bs []byte) {
k.mutex.Lock()
if info := k.addrToInfo[addr]; info != nil {
k.resetTimeout(info)
k.mutex.Unlock()
_, _ = k.core.WriteTo(bs, iwt.Addr(info.key[:]))
} else {
var buf *buffer
if buf = k.addrBuffer[addr]; buf == nil {
buf = new(buffer)
k.addrBuffer[addr] = buf
}
msg := append([]byte(nil), bs...)
buf.packet = msg
if buf.timeout != nil {
buf.timeout.Stop()
}
buf.timeout = time.AfterFunc(keyStoreTimeout, func() {
k.mutex.Lock()
defer k.mutex.Unlock()
if nbuf := k.addrBuffer[addr]; nbuf == buf {
delete(k.addrBuffer, addr)
}
})
k.mutex.Unlock()
k.sendKeyLookup(addr.GetKey())
}
}
func (k *keyStore) sendToSubnet(subnet address.Subnet, bs []byte) {
k.mutex.Lock()
if info := k.subnetToInfo[subnet]; info != nil {
k.resetTimeout(info)
k.mutex.Unlock()
_, _ = k.core.WriteTo(bs, iwt.Addr(info.key[:]))
} else {
var buf *buffer
if buf = k.subnetBuffer[subnet]; buf == nil {
buf = new(buffer)
k.subnetBuffer[subnet] = buf
}
msg := append([]byte(nil), bs...)
buf.packet = msg
if buf.timeout != nil {
buf.timeout.Stop()
}
buf.timeout = time.AfterFunc(keyStoreTimeout, func() {
k.mutex.Lock()
defer k.mutex.Unlock()
if nbuf := k.subnetBuffer[subnet]; nbuf == buf {
delete(k.subnetBuffer, subnet)
}
})
k.mutex.Unlock()
k.sendKeyLookup(subnet.GetKey())
}
}
func (k *keyStore) update(key ed25519.PublicKey) *keyInfo {
k.mutex.Lock()
var kArray keyArray
copy(kArray[:], key)
var info *keyInfo
var packets [][]byte
if info = k.keyToInfo[kArray]; info == nil {
info = new(keyInfo)
info.key = kArray
info.address = *address.AddrForKey(ed25519.PublicKey(info.key[:]))
info.subnet = *address.SubnetForKey(ed25519.PublicKey(info.key[:]))
k.keyToInfo[info.key] = info
k.addrToInfo[info.address] = info
k.subnetToInfo[info.subnet] = info
if buf := k.addrBuffer[info.address]; buf != nil {
packets = append(packets, buf.packet)
delete(k.addrBuffer, info.address)
}
if buf := k.subnetBuffer[info.subnet]; buf != nil {
packets = append(packets, buf.packet)
delete(k.subnetBuffer, info.subnet)
}
}
k.resetTimeout(info)
k.mutex.Unlock()
for _, packet := range packets {
_, _ = k.core.WriteTo(packet, iwt.Addr(info.key[:]))
}
return info
}
func (k *keyStore) resetTimeout(info *keyInfo) {
if info.timeout != nil {
info.timeout.Stop()
}
info.timeout = time.AfterFunc(keyStoreTimeout, func() {
k.mutex.Lock()
defer k.mutex.Unlock()
if nfo := k.keyToInfo[info.key]; nfo == info {
delete(k.keyToInfo, info.key)
}
if nfo := k.addrToInfo[info.address]; nfo == info {
delete(k.addrToInfo, info.address)
}
if nfo := k.subnetToInfo[info.subnet]; nfo == info {
delete(k.subnetToInfo, info.subnet)
}
})
}
/*
func (k *keyStore) oobHandler(fromKey, toKey ed25519.PublicKey, data []byte) { // nolint:unused
if len(data) != 1+ed25519.SignatureSize {
return
}
sig := data[1:]
switch data[0] {
case typeKeyLookup:
snet := *address.SubnetForKey(toKey)
if snet == k.subnet && ed25519.Verify(fromKey, toKey[:], sig) {
// This is looking for at least our subnet (possibly our address)
// Send a response
k.sendKeyResponse(fromKey)
}
case typeKeyResponse:
// TODO keep a list of something to match against...
// Ignore the response if it doesn't match anything of interest...
if ed25519.Verify(fromKey, toKey[:], sig) {
k.update(fromKey)
}
}
}
*/
func (k *keyStore) sendKeyLookup(partial ed25519.PublicKey) {
/*
sig := ed25519.Sign(k.core.PrivateKey(), partial[:])
bs := append([]byte{typeKeyLookup}, sig...)
//_ = k.core.SendOutOfBand(partial, bs)
_ = bs
*/
k.core.SendLookup(partial)
}
/*
func (k *keyStore) sendKeyResponse(dest ed25519.PublicKey) { // nolint:unused
sig := ed25519.Sign(k.core.PrivateKey(), dest[:])
bs := append([]byte{typeKeyResponse}, sig...)
//_ = k.core.SendOutOfBand(dest, bs)
_ = bs
}
*/
func (k *keyStore) readPC(p []byte) (int, error) {
buf := make([]byte, k.core.MTU(), 65535)
for {
bs := buf
n, from, err := k.core.ReadFrom(bs)
if err != nil {
return n, err
}
if n == 0 {
continue
}
bs = bs[:n]
if len(bs) == 0 {
continue
}
if bs[0]&0xf0 != 0x60 {
continue // not IPv6
}
if len(bs) < 40 {
continue
}
k.mutex.Lock()
mtu := int(k.mtu)
k.mutex.Unlock()
if len(bs) > mtu {
// Using bs would make it leak off the stack, so copy to buf
buf := make([]byte, 512)
cn := copy(buf, bs)
ptb := &icmp.PacketTooBig{
MTU: mtu,
Data: buf[:cn],
}
if packet, err := CreateICMPv6(buf[8:24], buf[24:40], ipv6.ICMPTypePacketTooBig, 0, ptb); err == nil {
_, _ = k.writePC(packet)
}
continue
}
var srcAddr, dstAddr address.Address
var srcSubnet, dstSubnet address.Subnet
copy(srcAddr[:], bs[8:])
copy(dstAddr[:], bs[24:])
copy(srcSubnet[:], bs[8:])
copy(dstSubnet[:], bs[24:])
if dstAddr != k.address && dstSubnet != k.subnet {
continue // bad local address/subnet
}
info := k.update(ed25519.PublicKey(from.(iwt.Addr)))
if srcAddr != info.address && srcSubnet != info.subnet {
continue // bad remote address/subnet
}
n = copy(p, bs)
return n, nil
}
}
func (k *keyStore) writePC(bs []byte) (int, error) {
if bs[0]&0xf0 != 0x60 {
return 0, errors.New("not an IPv6 packet") // not IPv6
}
if len(bs) < 40 {
strErr := fmt.Sprint("undersized IPv6 packet, length: ", len(bs))
return 0, errors.New(strErr)
}
var srcAddr, dstAddr address.Address
var srcSubnet, dstSubnet address.Subnet
copy(srcAddr[:], bs[8:])
copy(dstAddr[:], bs[24:])
copy(srcSubnet[:], bs[8:])
copy(dstSubnet[:], bs[24:])
if srcAddr != k.address && srcSubnet != k.subnet {
// This happens all the time due to link-local traffic
// Don't send back an error, just drop it
strErr := fmt.Sprint("incorrect source address: ", net.IP(srcAddr[:]).String())
return 0, errors.New(strErr)
}
if dstAddr.IsValid() {
k.sendToAddress(dstAddr, bs)
} else if dstSubnet.IsValid() {
k.sendToSubnet(dstSubnet, bs)
} else {
return 0, errors.New("invalid destination address")
}
return len(bs), nil
}
// Exported API
func (k *keyStore) MaxMTU() uint64 {
return k.core.MTU()
}
func (k *keyStore) SetMTU(mtu uint64) {
if mtu > k.MaxMTU() {
mtu = k.MaxMTU()
}
if mtu < 1280 {
mtu = 1280
}
k.mutex.Lock()
k.mtu = mtu
k.mutex.Unlock()
}
func (k *keyStore) MTU() uint64 {
k.mutex.Lock()
mtu := k.mtu
k.mutex.Unlock()
return mtu
}
type ReadWriteCloser struct {
keyStore
}
func NewReadWriteCloser(c *core.Core) *ReadWriteCloser {
rwc := new(ReadWriteCloser)
rwc.init(c)
return rwc
}
func (rwc *ReadWriteCloser) Address() address.Address {
return rwc.address
}
func (rwc *ReadWriteCloser) Subnet() address.Subnet {
return rwc.subnet
}
func (rwc *ReadWriteCloser) Read(p []byte) (n int, err error) {
return rwc.readPC(p)
}
func (rwc *ReadWriteCloser) Write(p []byte) (n int, err error) {
return rwc.writePC(p)
}
func (rwc *ReadWriteCloser) Close() error {
err := rwc.core.Close()
rwc.core.Stop()
return err
}

20
src/module/module.go Normal file
View file

@ -0,0 +1,20 @@
package module
import (
"github.com/gologme/log"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
"github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
)
// Module is an interface that defines which functions must be supported by a
// given Yggdrasil module.
type Module interface {
Init(core *yggdrasil.Core, state *config.NodeState, log *log.Logger, options interface{}) error
Start() error
Stop() error
UpdateConfig(config *config.NodeConfig)
SetupAdminHandlers(a *admin.AdminSocket)
IsStarted() bool
}

View file

@ -1,64 +1,13 @@
package multicast
import (
"encoding/json"
"slices"
"strings"
"github.com/Arceliar/phony"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
)
type GetMulticastInterfacesRequest struct{}
type GetMulticastInterfacesResponse struct {
Interfaces []MulticastInterfaceState `json:"multicast_interfaces"`
}
type MulticastInterfaceState struct {
Name string `json:"name"`
Address string `json:"address"`
Beacon bool `json:"beacon"`
Listen bool `json:"listen"`
Password bool `json:"password"`
}
func (m *Multicast) getMulticastInterfacesHandler(_ *GetMulticastInterfacesRequest, res *GetMulticastInterfacesResponse) error {
res.Interfaces = []MulticastInterfaceState{}
phony.Block(m, func() {
for name, intf := range m._interfaces {
is := MulticastInterfaceState{
Name: intf.iface.Name,
Beacon: intf.beacon,
Listen: intf.listen,
Password: len(intf.password) > 0,
}
if li := m._listeners[name]; li != nil && li.listener != nil {
is.Address = li.listener.Addr().String()
} else {
is.Address = "-"
}
res.Interfaces = append(res.Interfaces, is)
}
})
slices.SortStableFunc(res.Interfaces, func(a, b MulticastInterfaceState) int {
return strings.Compare(a.Name, b.Name)
})
return nil
}
import "github.com/yggdrasil-network/yggdrasil-go/src/admin"
func (m *Multicast) SetupAdminHandlers(a *admin.AdminSocket) {
_ = a.AddHandler(
"getMulticastInterfaces", "Show which interfaces multicast is enabled on", []string{},
func(in json.RawMessage) (interface{}, error) {
req := &GetMulticastInterfacesRequest{}
res := &GetMulticastInterfacesResponse{}
if err := json.Unmarshal(in, &req); err != nil {
return nil, err
}
if err := m.getMulticastInterfacesHandler(req, res); err != nil {
return nil, err
}
return res, nil
},
)
a.AddHandler("getMulticastInterfaces", []string{}, func(in admin.Info) (admin.Info, error) {
var intfs []string
for _, v := range m.Interfaces() {
intfs = append(intfs, v.Name)
}
return admin.Info{"multicast_interfaces": intfs}, nil
})
}

View file

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

Some files were not shown because too many files have changed in this diff Show more